Esempio n. 1
0
        public LiBoProtocol(IButtplugLogManager aLogManager,
                            IButtplugDeviceImpl aInterface)
            : base(aLogManager,
                   $"LiBo ({aInterface.Name})",
                   aInterface)
        {
            if (DevInfos.ContainsKey(aInterface.Name))
            {
                _devInfo = DevInfos[aInterface.Name];
                Name     = $"LiBo {_devInfo.Name}";
            }
            else
            {
                // Pick the single vibe baseline
                BpLogger.Warn($"Cannot identify device {Name}, defaulting to LuLu settings.");
                _devInfo = DevInfos["LuXiaoHan"];
            }

            AddMessageHandler <StopDeviceCmd>(HandleStopDeviceCmd);
            if (_devInfo.VibeCount > 0)
            {
                AddMessageHandler <SingleMotorVibrateCmd>(HandleSingleMotorVibrateCmd);
                AddMessageHandler <VibrateCmd>(HandleVibrateCmd,
                                               new MessageAttributes {
                    FeatureCount = _devInfo.VibeCount
                });
            }

            // TODO Add an explicit handler for Estim shocking, kegel pressure and add a battery handler.
        }
Esempio n. 2
0
        public KiirooGen2Vibe([NotNull] IButtplugLogManager aLogManager,
                              [NotNull] IBluetoothDeviceInterface aInterface,
                              [NotNull] IBluetoothDeviceInfo aInfo)
            : base(aLogManager,
                   "Kiiroo Unknown",
                   aInterface,
                   aInfo)
        {
            if (DevInfos.ContainsKey(aInterface.Name))
            {
                Name     = $"{DevInfos[aInterface.Name].Brand} {aInterface.Name}";
                _devInfo = DevInfos[aInterface.Name];
            }
            else
            {
                BpLogger.Warn($"Cannot identify device {Name}, defaulting to Pearl2 settings.");
                _devInfo = DevInfos["Unknown"];
            }

            AddMessageHandler <StopDeviceCmd>(HandleStopDeviceCmd);
            AddMessageHandler <VibrateCmd>(HandleVibrateCmd, new MessageAttributes {
                FeatureCount = _devInfo.VibeCount
            });
            AddMessageHandler <SingleMotorVibrateCmd>(HandleSingleMotorVibrateCmd);
        }
Esempio n. 3
0
        public CuemeProtocol(IButtplugLogManager aLogManager,
                             IButtplugDeviceImpl aInterface)
            : base(aLogManager,
                   "Cueme Unknown",
                   aInterface)
        {
            var bits = aInterface.Name.Split('_');

            if (bits.Length == 3 && uint.TryParse(bits[2], out var typeNum) && DevInfos.ContainsKey(typeNum))
            {
                _devInfo = DevInfos[typeNum];
            }
            else
            {
                BpLogger.Warn($"Cannot identify Cueme device {Name}, defaulting to Womens settings.");
                _devInfo = DevInfos[3];
            }

            Name = $"Cueme {_devInfo.Name}";

            // Create a new timer that wont fire any events just yet
            _updateValueTimer.Interval = DelayTimeMS;
            _updateValueTimer.Elapsed += CuemeUpdateHandler;
            _updateValueTimer.Enabled  = false;
            aInterface.DeviceRemoved  += OnDeviceRemoved;

            AddMessageHandler <StopDeviceCmd>(HandleStopDeviceCmd);
            AddMessageHandler <SingleMotorVibrateCmd>(HandleSingleMotorVibrateCmd);
            AddMessageHandler <VibrateCmd>(HandleVibrateCmd, new MessageAttributes {
                FeatureCount = _devInfo.VibeCount
            });
        }
Esempio n. 4
0
        public MagicMotion(IButtplugLogManager aLogManager,
                           IBluetoothDeviceInterface aInterface,
                           IBluetoothDeviceInfo aInfo)
            : base(aLogManager,
                   $"Unknown MagicMotion Device ({aInterface.Name})",
                   aInterface,
                   aInfo)
        {
            if (DevInfos.ContainsKey(aInterface.Name))
            {
                Name     = $"MagicMotion {DevInfos[aInterface.Name].Name}";
                _devInfo = DevInfos[aInterface.Name];
            }
            else
            {
                BpLogger.Warn($"Cannot identify device {Name}, defaulting to Smart Mini Vibe settings.");
                _devInfo = DevInfos["Smart Mini Vibe"];
            }

            AddMessageHandler <SingleMotorVibrateCmd>(HandleSingleMotorVibrateCmd);
            AddMessageHandler <VibrateCmd>(HandleVibrateCmd, new MessageAttributes {
                FeatureCount = _devInfo.VibeCount
            });
            AddMessageHandler <StopDeviceCmd>(HandleStopDeviceCmd);
        }
