From 7944094adc027424cc6088a3b9d4ca64565eceea Mon Sep 17 00:00:00 2001 From: HRS Date: Thu, 4 Jun 2026 21:16:08 +0200 Subject: [PATCH 1/9] Upgrade buttplug from v3 to v4 - Replace ActuatorType/SensorType enums with OutputType/InputType - Replace messageAttributes API with new features Map API - Use DeviceOutput.X.percent() for sending actuator commands - Delete SlvCtrlPlusButtplugWebsocketClientConnector workaround (fixed upstream in v4) Closes #69 Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 20 +++---- package.json | 2 +- .../protocol/buttplugIo/buttplugIoDevice.ts | 56 ++++++++++++++----- .../buttplugIo/buttplugIoDeviceFactory.ts | 48 ++++++---------- .../buttplugIoWebsocketDeviceProvider.ts | 3 +- ...trlPlusButtplugWebsocketClientConnector.ts | 41 -------------- .../buttplugIo/buttplugIoDevice.spec.ts | 52 +++++++++-------- 7 files changed, 97 insertions(+), 125 deletions(-) delete mode 100644 src/device/protocol/buttplugIo/slvCtrlPlusButtplugWebsocketClientConnector.ts diff --git a/package-lock.json b/package-lock.json index 5aceaf1..4733233 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "@timesplinter/pimple": "^2.0.0", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", - "buttplug": "^3.2.1", + "buttplug": "^4.0.2", "class-transformer": "^0.5.1", "cors": "^2.8.5", "dotenv": "^17.2.0", @@ -3360,19 +3360,15 @@ "license": "MIT" }, "node_modules/buttplug": { - "version": "3.2.2", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/buttplug/-/buttplug-4.0.2.tgz", + "integrity": "sha512-PciEJEoBkHjeA0UFfdymr5+jHXukzF0T8Pg7TaAxxWFb/0Ynfy1dWaGt1c7O5E1sfJSVyWxrvBQQCSeNWzprbg==", "license": "BSD-3-Clause", "dependencies": { - "class-transformer": "^0.5.1", - "eventemitter3": "^5.0.1", - "reflect-metadata": "^0.2.1", - "ws": "^8.16.0" + "eventemitter3": "^5.0.4", + "ws": "^8.20.1" } }, - "node_modules/buttplug/node_modules/reflect-metadata": { - "version": "0.2.2", - "license": "Apache-2.0" - }, "node_modules/bytes": { "version": "3.1.2", "license": "MIT", @@ -4363,7 +4359,9 @@ } }, "node_modules/eventemitter3": { - "version": "5.0.1", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, "node_modules/events": { diff --git a/package.json b/package.json index e912776..c7b6583 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@timesplinter/pimple": "^2.0.0", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", - "buttplug": "^3.2.1", + "buttplug": "^4.0.2", "class-transformer": "^0.5.1", "cors": "^2.8.5", "dotenv": "^17.2.0", diff --git a/src/device/protocol/buttplugIo/buttplugIoDevice.ts b/src/device/protocol/buttplugIo/buttplugIoDevice.ts index 55e484e..e8f65e8 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDevice.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDevice.ts @@ -1,5 +1,5 @@ import { Exclude, Expose } from 'class-transformer'; -import { ActuatorType, ButtplugClientDevice, SensorType } from 'buttplug'; +import { ButtplugClientDevice, DeviceOutput, DeviceOutputCommand, InputCommandType, InputType, OutputType } from 'buttplug'; import Device, { ExtractAttributeValue } from '../../device.js'; import IntRangeDeviceAttribute from '../../attribute/intRangeDeviceAttribute.js'; import BoolDeviceAttribute from '../../attribute/boolDeviceAttribute.js'; @@ -8,8 +8,8 @@ import IntDeviceAttribute from '../../attribute/intDeviceAttribute.js'; import { DeviceAttributeModifier } from '../../attribute/deviceAttribute.js'; import EventEmitter from 'events'; -type ButtplugActuatorTypeKey = `${ActuatorType}-${number}`; -type ButtplugSensorTypeKey = `${SensorType}-${number}`; +type ButtplugActuatorTypeKey = `${OutputType}-${number}`; +type ButtplugSensorTypeKey = `${InputType}-${number}`; export type ButtplugIoDeviceAttributeKey = ButtplugActuatorTypeKey | ButtplugSensorTypeKey; export type ButtplugIoDeviceAttributes = Record< @@ -41,9 +41,20 @@ export default class ButtplugIoDevice extends Device } protected override async doRefresh(): Promise { - for (const sensor of this.buttplugClientDevice.messageAttributes.SensorReadCmd ?? []) { - const value = await this.buttplugClientDevice.sensorRead(sensor.Index, sensor.SensorType); - this.attributes[`${sensor.SensorType}-${sensor.Index}`].value = Int.from(value[0]); + for (const [featureIndex, feature] of this.buttplugClientDevice.features) { + for (const inputType of Object.values(InputType)) { + if (inputType === InputType.Unknown || !feature.hasInput(inputType)) { + continue; + } + const attrKey = `${inputType}-${featureIndex}` as ButtplugIoDeviceAttributeKey; + if (this.attributes[attrKey] === undefined) { + continue; + } + const response = await feature.runInput(inputType, InputCommandType.Read); + if (response?.Reading[inputType] !== undefined) { + this.attributes[attrKey].value = Int.from(response.Reading[inputType].Value); + } + } } } @@ -77,21 +88,38 @@ export default class ButtplugIoDevice extends Device throw new Error(`Unsupported attribute type '${attribute.constructor.name}' for buttplug.io`); } - const [actuatorType, index] = attributeName.split('-'); + const dashIndex = attributeName.lastIndexOf('-'); + const outputType = attributeName.slice(0, dashIndex) as OutputType; + const index = parseInt(attributeName.slice(dashIndex + 1), 10); - await this.send(actuatorType as ActuatorType, parseInt(index, 10), valueToSend); + await this.send(outputType, index, valueToSend); this.attributes[`${attributeName}`].value = value; return value; } - protected async send(command: ActuatorType, index: number, value: number): Promise { - return await this.buttplugClientDevice.scalar({ - 'ActuatorType': command, - 'Scalar': value, - 'Index': index - }); + protected async send(command: OutputType, index: number, value: number): Promise { + const feature = this.buttplugClientDevice.features.get(index); + if (feature === undefined) { + throw new Error(`Feature index ${index} not found`); + } + await feature.runOutput(ButtplugIoDevice.createOutputCommand(command, value)); + } + + private static createOutputCommand(type: OutputType, value: number): DeviceOutputCommand { + switch (type) { + case OutputType.Vibrate: return DeviceOutput.Vibrate.percent(value); + case OutputType.Rotate: return DeviceOutput.Rotate.percent(value); + case OutputType.Oscillate: return DeviceOutput.Oscillate.percent(value); + case OutputType.Constrict: return DeviceOutput.Constrict.percent(value); + case OutputType.Inflate: return DeviceOutput.Inflate.percent(value); + case OutputType.Temperature: return DeviceOutput.Temperature.percent(value); + case OutputType.Led: return DeviceOutput.Led.percent(value); + case OutputType.Spray: return DeviceOutput.Spray.percent(value); + case OutputType.Position: return DeviceOutput.Position.percent(value); + default: throw new Error(`Unsupported output type: ${type}`); + } } public get getButtplugClientDevice(): ButtplugClientDevice diff --git a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts index c45eacb..d3f82c2 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts @@ -1,12 +1,11 @@ import UuidFactory from '../../../factory/uuidFactory.js'; import Settings from '../../../settings/settings.js'; -import { ButtplugClientDevice } from 'buttplug'; +import { ButtplugClientDevice, InputType, OutputType } from 'buttplug'; import ButtplugIoDevice, { ButtplugIoDeviceAttributeKey, ButtplugIoDeviceAttributes } from './buttplugIoDevice.js'; import KnownDevice from '../../../settings/knownDevice.js'; import Logger from '../../../logging/Logger.js'; import { DeviceAttributeModifier } from '../../attribute/deviceAttribute.js'; import IntRangeDeviceAttribute from '../../attribute/intRangeDeviceAttribute.js'; -import BoolDeviceAttribute from '../../attribute/boolDeviceAttribute.js'; import DateFactory from '../../../factory/dateFactory.js'; import { Int } from '../../../util/numbers.js'; import IntDeviceAttribute from '../../attribute/intDeviceAttribute.js'; @@ -62,47 +61,32 @@ export default class ButtplugIoDeviceFactory private static parseDeviceAttributes(buttplugDevice: ButtplugClientDevice): ButtplugIoDeviceAttributes { const attributes = {} as ButtplugIoDeviceAttributes; - for (const item of buttplugDevice.messageAttributes.ScalarCmd ?? []) { - const attrName = `${item.ActuatorType}-${item.Index}` as ButtplugIoDeviceAttributeKey; - - if (item.StepCount > 2) { + for (const [featureIndex, feature] of buttplugDevice.features) { + for (const outputType of Object.values(OutputType)) { + if (outputType === OutputType.Unknown || !feature.hasOutput(outputType)) { + continue; + } + const attrName = `${outputType}-${featureIndex}` as ButtplugIoDeviceAttributeKey; attributes[attrName] = IntRangeDeviceAttribute.createInitialized( attrName, - item.FeatureDescriptor, + undefined, DeviceAttributeModifier.writeOnly, undefined, Int.ZERO, - Int.from(item.StepCount), + Int.from(100), Int.from(1), Int.ZERO ); - } else { - attributes[attrName] = BoolDeviceAttribute.createInitialized( - attrName, item.FeatureDescriptor, DeviceAttributeModifier.writeOnly, false - ); } - } - for (const item of buttplugDevice.messageAttributes.SensorReadCmd ?? []) { - const attrName = `${item.SensorType}-${item.Index}` as ButtplugIoDeviceAttributeKey; - - // A range is defined by two numbers, if there are more or less, let's fallback - // to a normal integer attribute. Not that dramatic for a sensor after all. - if (item.StepRange.length === 2) { - attributes[attrName] = IntRangeDeviceAttribute.createInitialized( - `${item.SensorType}-${item.Index}`, - item.FeatureDescriptor, - DeviceAttributeModifier.readOnly, - undefined, - Int.from(item.StepRange[0]), - Int.from(item.StepRange[1]), - Int.from(1), - Int.ZERO - ); - } else { + for (const inputType of Object.values(InputType)) { + if (inputType === InputType.Unknown || !feature.hasInput(inputType)) { + continue; + } + const attrName = `${inputType}-${featureIndex}` as ButtplugIoDeviceAttributeKey; attributes[attrName] = IntDeviceAttribute.createInitialized( - `${item.SensorType}-${item.Index}`, - item.FeatureDescriptor, + attrName, + undefined, DeviceAttributeModifier.readOnly, undefined, Int.ZERO diff --git a/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts b/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts index ac92563..a84f92f 100644 --- a/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts +++ b/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts @@ -5,7 +5,6 @@ import DeviceProvider from '../../provider/deviceProvider.js'; import ButtplugIoDeviceFactory from './buttplugIoDeviceFactory.js'; import Logger from '../../../logging/Logger.js'; import { asyncHandler, setImmediateInterval } from '../../../util/async.js'; -import SlvCtrlPlusButtplugWebsocketClientConnector from './slvCtrlPlusButtplugWebsocketClientConnector.js'; import DeviceManager from '../../deviceManager.js'; import { logError } from '../../../util/error.js'; @@ -43,7 +42,7 @@ export default class ButtplugIoWebsocketDeviceProvider extends DeviceProvider => { - return new Promise((resolve, reject) => { - const ws = new (this._websocketConstructor ?? WebSocket)(this._url2); - const onErrorCallback = (event: Event): void => reject(event); - const onCloseCallback = (event: CloseEvent): void => reject(event.reason); - ws.addEventListener('open', async (): Promise => { - this._ws = ws; - try { - await this.initialize(); - this._ws.addEventListener('message', (msg) => { - this.parseIncomingMessage(msg); - }); - this._ws.removeEventListener('close', onCloseCallback); - this._ws.removeEventListener('error', onErrorCallback); - this._ws.addEventListener('close', this.disconnect); - resolve(); - } catch (e) { - reject(e); - } - }); - // In websockets, our error rarely tells us much, as for security reasons - // browsers usually only throw Error Code 1006. It's up to those using this - // library to state what the problem might be. - - ws.addEventListener('error', onErrorCallback) - ws.addEventListener('close', onCloseCallback); - }); - } -} diff --git a/tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts b/tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts index 5ab9acd..c9c7b81 100644 --- a/tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts +++ b/tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts @@ -1,4 +1,5 @@ -import {ButtplugClientDevice} from "buttplug"; +import {ButtplugClientDevice, DeviceOutputCommand, OutputType} from "buttplug"; +import {Mock} from "@vitest/spy"; import BoolDeviceAttribute from "../../../../../src/device/attribute/boolDeviceAttribute.js"; import IntRangeDeviceAttribute from "../../../../../src/device/attribute/intRangeDeviceAttribute.js"; import ButtplugIoDevice, { @@ -7,13 +8,18 @@ import ButtplugIoDevice, { } from "../../../../../src/device/protocol/buttplugIo/buttplugIoDevice.js"; import {DeviceAttributeModifier} from "../../../../../src/device/attribute/deviceAttribute.js"; import {Int} from "../../../../../src/util/numbers.js"; -import {describe, it, expect} from "vitest"; -import {mock} from "vitest-mock-extended"; +import {describe, it, expect, vi} from "vitest"; import {EventEmitter} from "events"; +type RunOutputFn = (cmd: DeviceOutputCommand) => Promise; + +function makeDeviceStub(entries: Array<[number, { runOutput: Mock }]>): ButtplugClientDevice { + return { features: new Map(entries) } as unknown as ButtplugClientDevice; +} + describe('ButtplugIoDevice', () => { - function createDevice(buttplugDeviceMock: ButtplugClientDevice, attrs: ButtplugIoDeviceAttributes): ButtplugIoDevice + function createDevice(buttplugDevice: ButtplugClientDevice, attrs: ButtplugIoDeviceAttributes): ButtplugIoDevice { return new ButtplugIoDevice( 'device-id', @@ -21,7 +27,7 @@ describe('ButtplugIoDevice', () => { 'device model', 'buttplugIo', new Date(), - buttplugDeviceMock, + buttplugDevice, attrs, new EventEmitter(), ); @@ -30,9 +36,7 @@ describe('ButtplugIoDevice', () => { it('it throws an error if non-existing attribute is set', async () => { // Arrange - const buttplugDeviceMock = mock(); - const device = createDevice(buttplugDeviceMock, {}); - + const device = createDevice(makeDeviceStub([]), {}); const attrName = 'bool' as ButtplugIoDeviceAttributeKey; // Act @@ -45,32 +49,34 @@ describe('ButtplugIoDevice', () => { it('it updates device data and calls buttplugClientDevice on setting boolean attribute', async () => { // Arrange - const buttplugDeviceMock = mock(); - const boolAttrKey = 'bool-1' as ButtplugIoDeviceAttributeKey; + const runOutput = vi.fn().mockResolvedValue(undefined); + const boolAttrKey = `${OutputType.Vibrate}-1` as ButtplugIoDeviceAttributeKey; const boolAttr = BoolDeviceAttribute.create(boolAttrKey, undefined, DeviceAttributeModifier.readWrite); const device = createDevice( - buttplugDeviceMock, + makeDeviceStub([[1, {runOutput}]]), {[boolAttrKey]: boolAttr}, ); // Act await device.setAttribute(boolAttrKey, false); + // Assert expect((await device.getAttribute(boolAttrKey))?.value).toStrictEqual(false); - // Assert - expect(buttplugDeviceMock.scalar).toHaveBeenCalledTimes(1); - expect(buttplugDeviceMock.scalar).toHaveBeenCalledWith({ActuatorType: 'bool', Index: 1, Scalar: 0}); + expect(runOutput).toHaveBeenCalledTimes(1); + const cmd = runOutput.mock.calls[0][0]; + expect(cmd.outputType).toBe(OutputType.Vibrate); + expect(cmd.value.percent).toBe(0); }); it('it updates device data and calls buttplugClientDevice on setting range attribute', async () => { // Arrange - const buttplugDeviceMock = mock(); + const runOutput = vi.fn().mockResolvedValue(undefined); - const rangeAttrName = 'range-2' as ButtplugIoDeviceAttributeKey; + const rangeAttrName = `${OutputType.Vibrate}-2` as ButtplugIoDeviceAttributeKey; const rangeAttr = IntRangeDeviceAttribute.create( rangeAttrName, undefined, @@ -82,7 +88,7 @@ describe('ButtplugIoDevice', () => { ); const device = createDevice( - buttplugDeviceMock, + makeDeviceStub([[2, {runOutput}]]), {[rangeAttrName]: rangeAttr}, ); @@ -91,14 +97,12 @@ describe('ButtplugIoDevice', () => { // Act await device.setAttribute(rangeAttrName, Int.from(newValue)); + // Assert expect((await device.getAttribute(rangeAttrName))?.value).toStrictEqual(newValue); - // Assert - expect(buttplugDeviceMock.scalar).toHaveBeenCalledTimes(1); - expect(buttplugDeviceMock.scalar).toHaveBeenCalledWith({ - ActuatorType: 'range', - Index: 2, - Scalar: newValue/rangeAttr.max - }); + expect(runOutput).toHaveBeenCalledTimes(1); + const cmd = runOutput.mock.calls[0][0]; + expect(cmd.outputType).toBe(OutputType.Vibrate); + expect(cmd.value.percent).toBeCloseTo(newValue / 20); }); }); From 432930484392af10c6b7a4df3fc11e7903ee8516 Mon Sep 17 00:00:00 2001 From: HRS Date: Thu, 4 Jun 2026 22:18:29 +0200 Subject: [PATCH 2/9] Improve typing --- src/device/protocol/buttplugIo/buttplugIoDevice.ts | 2 +- .../protocol/buttplugIo/buttplugIoDeviceFactory.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/device/protocol/buttplugIo/buttplugIoDevice.ts b/src/device/protocol/buttplugIo/buttplugIoDevice.ts index e8f65e8..b843b09 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDevice.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDevice.ts @@ -43,7 +43,7 @@ export default class ButtplugIoDevice extends Device protected override async doRefresh(): Promise { for (const [featureIndex, feature] of this.buttplugClientDevice.features) { for (const inputType of Object.values(InputType)) { - if (inputType === InputType.Unknown || !feature.hasInput(inputType)) { + if (inputType === InputType.Unknown || false === feature.hasInput(inputType)) { continue; } const attrKey = `${inputType}-${featureIndex}` as ButtplugIoDeviceAttributeKey; diff --git a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts index d3f82c2..76eb060 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts @@ -59,14 +59,14 @@ export default class ButtplugIoDeviceFactory } private static parseDeviceAttributes(buttplugDevice: ButtplugClientDevice): ButtplugIoDeviceAttributes { - const attributes = {} as ButtplugIoDeviceAttributes; + const attributes: ButtplugIoDeviceAttributes = {}; for (const [featureIndex, feature] of buttplugDevice.features) { for (const outputType of Object.values(OutputType)) { - if (outputType === OutputType.Unknown || !feature.hasOutput(outputType)) { + if (outputType === OutputType.Unknown || false === feature.hasOutput(outputType)) { continue; } - const attrName = `${outputType}-${featureIndex}` as ButtplugIoDeviceAttributeKey; + const attrName: ButtplugIoDeviceAttributeKey = `${outputType}-${featureIndex}`; attributes[attrName] = IntRangeDeviceAttribute.createInitialized( attrName, undefined, @@ -80,10 +80,10 @@ export default class ButtplugIoDeviceFactory } for (const inputType of Object.values(InputType)) { - if (inputType === InputType.Unknown || !feature.hasInput(inputType)) { + if (inputType === InputType.Unknown || false === feature.hasInput(inputType)) { continue; } - const attrName = `${inputType}-${featureIndex}` as ButtplugIoDeviceAttributeKey; + const attrName: ButtplugIoDeviceAttributeKey = `${inputType}-${featureIndex}`; attributes[attrName] = IntDeviceAttribute.createInitialized( attrName, undefined, From cc4bf7af3c7352e7e0c3c9f41e22b1517d723788 Mon Sep 17 00:00:00 2001 From: HRS Date: Sun, 7 Jun 2026 14:21:06 +0200 Subject: [PATCH 3/9] Use buttplugio-js@v5 --- package-lock.json | 8 +++--- package.json | 2 +- .../buttplugIo/buttplugIoDeviceFactory.ts | 27 +++++++++++-------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4733233..f7adad9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "@timesplinter/pimple": "^2.0.0", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", - "buttplug": "^4.0.2", + "buttplug": "^5.0.1", "class-transformer": "^0.5.1", "cors": "^2.8.5", "dotenv": "^17.2.0", @@ -3360,9 +3360,9 @@ "license": "MIT" }, "node_modules/buttplug": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/buttplug/-/buttplug-4.0.2.tgz", - "integrity": "sha512-PciEJEoBkHjeA0UFfdymr5+jHXukzF0T8Pg7TaAxxWFb/0Ynfy1dWaGt1c7O5E1sfJSVyWxrvBQQCSeNWzprbg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/buttplug/-/buttplug-5.0.1.tgz", + "integrity": "sha512-m7Qzoi6TEsr0DkqqXc3Gun/wikWiHziQeXu73Oxzd1ETqj2y1P0VdJ9QxAT811bLcTIYvyf0VkuHlXbEGcEXCw==", "license": "BSD-3-Clause", "dependencies": { "eventemitter3": "^5.0.4", diff --git a/package.json b/package.json index c7b6583..2fe569a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@timesplinter/pimple": "^2.0.0", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", - "buttplug": "^4.0.2", + "buttplug": "^5.0.1", "class-transformer": "^0.5.1", "cors": "^2.8.5", "dotenv": "^17.2.0", diff --git a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts index 76eb060..dc1a0cf 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts @@ -62,33 +62,38 @@ export default class ButtplugIoDeviceFactory const attributes: ButtplugIoDeviceAttributes = {}; for (const [featureIndex, feature] of buttplugDevice.features) { - for (const outputType of Object.values(OutputType)) { - if (outputType === OutputType.Unknown || false === feature.hasOutput(outputType)) { + for (const [outputType, output] of feature.outputs) { + if (outputType === OutputType.Unknown) { continue; } + const attrName: ButtplugIoDeviceAttributeKey = `${outputType}-${featureIndex}`; attributes[attrName] = IntRangeDeviceAttribute.createInitialized( attrName, + feature.featureDescriptor, + DeviceAttributeModifier.readOnly, undefined, - DeviceAttributeModifier.writeOnly, - undefined, - Int.ZERO, - Int.from(100), + Int.from(output.valueRange[0]), + Int.from(output.valueRange[1]), Int.from(1), Int.ZERO ); } - for (const inputType of Object.values(InputType)) { - if (inputType === InputType.Unknown || false === feature.hasInput(inputType)) { + for (const [inputType, input] of feature.inputs) { + if (inputType === InputType.Unknown) { continue; } + const attrName: ButtplugIoDeviceAttributeKey = `${inputType}-${featureIndex}`; - attributes[attrName] = IntDeviceAttribute.createInitialized( + attributes[attrName] = IntRangeDeviceAttribute.createInitialized( attrName, + feature.featureDescriptor, + DeviceAttributeModifier.writeOnly, undefined, - DeviceAttributeModifier.readOnly, - undefined, + Int.from(input.valueRange[0]), + Int.from(input.valueRange[1]), + Int.from(1), Int.ZERO ); } From 240b793ca832d464347a22b630f2312d4a872e26 Mon Sep 17 00:00:00 2001 From: HRS Date: Sun, 7 Jun 2026 14:48:06 +0200 Subject: [PATCH 4/9] Fix linting and types --- .../automation/stopScriptController.ts | 4 +- .../protocol/buttplugIo/buttplugIoDevice.ts | 4 +- .../buttplugIo/buttplugIoDeviceFactory.ts | 53 +++++++++++-------- .../buttplugIo/buttplugIoDevice.spec.ts | 4 +- 4 files changed, 37 insertions(+), 28 deletions(-) diff --git a/src/controller/automation/stopScriptController.ts b/src/controller/automation/stopScriptController.ts index 5e88e1c..76749b8 100644 --- a/src/controller/automation/stopScriptController.ts +++ b/src/controller/automation/stopScriptController.ts @@ -11,9 +11,9 @@ export default class StopScriptController implements ControllerInterface this.scriptRuntime = scriptRuntime; } - public execute(req: Request, res: Response): void + public async execute(req: Request, res: Response): Promise { - this.scriptRuntime.stop(); + await this.scriptRuntime.stop(); res.sendStatus(200); } diff --git a/src/device/protocol/buttplugIo/buttplugIoDevice.ts b/src/device/protocol/buttplugIo/buttplugIoDevice.ts index b843b09..44464ca 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDevice.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDevice.ts @@ -51,8 +51,8 @@ export default class ButtplugIoDevice extends Device continue; } const response = await feature.runInput(inputType, InputCommandType.Read); - if (response?.Reading[inputType] !== undefined) { - this.attributes[attrKey].value = Int.from(response.Reading[inputType].Value); + if (response?.value !== undefined) { + this.attributes[attrKey].value = Int.from(response.value); } } } diff --git a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts index dc1a0cf..c384c1a 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts @@ -1,6 +1,6 @@ import UuidFactory from '../../../factory/uuidFactory.js'; import Settings from '../../../settings/settings.js'; -import { ButtplugClientDevice, InputType, OutputType } from 'buttplug'; +import { ButtplugClientDevice, ButtplugClientDeviceFeatureValueRange, InputType, OutputType } from 'buttplug'; import ButtplugIoDevice, { ButtplugIoDeviceAttributeKey, ButtplugIoDeviceAttributes } from './buttplugIoDevice.js'; import KnownDevice from '../../../settings/knownDevice.js'; import Logger from '../../../logging/Logger.js'; @@ -8,8 +8,8 @@ import { DeviceAttributeModifier } from '../../attribute/deviceAttribute.js'; import IntRangeDeviceAttribute from '../../attribute/intRangeDeviceAttribute.js'; import DateFactory from '../../../factory/dateFactory.js'; import { Int } from '../../../util/numbers.js'; -import IntDeviceAttribute from '../../attribute/intDeviceAttribute.js'; import EventEmitterFactory from '../../../factory/eventEmitterFactory.js'; +import BoolDeviceAttribute from '../../attribute/boolDeviceAttribute.js'; export default class ButtplugIoDeviceFactory @@ -68,16 +68,8 @@ export default class ButtplugIoDeviceFactory } const attrName: ButtplugIoDeviceAttributeKey = `${outputType}-${featureIndex}`; - attributes[attrName] = IntRangeDeviceAttribute.createInitialized( - attrName, - feature.featureDescriptor, - DeviceAttributeModifier.readOnly, - undefined, - Int.from(output.valueRange[0]), - Int.from(output.valueRange[1]), - Int.from(1), - Int.ZERO - ); + + attributes[attrName] = this.createAttribute(attrName, feature.featureDescriptor, output.valueRange); } for (const [inputType, input] of feature.inputs) { @@ -86,22 +78,39 @@ export default class ButtplugIoDeviceFactory } const attrName: ButtplugIoDeviceAttributeKey = `${inputType}-${featureIndex}`; - attributes[attrName] = IntRangeDeviceAttribute.createInitialized( - attrName, - feature.featureDescriptor, - DeviceAttributeModifier.writeOnly, - undefined, - Int.from(input.valueRange[0]), - Int.from(input.valueRange[1]), - Int.from(1), - Int.ZERO - ); + + attributes[attrName] = this.createAttribute(attrName, feature.featureDescriptor, input.valueRange); } } return attributes; } + private static createAttribute( + attrName: ButtplugIoDeviceAttributeKey, + featureDescriptor: string, + valueRange: ButtplugClientDeviceFeatureValueRange + ): IntRangeDeviceAttribute|BoolDeviceAttribute { + const [valueRangeMin, valueRangeMax] = valueRange; + + if (valueRangeMin === 0 && valueRangeMax === 1) { + return BoolDeviceAttribute.createInitialized( + attrName, featureDescriptor, DeviceAttributeModifier.writeOnly, false + ); + } + + return IntRangeDeviceAttribute.createInitialized( + attrName, + featureDescriptor, + DeviceAttributeModifier.writeOnly, + undefined, + Int.from(valueRangeMin), + Int.from(valueRangeMax), + Int.from(1), + Int.ZERO + ); + } + private createKnownDevice(buttplugDevice: ButtplugClientDevice, provider: string, useDeviceNameAsId: boolean): KnownDevice { // Since we don't get a unique identifier for the Bluetooth device from Intiface, // we need to use the index assigned to the device by Intiface. It's the best we have. diff --git a/tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts b/tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts index c9c7b81..aa3bfc1 100644 --- a/tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts +++ b/tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts @@ -68,7 +68,7 @@ describe('ButtplugIoDevice', () => { expect(runOutput).toHaveBeenCalledTimes(1); const cmd = runOutput.mock.calls[0][0]; expect(cmd.outputType).toBe(OutputType.Vibrate); - expect(cmd.value.percent).toBe(0); + expect(cmd.value).toBe(0); }); it('it updates device data and calls buttplugClientDevice on setting range attribute', async () => { @@ -103,6 +103,6 @@ describe('ButtplugIoDevice', () => { expect(runOutput).toHaveBeenCalledTimes(1); const cmd = runOutput.mock.calls[0][0]; expect(cmd.outputType).toBe(OutputType.Vibrate); - expect(cmd.value.percent).toBeCloseTo(newValue / 20); + expect(cmd.value).toBeCloseTo(newValue / 20); }); }); From bf383b967251808eef3a3dae53952def776bd452 Mon Sep 17 00:00:00 2001 From: HRS Date: Sun, 7 Jun 2026 14:50:46 +0200 Subject: [PATCH 5/9] Use actual values, not percentage --- .../protocol/buttplugIo/buttplugIoDevice.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/device/protocol/buttplugIo/buttplugIoDevice.ts b/src/device/protocol/buttplugIo/buttplugIoDevice.ts index 44464ca..6c4ddef 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDevice.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDevice.ts @@ -109,15 +109,15 @@ export default class ButtplugIoDevice extends Device private static createOutputCommand(type: OutputType, value: number): DeviceOutputCommand { switch (type) { - case OutputType.Vibrate: return DeviceOutput.Vibrate.percent(value); - case OutputType.Rotate: return DeviceOutput.Rotate.percent(value); - case OutputType.Oscillate: return DeviceOutput.Oscillate.percent(value); - case OutputType.Constrict: return DeviceOutput.Constrict.percent(value); - case OutputType.Inflate: return DeviceOutput.Inflate.percent(value); - case OutputType.Temperature: return DeviceOutput.Temperature.percent(value); - case OutputType.Led: return DeviceOutput.Led.percent(value); - case OutputType.Spray: return DeviceOutput.Spray.percent(value); - case OutputType.Position: return DeviceOutput.Position.percent(value); + case OutputType.Vibrate: return DeviceOutput.Vibrate.value(value); + case OutputType.Rotate: return DeviceOutput.Rotate.value(value); + case OutputType.Oscillate: return DeviceOutput.Oscillate.value(value); + case OutputType.Constrict: return DeviceOutput.Constrict.value(value); + case OutputType.Inflate: return DeviceOutput.Inflate.value(value); + case OutputType.Temperature: return DeviceOutput.Temperature.value(value); + case OutputType.Led: return DeviceOutput.Led.value(value); + case OutputType.Spray: return DeviceOutput.Spray.value(value); + case OutputType.Position: return DeviceOutput.Position.value(value); default: throw new Error(`Unsupported output type: ${type}`); } } From 6b6c2381e89748048e814f2b22fba4370dcb9697 Mon Sep 17 00:00:00 2001 From: HRS Date: Sun, 7 Jun 2026 14:55:28 +0200 Subject: [PATCH 6/9] Fix the i/o confusion --- src/device/protocol/buttplugIo/buttplugIoDevice.ts | 6 +++--- .../protocol/buttplugIo/buttplugIoDeviceFactory.ts | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/device/protocol/buttplugIo/buttplugIoDevice.ts b/src/device/protocol/buttplugIo/buttplugIoDevice.ts index 6c4ddef..0b3dcdb 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDevice.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDevice.ts @@ -8,9 +8,9 @@ import IntDeviceAttribute from '../../attribute/intDeviceAttribute.js'; import { DeviceAttributeModifier } from '../../attribute/deviceAttribute.js'; import EventEmitter from 'events'; -type ButtplugActuatorTypeKey = `${OutputType}-${number}`; -type ButtplugSensorTypeKey = `${InputType}-${number}`; -export type ButtplugIoDeviceAttributeKey = ButtplugActuatorTypeKey | ButtplugSensorTypeKey; +type ButtplugOutputTypeKey = `${OutputType}-${number}`; +type ButtplugInputTypeKey = `${InputType}-${number}`; +export type ButtplugIoDeviceAttributeKey = ButtplugOutputTypeKey | ButtplugInputTypeKey; export type ButtplugIoDeviceAttributes = Record< ButtplugIoDeviceAttributeKey, diff --git a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts index c384c1a..98dc94a 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts @@ -69,7 +69,7 @@ export default class ButtplugIoDeviceFactory const attrName: ButtplugIoDeviceAttributeKey = `${outputType}-${featureIndex}`; - attributes[attrName] = this.createAttribute(attrName, feature.featureDescriptor, output.valueRange); + attributes[attrName] = this.createAttribute(attrName, feature.featureDescriptor, output.valueRange, DeviceAttributeModifier.writeOnly); } for (const [inputType, input] of feature.inputs) { @@ -79,7 +79,7 @@ export default class ButtplugIoDeviceFactory const attrName: ButtplugIoDeviceAttributeKey = `${inputType}-${featureIndex}`; - attributes[attrName] = this.createAttribute(attrName, feature.featureDescriptor, input.valueRange); + attributes[attrName] = this.createAttribute(attrName, feature.featureDescriptor, input.valueRange, DeviceAttributeModifier.readOnly); } } @@ -89,7 +89,8 @@ export default class ButtplugIoDeviceFactory private static createAttribute( attrName: ButtplugIoDeviceAttributeKey, featureDescriptor: string, - valueRange: ButtplugClientDeviceFeatureValueRange + valueRange: ButtplugClientDeviceFeatureValueRange, + attributeModifier: DeviceAttributeModifier ): IntRangeDeviceAttribute|BoolDeviceAttribute { const [valueRangeMin, valueRangeMax] = valueRange; @@ -102,7 +103,7 @@ export default class ButtplugIoDeviceFactory return IntRangeDeviceAttribute.createInitialized( attrName, featureDescriptor, - DeviceAttributeModifier.writeOnly, + attributeModifier, undefined, Int.from(valueRangeMin), Int.from(valueRangeMax), From 02d29d2969c5f4445cd0b3eb2a80dd094a84c800 Mon Sep 17 00:00:00 2001 From: HRS Date: Sun, 7 Jun 2026 15:01:18 +0200 Subject: [PATCH 7/9] Fix input refresh --- src/device/protocol/buttplugIo/buttplugIoDevice.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/device/protocol/buttplugIo/buttplugIoDevice.ts b/src/device/protocol/buttplugIo/buttplugIoDevice.ts index 0b3dcdb..29782e6 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDevice.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDevice.ts @@ -42,15 +42,15 @@ export default class ButtplugIoDevice extends Device protected override async doRefresh(): Promise { for (const [featureIndex, feature] of this.buttplugClientDevice.features) { - for (const inputType of Object.values(InputType)) { - if (inputType === InputType.Unknown || false === feature.hasInput(inputType)) { + for (const [, input] of feature.inputs) { + if (input.type === InputType.Unknown) { continue; } - const attrKey = `${inputType}-${featureIndex}` as ButtplugIoDeviceAttributeKey; + const attrKey = `${input.type}-${featureIndex}` as ButtplugIoDeviceAttributeKey; if (this.attributes[attrKey] === undefined) { continue; } - const response = await feature.runInput(inputType, InputCommandType.Read); + const response = await feature.runInput(input.type, InputCommandType.Read); if (response?.value !== undefined) { this.attributes[attrKey].value = Int.from(response.value); } From f7895e76ee54043475dcf0ed38fedfb58d87cb16 Mon Sep 17 00:00:00 2001 From: HRS Date: Sun, 7 Jun 2026 15:03:26 +0200 Subject: [PATCH 8/9] Only use value --- src/device/protocol/buttplugIo/buttplugIoDevice.ts | 4 ++-- .../protocol/buttplugIo/buttplugIoDeviceFactory.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/device/protocol/buttplugIo/buttplugIoDevice.ts b/src/device/protocol/buttplugIo/buttplugIoDevice.ts index 29782e6..c7066fc 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDevice.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDevice.ts @@ -41,12 +41,12 @@ export default class ButtplugIoDevice extends Device } protected override async doRefresh(): Promise { - for (const [featureIndex, feature] of this.buttplugClientDevice.features) { + for (const [, feature] of this.buttplugClientDevice.features) { for (const [, input] of feature.inputs) { if (input.type === InputType.Unknown) { continue; } - const attrKey = `${input.type}-${featureIndex}` as ButtplugIoDeviceAttributeKey; + const attrKey = `${input.type}-${feature.index}` as ButtplugIoDeviceAttributeKey; if (this.attributes[attrKey] === undefined) { continue; } diff --git a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts index 98dc94a..77eb766 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts @@ -61,23 +61,23 @@ export default class ButtplugIoDeviceFactory private static parseDeviceAttributes(buttplugDevice: ButtplugClientDevice): ButtplugIoDeviceAttributes { const attributes: ButtplugIoDeviceAttributes = {}; - for (const [featureIndex, feature] of buttplugDevice.features) { + for (const [, feature] of buttplugDevice.features) { for (const [outputType, output] of feature.outputs) { if (outputType === OutputType.Unknown) { continue; } - const attrName: ButtplugIoDeviceAttributeKey = `${outputType}-${featureIndex}`; + const attrName: ButtplugIoDeviceAttributeKey = `${outputType}-${feature.index}`; attributes[attrName] = this.createAttribute(attrName, feature.featureDescriptor, output.valueRange, DeviceAttributeModifier.writeOnly); } - for (const [inputType, input] of feature.inputs) { - if (inputType === InputType.Unknown) { + for (const [, input] of feature.inputs) { + if (input.type === InputType.Unknown) { continue; } - const attrName: ButtplugIoDeviceAttributeKey = `${inputType}-${featureIndex}`; + const attrName: ButtplugIoDeviceAttributeKey = `${input.type}-${feature.index}`; attributes[attrName] = this.createAttribute(attrName, feature.featureDescriptor, input.valueRange, DeviceAttributeModifier.readOnly); } From 3b74202f95a16abef580734180587935bf151aec Mon Sep 17 00:00:00 2001 From: HRS Date: Sun, 7 Jun 2026 18:08:18 +0200 Subject: [PATCH 9/9] wip --- src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts | 2 +- .../protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts index 77eb766..47d2d00 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts @@ -102,7 +102,7 @@ export default class ButtplugIoDeviceFactory return IntRangeDeviceAttribute.createInitialized( attrName, - featureDescriptor, + featureDescriptor || undefined, attributeModifier, undefined, Int.from(valueRangeMin), diff --git a/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts b/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts index a84f92f..6f26232 100644 --- a/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts +++ b/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts @@ -43,7 +43,7 @@ export default class ButtplugIoWebsocketDeviceProvider extends DeviceProvider logError(this.logger, `Error in disconnect handler`, e)