Skip to content

Commit

Permalink
fix: Fix issues with noble and Lovense devices.
Browse files Browse the repository at this point in the history
Fixes issues with:

- noble using short UUIDs with no way to reconfigure
- older Lovense devices never sending correct updates to noble on
  DeviceType query (fixes #177)
- Newer Lovense devices having some sort of value wedged in the read
  characteristic on startup (fixes #155)

Also adds more debug logging to noble because apparently we're gonna
need it.
  • Loading branch information
qdot committed Feb 23, 2019
1 parent f8a6546 commit b28b888
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 27 deletions.
Expand Up @@ -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);
Expand Down Expand Up @@ -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 = () => {
Expand All @@ -111,23 +131,26 @@ export class ButtplugNodeBluetoothLEDevice extends ButtplugDeviceImpl {

public ReadValueInternal = async (aOptions: ButtplugDeviceReadOptions): Promise<Buffer> => {
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<void> => {
public SubscribeToUpdatesInternal = async (aOptions: ButtplugDeviceReadOptions): Promise<void> => {
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();
}

Expand Down
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
}
}
26 changes: 15 additions & 11 deletions packages/buttplug/src/devices/protocols/Lovense.ts
Expand Up @@ -43,7 +43,7 @@ export class Lovense extends ButtplugDeviceProtocol {
public Initialize = async (): Promise<void> => {
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();
Expand All @@ -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";
Expand All @@ -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
Expand Down

0 comments on commit b28b888

Please sign in to comment.