Esempio n. 5
0
        public VorzeSA(IButtplugLogManager aLogManager,
                       IBluetoothDeviceInterface aInterface,
                       IBluetoothDeviceInfo aInfo)
            : base(aLogManager,
                   "Vorze SA Unknown",
                   aInterface,
                   aInfo)
        {
            if (aInterface.Name == "CycSA")
            {
                _deviceType = DeviceType.CycloneOrUnknown;
                Name        = "Vorze A10 Cyclone SA";
            }
            else if (aInterface.Name == "UFOSA")
            {
                _deviceType = DeviceType.UFO;
                Name        = "Vorze UFO SA";
            }
            else
            {
                // If the device doesn't identify, warn and try sending it Cyclone packets.
                BpLogger.Warn($"Vorze product with unrecognized name ({Name}) found. This product may not work with B******g. Contact the developers for more info.");
            }

            MsgFuncs.Add(typeof(VorzeA10CycloneCmd), new ButtplugDeviceWrapper(HandleVorzeA10CycloneCmd));
            MsgFuncs.Add(typeof(RotateCmd), new ButtplugDeviceWrapper(HandleRotateCmd, new MessageAttributes()
            {
                FeatureCount = 1
            }));
            MsgFuncs.Add(typeof(StopDeviceCmd), new ButtplugDeviceWrapper(HandleStopDeviceCmd));
        }
Esempio n. 6
0
        public VorzeSA(IButtplugLogManager aLogManager,
                       IBluetoothDeviceInterface aInterface,
                       IBluetoothDeviceInfo aInfo)
            : base(aLogManager,
                   "Vorze SA Unknown",
                   aInterface,
                   aInfo)
        {
            switch (aInterface.Name)
            {
            case "CycSA":
                _deviceType  = DeviceType.CycloneOrUnknown;
                _commandType = CommandType.Rotate;
                Name         = "Vorze A10 Cyclone SA";
                break;

            case "UFOSA":
                _deviceType  = DeviceType.UFO;
                _commandType = CommandType.Rotate;
                Name         = "Vorze UFO SA";
                break;

            case "Bach smart":
                _deviceType  = DeviceType.Bach;
                _commandType = CommandType.Vibrate;
                Name         = "Vorze Bach";
                break;

            default:
                // If the device doesn't identify, warn and try sending it Cyclone packets.
                BpLogger.Warn($"Vorze product with unrecognized name ({Name}) found. This product may not work with B******g. Contact the developers for more info.");
                break;
            }

            switch (_commandType)
            {
            case CommandType.Rotate:
                AddMessageHandler <VorzeA10CycloneCmd>(HandleVorzeA10CycloneCmd);
                AddMessageHandler <RotateCmd>(HandleRotateCmd, new MessageAttributes()
                {
                    FeatureCount = 1
                });
                break;

            case CommandType.Vibrate:
                AddMessageHandler <SingleMotorVibrateCmd>(HandleSingleMotorVibrateCmd);
                AddMessageHandler <VibrateCmd>(HandleVibrateCmd, new MessageAttributes()
                {
                    FeatureCount = 1
                });
                break;

            default:
                BpLogger.Error("Unhandled command type.");
                break;
            }

            AddMessageHandler <StopDeviceCmd>(HandleStopDeviceCmd);
        }
