diff --git a/packages/buttplug-node-bluetoothle-manager/src/ButtplugNodeBluetoothLEDevice.ts b/packages/buttplug-node-bluetoothle-manager/src/ButtplugNodeBluetoothLEDevice.ts index 066577f5..9d1e4a5e 100644 --- a/packages/buttplug-node-bluetoothle-manager/src/ButtplugNodeBluetoothLEDevice.ts +++ b/packages/buttplug-node-bluetoothle-manager/src/ButtplugNodeBluetoothLEDevice.ts @@ -48,12 +48,19 @@ export class ButtplugNodeBluetoothLEDevice extends ButtplugDeviceImpl { // is horrible. let nobleServices = Array.from(this._deviceInfo.Services.keys()).map((x) => this.RegularToNobleUuid(x)); - // TODO Figure out service uuid shortening rules because one god damn line - // later everything continues to be fucking horrible and this will miss - // services starting with 0000 because it assumes 16-bit shortened form. + // Caution: We have to do weird service uuid shortening rules because one + // god damn line later everything continues to be fucking horrible and this + // will miss services starting with 0000 because it assumes 16-bit shortened + // form if we don't shorten them ourselves. This happens back in + // RegularToNobleUuid also. let services = await discoverServicesAsync(nobleServices); + if (services.length === 0) { + throw new ButtplugDeviceException(`Cannot find any valid services on device ${this._device.advertisement.localName}`); + } + for (const service of services) { + this._logger.Debug(`Found service ${service.uuid} for device ${this._device.advertisement.localName}`); const discoverCharsAsync: (x: string[]) => noble.Characteristic[] = util.promisify(service.discoverCharacteristics.bind(service)); const serviceUuid = this.NobleToRegularUuid(service.uuid); @@ -87,13 +94,26 @@ export class ButtplugNodeBluetoothLEDevice extends ButtplugDeviceImpl { } private RegularToNobleUuid(aRegularUuid: string): string { + // Noble autoshortens default IDs. This is fucking horrible and I'm not sure + // how to turn it off. If we see something start with 4 0's, assume we'll + // have to shorten. + // + // Find a new fucking maintainer already, Sandeep. Many of us have + // offered. I know you're out there. You're updating your twitter. + if (aRegularUuid.startsWith("0000")) { + return aRegularUuid.substr(4, 4); + } return aRegularUuid.replace(/-/g, ""); } - private NobleToRegularUuid(aRegularUuid: string): string { + private NobleToRegularUuid(aNobleUuid: string): string { + // And, once again, shortened IDs we have to convert by hand. God damnit. + if (aNobleUuid.length === 4) { + return `0000${aNobleUuid}-0000-1000-8000-00805f9b34fb`; + } // I can't believe I'm bringing in a whole UUID library for this but such is // life in node. - return uuidParse.unparse(Buffer.from(aRegularUuid, 'hex')); + return uuidParse.unparse(Buffer.from(aNobleUuid, 'hex')); } public OnDisconnect = () => { @@ -111,23 +131,26 @@ export class ButtplugNodeBluetoothLEDevice extends ButtplugDeviceImpl { public ReadValueInternal = async (aOptions: ButtplugDeviceReadOptions): Promise => { if (!this._characteristics.has(aOptions.Endpoint)) { - throw new ButtplugDeviceException(`Device ${this._device.advertisement.localName} has not endpoint named ${aOptions.Endpoint}`); + throw new ButtplugDeviceException(`Device ${this._device.advertisement.localName} has no endpoint named ${aOptions.Endpoint}`); } const chr = this._characteristics.get(aOptions.Endpoint)!; return await util.promisify(chr.read.bind(chr))(); } - public SubscribeToUpdatesInternal = (aOptions: ButtplugDeviceReadOptions): Promise => { + public SubscribeToUpdatesInternal = async (aOptions: ButtplugDeviceReadOptions): Promise => { + this._logger.Debug(`Subscripting to updates on noble device ${this._device.advertisement.localName}`); if (!this._characteristics.has(aOptions.Endpoint)) { - throw new ButtplugDeviceException(`Device ${this._device.advertisement.localName} has not endpoint named ${aOptions.Endpoint}`); + throw new ButtplugDeviceException(`Device ${this._device.advertisement.localName} has no endpoint named ${aOptions.Endpoint}`); } - console.log("Subscribing!"); const chr = this._characteristics.get(aOptions.Endpoint)!; + if (chr.properties.find((x) => x === "notify" || x === "indicate") === undefined) { + throw new ButtplugDeviceException(`Device ${this._device.advertisement.localName} endpoint ${aOptions.Endpoint} does not have notify or indicate properties.`); + } this._notificationHandlers.set(aOptions.Endpoint, (aIsNotification: boolean) => { this.CharacteristicValueChanged(aOptions.Endpoint, aIsNotification); }); - chr.subscribe(); - chr.on("notify", this._notificationHandlers.get(aOptions.Endpoint)!); + await util.promisify(chr.subscribe.bind(chr))(); + chr.on("data", this._notificationHandlers.get(aOptions.Endpoint)!); return Promise.resolve(); } diff --git a/packages/buttplug-node-bluetoothle-manager/src/ButtplugNodeBluetoothLEDeviceManager.ts b/packages/buttplug-node-bluetoothle-manager/src/ButtplugNodeBluetoothLEDeviceManager.ts index 738a0951..011f0c1f 100644 --- a/packages/buttplug-node-bluetoothle-manager/src/ButtplugNodeBluetoothLEDeviceManager.ts +++ b/packages/buttplug-node-bluetoothle-manager/src/ButtplugNodeBluetoothLEDeviceManager.ts @@ -6,7 +6,7 @@ * @copyright Copyright (c) Nonpolynomial Labs LLC. All rights reserved. */ import * as noble from "noble-mac"; -import { IDeviceSubtypeManager, ButtplugLogger, DeviceConfigurationManager, BluetoothLEProtocolConfiguration, ButtplugDevice } from "buttplug"; +import { IDeviceSubtypeManager, ButtplugLogger, DeviceConfigurationManager, BluetoothLEProtocolConfiguration, ButtplugDevice, ButtplugException, ButtplugDeviceException } from "buttplug"; import { EventEmitter } from "events"; import { ButtplugNodeBluetoothLEDevice } from "./ButtplugNodeBluetoothLEDevice"; @@ -74,14 +74,59 @@ export class ButtplugNodeBluetoothLEDeviceManager extends EventEmitter implement if (foundConfig === undefined) { return; } + this.logger.Debug(`Found configuration for device ${device.advertisement.localName}`); const [config, protocolType] = foundConfig; const bpDevImpl = new ButtplugNodeBluetoothLEDevice(config as BluetoothLEProtocolConfiguration, device); - await bpDevImpl.Connect(); + this.logger.Debug(`Connecting to noble device ${device.advertisement.localName}`); + try { + await bpDevImpl.Connect(); + } catch (e) { + let errStr: string; + switch (e) { + case ButtplugDeviceException: { + errStr = e.errorMessage; + break; + } + case Error: { + errStr = e.message; + break; + } + default: { + errStr = e.toString(); + break; + } + } + this.logger.Info(`Error while connecting to ${device.advertisement.localName}: ${errStr}`); + // We can't rethrow here, as this method is only called from an event + // handler, so just return; + return; + } const bpProtocol = new protocolType(bpDevImpl); const bpDevice = new ButtplugDevice(bpProtocol, bpDevImpl); - console.log("initializing"); - await bpDevice.Initialize(); - console.log("initialize"); + this.logger.Debug(`Initializing noble device ${device.advertisement.localName}`); + try { + await bpDevice.Initialize(); + } catch (e) { + let errStr: string; + switch (e) { + case ButtplugDeviceException: { + errStr = e.errorMessage; + break; + } + case Error: { + errStr = e.message; + break; + } + default: { + errStr = e.toString(); + break; + } + } + this.logger.Info(`Error while initializing ${device.advertisement.localName}: ${errStr}`); + // We can't rethrow here, as this method is only called from an event + // handler, so just return; + return; + } this.emit("deviceadded", bpDevice); } } diff --git a/packages/buttplug/src/devices/protocols/Lovense.ts b/packages/buttplug/src/devices/protocols/Lovense.ts index d2c5bfc1..16545c97 100644 --- a/packages/buttplug/src/devices/protocols/Lovense.ts +++ b/packages/buttplug/src/devices/protocols/Lovense.ts @@ -43,7 +43,7 @@ export class Lovense extends ButtplugDeviceProtocol { public Initialize = async (): Promise => { this._device.addListener("updateReceived", this.OnValueChanged); await this._device.SubscribeToUpdates(); - // TODO This is a bogus read that is required for noble on linux to dump + // xxx This is a bogus read that is required for noble on linux to dump // some weird characteristic value we get back on first notify. This doesn't // seem to happen in WebBluetooth. await this._device.ReadString(); @@ -57,14 +57,9 @@ export class Lovense extends ButtplugDeviceProtocol { } private ParseDeviceType(aDeviceType: string) { - // Typescript gets angry if we try to destructure this into consts/lets - // differently or all lets (since deviceVersion never changes and - // deviceAddress isn't used), and I don't wanna deal with assigning to const - // then let, so this works well enough. - let deviceLetter; - let deviceVersion; - let deviceAddress; - [deviceLetter, deviceVersion, deviceAddress] = aDeviceType.split(":"); + // This will return 3 values, but the last one (device address) we don't + // really care about. + let [deviceLetter, deviceVersion] = aDeviceType.split(":"); if (!Lovense._deviceNames.hasOwnProperty(deviceLetter)) { deviceLetter = "0"; @@ -90,11 +85,20 @@ export class Lovense extends ButtplugDeviceProtocol { // If we haven't initialized yet, consider this to be the first read, for the device info. if (this._initResolve !== undefined) { let identStr = aValue.toString('utf-8'); + // For some reason, our usual tricks with subscribe/notify don't work with + // noble, meaning older devices like the Nora and Max won't ever send a + // valid ident string. In this case, we just kludge an ident based on the + // device name and hope it works. Any device named + // LVS-[devicename][firmwarename] shouldn't hit this block, this really + // only applies to the oldest firmware. + if (identStr.length === 0 || identStr.indexOf(":") === -1) { + this._logger.Debug(`Lovense Device ${this._device.Name} got invalid initialization return "${identStr}, falling back to fake init`); + identStr=`${this._device.Name.substr(4, 1)}:00:000000000000`; + } this._logger.Debug(`Lovense Device ${this._device.Name} got initialization return ${identStr}`); this.ParseDeviceType(identStr); - const res = this._initResolve; + this._initResolve(); this._initResolve = undefined; - res(); return; } // TODO Fill in battery/accelerometer/etc reads