Esempio n. 7
0
        public override void StartScanning()
        {
            _scanning = true;
            var hidDevices = new HidEnumerator();

            foreach (var hid in hidDevices.Enumerate())
            {
                try
                {
                    hid.ReadProduct(out var product);
                    hid.ReadManufacturer(out var vendor);
                    var prod = Encoding.Unicode.GetString(product);
                    var vend = Encoding.Unicode.GetString(vendor);
                    prod = prod.Substring(0, prod.IndexOf('\0'));
                    vend = vend.Substring(0, vend.IndexOf('\0'));

                    BpLogger.Trace("Found HID device (" +
                                   hid.Attributes.VendorHexId + ":" + hid.Attributes.ProductHexId +
                                   "): " + vend + " - " + prod);

                    var factories = _deviceFactories.Where(x =>
                                                           x.MayBeDevice(hid.Attributes.VendorId, hid.Attributes.ProductId));
                    var buttplugHidDeviceFactories = factories as HidDeviceFactory[] ?? factories.ToArray();
                    if (buttplugHidDeviceFactories.Length != 1)
                    {
                        if (buttplugHidDeviceFactories.Any())
                        {
                            BpLogger.Warn($"Found multiple HID factories for {hid.Attributes.VendorHexId}:{hid.Attributes.ProductHexId}");
                            buttplugHidDeviceFactories.ToList().ForEach(x => BpLogger.Warn(x.GetType().Name));
                        }
                        else
                        {
                            // BpLogger.Trace("No BLE factories found for device.");
                        }

                        continue;
                    }

                    var factory = buttplugHidDeviceFactories.First();
                    BpLogger.Debug($"Found HID factory: {factory.GetType().Name}");

                    var d = factory.CreateDevice(hid);
                    InvokeDeviceAdded(new DeviceAddedEventArgs(d));
                }
                catch (Exception e)
                {
                    // TODO Figure out what exceptions can actually be thrown here.
                    BpLogger.Error(e.Message);
                }
            }

            _scanning = false;
            InvokeScanningFinished();
        }
Esempio n. 8
0
        public UWPBluetoothManager(IButtplugLogManager aLogManager)
            : base(aLogManager)
        {
            BpLogger.Info("Loading UWP Bluetooth Manager");
            _currentlyConnecting = new List <ulong>();

            // Introspect the ButtplugDevices namespace for all Factory classes, then create
            // instances of all of them.
            _deviceFactories = new List <UWPBluetoothDeviceFactory>();
            BuiltinDevices.ForEach(aDeviceFactory =>
            {
                BpLogger.Debug($"Loading Bluetooth Device Factory: {aDeviceFactory.GetType().Name}");
                _deviceFactories.Add(new UWPBluetoothDeviceFactory(aLogManager, aDeviceFactory));
            });

            _bleWatcher = new BluetoothLEAdvertisementWatcher {
                ScanningMode = BluetoothLEScanningMode.Active
            };

            // We can't filter device advertisements because you can only add one LocalName filter at
            // a time, meaning we would have to set up multiple watchers for multiple devices. We'll
            // handle our own filtering via the factory classes whenever we receive a device.
            _bleWatcher.Received += OnAdvertisementReceived;
            _bleWatcher.Stopped  += OnWatcherStopped;
            var adapterTask = Task.Run(() => BluetoothAdapter.GetDefaultAsync().AsTask());

            adapterTask.Wait();
            var adapter = adapterTask.Result;

            if (adapter == null)
            {
                BpLogger.Warn("No bluetooth adapter available for UWP Bluetooth Manager Connection");
                return;
            }

            if (!adapter.IsLowEnergySupported)
            {
                BpLogger.Warn("Bluetooth adapter available but does not support Bluetooth Low Energy.");
                return;
            }

            BpLogger.Debug("UWP Manager found working Bluetooth LE Adapter");

            // Only run radio information lookup if we're actually logging at the level it will show.
            if (aLogManager.Level >= ButtplugLogLevel.Debug)
            {
                // Do radio lookup in a background task, as the search query is very slow.
                // TODO Should probably try and cancel this if it's still running on object destruction, but the Get() call is uninterruptable?
                _radioTask = Task.Run(() => LogBluetoothRadioInfo());
            }
        }
Esempio n. 9
0
        public XamarinBluetoothManager(IButtplugLogManager aLogManager)
            : base(aLogManager)
        {
            BpLogger.Info("Loading UWP Bluetooth Manager");

            _adapter = CrossBluetoothLE.Current.Adapter;
            if (_adapter == null)
            {
                BpLogger.Warn("No bluetooth adapter available for UWP Bluetooth Manager Connection");
                return;
            }
            _adapter.DeviceAdvertised += _adapter_DeviceAdvertised;

            BpLogger.Debug("UWP Manager found working Bluetooth LE Adapter");
        }
        public UWPBluetoothManager(IButtplugLogManager aLogManager)
            : base(aLogManager)
        {
            BpLogger.Info("Loading UWP Bluetooth Manager");
            _currentlyConnecting = new List <ulong>();

            // Introspect the ButtplugDevices namespace for all Factory classes, then create instances of all of them.
            _deviceFactories = new List <UWPBluetoothDeviceFactory>();
            BuiltinDevices.ForEach(aDeviceFactory =>
            {
                BpLogger.Debug($"Loading Bluetooth Device Factory: {aDeviceFactory.GetType().Name}");
                _deviceFactories.Add(new UWPBluetoothDeviceFactory(aLogManager, aDeviceFactory));
            });

            _bleWatcher = new BluetoothLEAdvertisementWatcher {
                ScanningMode = BluetoothLEScanningMode.Active
            };

            // We can't filter device advertisements because you can only add one LocalName filter at a time, meaning we
            // would have to set up multiple watchers for multiple devices. We'll handle our own filtering via the factory
            // classes whenever we receive a device.
            _bleWatcher.Received += OnAdvertisementReceived;
            _bleWatcher.Stopped  += OnWatcherStopped;
            var adapterTask = Task.Run(() => BluetoothAdapter.GetDefaultAsync().AsTask());

            adapterTask.Wait();
            var adapter = adapterTask.Result;

            if (adapter == null)
            {
                BpLogger.Warn("No bluetooth adapter available for UWP Bluetooth Manager Connection");
                return;
            }

            if (!adapter.IsLowEnergySupported)
            {
                BpLogger.Warn("Bluetooth adapter available but does not support Bluetooth Low Energy.");
                return;
            }

            BpLogger.Debug("UWP Manager found working Bluetooth LE Adapter");
        }
        public UWPBluetoothManager(IButtplugLogManager aLogManager)
            : base(aLogManager)
        {
            BpLogger.Info("Loading UWP Bluetooth Manager");

            // We can't filter device advertisements because you can only add one LocalName filter at
            // a time, meaning we would have to set up multiple watchers for multiple devices. We'll
            // handle our own filtering via the factory classes whenever we receive a device.
            _bleWatcher.Received += OnAdvertisementReceived;
            _bleWatcher.Stopped  += OnWatcherStopped;
            var adapterTask = Task.Run(() => BluetoothAdapter.GetDefaultAsync().AsTask());

            adapterTask.Wait();
            var adapter = adapterTask.Result;

            if (adapter == null)
            {
                BpLogger.Warn("No bluetooth adapter available for UWP Bluetooth Manager Connection");
                return;
            }

            if (!adapter.IsLowEnergySupported)
            {
                BpLogger.Warn("Bluetooth adapter available but does not support Bluetooth Low Energy.");
                return;
            }

            BpLogger.Debug("UWP Manager found working Bluetooth LE Adapter");

            // Only run radio information lookup if we're actually logging at the level it will show.
            if (aLogManager.MaxLevel >= ButtplugLogLevel.Debug)
            {
                // Do radio lookup in a background task, as the search query is very slow. TODO
                // Should probably try and cancel this if it's still running on object destruction,
                // but the Get() call is uninterruptable?
                _radioTask = Task.Run(() => LogBluetoothRadioInfo());
            }
        }
        public KiirooGen21Protocol([NotNull] IButtplugLogManager aLogManager,
                                   [NotNull] IButtplugDeviceImpl aInterface)
            : base(aLogManager,
                   "Kiiroo Unknown",
                   aInterface)
        {
            if (DevInfos.ContainsKey(aInterface.Name))
            {
                Name     = $"{DevInfos[aInterface.Name].Brand} {DevInfos[aInterface.Name].Name}";
                _devInfo = DevInfos[aInterface.Name];
            }
            else
            {
                BpLogger.Warn($"Cannot identify device {Name}, defaulting to Pearl2 settings.");
                _devInfo = DevInfos["Pearl2.1"];
            }

            AddMessageHandler <StopDeviceCmd>(HandleStopDeviceCmd);

            if (_devInfo.VibeCount > 0)
            {
                AddMessageHandler <VibrateCmd>(HandleVibrateCmd,
                                               new MessageAttributes {
                    FeatureCount = _devInfo.VibeCount
                });
                AddMessageHandler <SingleMotorVibrateCmd>(HandleSingleMotorVibrateCmd);
            }

            if (_devInfo.HasLinear)
            {
                AddMessageHandler <LinearCmd>(HandleLinearCmd,
                                              new MessageAttributes {
                    FeatureCount = 1
                });
                AddMessageHandler <FleshlightLaunchFW12Cmd>(HandleFleshlightLaunchFW12Cmd);
            }
        }
Esempio n. 13
0
        public override async Task <ButtplugMessage> Initialize()
        {
            BpLogger.Trace($"Initializing {Name}");

            // Subscribing to read updates
            await Interface.SubscribeToUpdates();

            Interface.BluetoothNotifyReceived += NotifyReceived;

            // Retreiving device type info for identification.
            var writeMsg = await Interface.WriteValue(ButtplugConsts.SystemMsgId, Encoding.ASCII.GetBytes($"DeviceType;"), true);

            if (writeMsg is Error)
            {
                BpLogger.Error($"Error requesting device info from Lovense {Name}");
                return(writeMsg);
            }

            var(msg, result) = await Interface.ReadValue(ButtplugConsts.SystemMsgId);

            string deviceInfoString = string.Empty;

            if (msg is Ok)
            {
                deviceInfoString = Encoding.ASCII.GetString(result);
            }
            else
            {
                // The device info notification isn't available immediately.
                // TODO Turn this into a task semaphore with cancellation/timeout, let system handle check timing.
                int timeout = 500;
                while (timeout > 0)
                {
                    if (_lastNotifyReceived != string.Empty)
                    {
                        deviceInfoString = _lastNotifyReceived;
                        break;
                    }

                    timeout -= 5;
                    await Task.Delay(5);
                }
            }

            if (deviceInfoString != string.Empty)
            {
                BpLogger.Debug($"Received device query return for {Interface.Name}");
                // Expected Format X:YY:ZZZZZZZZZZZZ X is device type leter YY is firmware version Z
                // is bluetooth address
                var deviceInfo = deviceInfoString.Split(':');

                // If we don't get back the amount of tokens we expect, identify as unknown, log, bail.
                if (deviceInfo.Length != 3 || deviceInfo[0].Length != 1)
                {
                    return(BpLogger.LogErrorMsg(ButtplugConsts.SystemMsgId, Error.ErrorClass.ERROR_DEVICE,
                                                $"Unknown Lovense DeviceType of {deviceInfoString} found. Please report to B******g Developers by filing an issue at https://github.com/metafetish/b******g/"));
                }

                var deviceTypeLetter = deviceInfo[0][0];
                if (deviceTypeLetter == 'C')
                {
                    deviceTypeLetter = 'A';
                }
                int.TryParse(deviceInfo[1], out var deviceVersion);
                BpLogger.Trace($"Lovense DeviceType Return: {deviceTypeLetter}");
                if (!Enum.IsDefined(typeof(LovenseDeviceType), (uint)deviceTypeLetter))
                {
                    // If we don't know what device this is, just assume it has a single vibrator,
                    // call it unknown, log something.
                    return(BpLogger.LogErrorMsg(ButtplugConsts.SystemMsgId, Error.ErrorClass.ERROR_DEVICE,
                                                $"Unknown Lovense Device of Type {deviceTypeLetter} found. Please report to B******g Developers by filing an issue at https://github.com/metafetish/b******g/"));
                }

                Name = $"Lovense {Enum.GetName(typeof(LovenseDeviceType), (uint)deviceTypeLetter)} v{deviceVersion}";

                _deviceType = (LovenseDeviceType)deviceTypeLetter;
            }
            else
            {
                // If we for some reason don't get a device info query back, use fallback method.
                //
                // TODO Remove this branch at some point? Not sure we'll need it now since device queries seem stable.
                BpLogger.Warn($"Error retreiving device info from Lovense {Name}, using fallback method");

                // Some of the older devices seem to have issues with info lookups? Not sure why, so
                // for now use fallback method.
                switch (Interface.Name.Substring(0, 6))
                {
                case "LVS-B0":
                    _deviceType = LovenseDeviceType.Max;
                    break;

                case "LVS-A0":
                case "LVS-C0":
                    _deviceType = LovenseDeviceType.Nora;
                    break;

                case "LVS-L0":
                    _deviceType = LovenseDeviceType.Ambi;
                    break;

                default:
                    _deviceType = LovenseDeviceType.Unknown;
                    break;
                }

                Name = $"Lovense {Enum.GetName(typeof(LovenseDeviceType), (uint)_deviceType)} v{Interface.Name.Substring(Interface.Name.Length - 2)}";
            }

            if (_deviceType == LovenseDeviceType.Unknown)
            {
                BpLogger.Error("Lovense device type unknown, treating as single vibrator device. Please contact developers for more info.");
            }

            switch (_deviceType)
            {
            case LovenseDeviceType.Edge:

                // Edge has 2 vibrators
                _vibratorCount++;
                MsgFuncs.Remove(typeof(VibrateCmd));
                MsgFuncs.Add(typeof(VibrateCmd), new ButtplugDeviceWrapper(HandleVibrateCmd, new MessageAttributes()
                {
                    FeatureCount = _vibratorCount
                }));
                break;

            case LovenseDeviceType.Nora:

                // Nora has a rotator
                MsgFuncs.Add(typeof(RotateCmd), new ButtplugDeviceWrapper(HandleRotateCmd, new MessageAttributes()
                {
                    FeatureCount = 1
                }));
                break;
            }

            return(new Ok(ButtplugConsts.SystemMsgId));
        }
        private async void OnAdvertisementReceived(BluetoothLEAdvertisementWatcher aObj,
                                                   BluetoothLEAdvertisementReceivedEventArgs aEvent)
        {
            if (aEvent?.Advertisement == null)
            {
                BpLogger.Debug("Null BLE advertisement recieved: skipping");
                return;
            }

            var advertName  = aEvent.Advertisement.LocalName ?? string.Empty;
            var advertGUIDs = new List <Guid>();

            advertGUIDs.AddRange(aEvent.Advertisement.ServiceUuids ?? new Guid[] { });
            var btAddr = aEvent.BluetoothAddress;

            // BpLogger.Trace($"Got BLE Advertisement for device: {aEvent.Advertisement.LocalName} / {aEvent.BluetoothAddress}");
            if (_currentlyConnecting.Contains(btAddr))
            {
                // BpLogger.Trace($"Ignoring advertisement for already connecting device: {aEvent.Advertisement.LocalName} / {aEvent.BluetoothAddress}");
                return;
            }

            BpLogger.Trace("BLE device found: " + advertName);
            var factories = from x in _deviceFactories
                            where x.MayBeDevice(advertName, advertGUIDs)
                            select x;

            // We should always have either 0 or 1 factories.
            var buttplugBluetoothDeviceFactories = factories as UWPBluetoothDeviceFactory[] ?? factories.ToArray();

            if (buttplugBluetoothDeviceFactories.Length != 1)
            {
                if (buttplugBluetoothDeviceFactories.Any())
                {
                    BpLogger.Warn($"Found multiple BLE factories for {advertName} {btAddr}:");
                    buttplugBluetoothDeviceFactories.ToList().ForEach(x => BpLogger.Warn(x.GetType().Name));
                }
                else
                {
                    // BpLogger.Trace("No BLE factories found for device.");
                }

                return;
            }

            _currentlyConnecting.Add(btAddr);
            var factory = buttplugBluetoothDeviceFactories.First();

            BpLogger.Debug($"Found BLE factory: {factory.GetType().Name}");

            // If we actually have a factory for this device, go ahead and create the device
            var fromBluetoothAddressAsync = BluetoothLEDevice.FromBluetoothAddressAsync(btAddr);

            if (fromBluetoothAddressAsync != null)
            {
                var dev = await fromBluetoothAddressAsync;

                // If a device is turned on after scanning has started, windows seems to lose the
                // device handle the first couple of times it tries to deal with the advertisement.
                // Just log the error and hope it reconnects on a later retry.
                try
                {
                    var d = await factory.CreateDeviceAsync(dev);

                    InvokeDeviceAdded(new DeviceAddedEventArgs(d));
                }
                catch (Exception ex)
                {
                    BpLogger.Error(
                        $"Cannot connect to device {advertName} {btAddr}: {ex.Message}");
                    _currentlyConnecting.Remove(btAddr);
                    return;
                }
            }

            _currentlyConnecting.Remove(btAddr);
        }