diff --git a/src/device/protocol/airotic/airoticDevice.ts b/src/device/protocol/airotic/airoticDevice.ts index 94e03b6..7c18293 100644 --- a/src/device/protocol/airotic/airoticDevice.ts +++ b/src/device/protocol/airotic/airoticDevice.ts @@ -8,7 +8,7 @@ import { DeviceId } from '../../deviceId.js'; import Logger from '../../../logging/Logger.js'; import BleDevice from '../../bleDevice.js'; import MessageResponseHandler from '../messageResponseHandler.js'; -import AiroticProtocol from './airtonicProtocol.js'; +import AiroticProtocol from './airoticProtocol.js'; import BoolDeviceAttribute from '../../attribute/boolDeviceAttribute.js'; import FloatDeviceAttribute from '../../attribute/floatDeviceAttribute.js'; import { sleep } from '../../../util/async.js'; diff --git a/src/device/protocol/airotic/airoticDeviceFactory.ts b/src/device/protocol/airotic/airoticDeviceFactory.ts new file mode 100644 index 0000000..e8a7627 --- /dev/null +++ b/src/device/protocol/airotic/airoticDeviceFactory.ts @@ -0,0 +1,110 @@ +import { Peripheral } from '@stoprocent/noble'; +import DateFactory from '../../../factory/dateFactory.js'; +import EventEmitterFactory from '../../../factory/eventEmitterFactory.js'; +import Logger from '../../../logging/Logger.js'; +import KnownDeviceRegistry from '../../knownDeviceRegistry.js'; +import { DeviceAttributeModifier } from '../../attribute/deviceAttribute.js'; +import StrDeviceAttribute from '../../attribute/strDeviceAttribute.js'; +import BoolDeviceAttribute from '../../attribute/boolDeviceAttribute.js'; +import FloatDeviceAttribute from '../../attribute/floatDeviceAttribute.js'; +import BleUartDeviceTransport from '../../transport/bleDeviceTransport.js'; +import { hsvByteToRgb } from '../../../util/color.js'; +import { DeviceId } from '../../deviceId.js'; +import AiroticDevice, { AiroticDeviceAttributes } from './airoticDevice.js'; +import AiroticProtocol from './airoticProtocol.js'; +import MessageResponseHandler from '../messageResponseHandler.js'; + +export default class AiroticDeviceFactory +{ + private readonly dateFactory: DateFactory; + + private readonly eventEmitterFactory: EventEmitterFactory; + + private readonly knownDeviceRegistry: KnownDeviceRegistry; + + private readonly logger: Logger; + + public constructor( + dateFactory: DateFactory, + eventEmitterFactory: EventEmitterFactory, + knownDeviceRegistry: KnownDeviceRegistry, + logger: Logger + ) { + this.dateFactory = dateFactory; + this.eventEmitterFactory = eventEmitterFactory; + this.knownDeviceRegistry = knownDeviceRegistry; + this.logger = logger; + } + + public create( + deviceId: DeviceId, + peripheral: Peripheral, + transport: BleUartDeviceTransport, + messageResponseHandler: MessageResponseHandler, + provider: string + ): AiroticDevice { + const knownDevice = this.knownDeviceRegistry.resolve( + deviceId, + 'airotic', + provider, + peripheral.advertisement.localName ?? `Airotic ${deviceId}`, + ); + + const advertisedColors = this.parseAdvertisedColors(peripheral.advertisement.manufacturerData); + const attributes = this.getAttributes(advertisedColors); + + const device = new AiroticDevice( + knownDevice.id, + knownDevice.name, + provider, + peripheral, + transport, + messageResponseHandler, + this.dateFactory.now(), + true, + attributes, + {}, + this.eventEmitterFactory.create(), + this.logger, + ); + + this.knownDeviceRegistry.persist(knownDevice); + + return device; + } + + private getAttributes(advertisedColors: { restColor?: string, breathInColor?: string } | undefined): AiroticDeviceAttributes { + return { + restColor: StrDeviceAttribute.create('restColor', 'Rest Color', DeviceAttributeModifier.readWrite, advertisedColors?.restColor), + breathInColor: StrDeviceAttribute.create('breathInColor', 'Breath In Color', DeviceAttributeModifier.readWrite, advertisedColors?.breathInColor), + resetColors: BoolDeviceAttribute.create('resetColors', 'Reset Colors', DeviceAttributeModifier.writeOnly), + reboot: BoolDeviceAttribute.create('reboot', 'Reboot bottle', DeviceAttributeModifier.writeOnly), + breathsPerMin: FloatDeviceAttribute.create('breathsPerMin', 'Breaths/min', DeviceAttributeModifier.readOnly, 'breaths/min'), + bpmTrend: StrDeviceAttribute.create('bpmTrend', 'BPM Trend', DeviceAttributeModifier.readOnly), + }; + } + + /** + * Parses colors broadcast in the BLE advertising manufacturer data payload, so + * initial rest/breath-in colors can be reflected without needing a UART round-trip. + * Byte layout (0-indexed): 3 = colorStart.h, 4 = colorStart.s, 5 = colorTarget.h, 6 = colorTarget.s + */ + private parseAdvertisedColors(manufacturerData: Buffer | undefined): { restColor?: string, breathInColor?: string } | undefined { + if (undefined === manufacturerData || manufacturerData.length < 7) { + return undefined; + } + + const colorStartH = manufacturerData[3]; + const colorStartS = manufacturerData[4]; + const colorTargetH = manufacturerData[5]; + const colorTargetS = manufacturerData[6]; + + const target = hsvByteToRgb(colorTargetH, colorTargetS, 255); + const start = hsvByteToRgb(colorStartH, colorStartS, 255); + + return { + restColor: `${start.r},${start.g},${start.b}`, + breathInColor: `${target.r},${target.g},${target.b}`, + }; + } +} diff --git a/src/device/protocol/airotic/airoticDeviceProvider.ts b/src/device/protocol/airotic/airoticDeviceProvider.ts index 3a70b50..5dd5c05 100644 --- a/src/device/protocol/airotic/airoticDeviceProvider.ts +++ b/src/device/protocol/airotic/airoticDeviceProvider.ts @@ -6,15 +6,10 @@ import Logger from '../../../logging/Logger.js'; import { promiseWithTimeout } from '../../../util/async.js'; import { BleDeviceInfo } from '../../transport/bleObserver.js'; import BleUartDeviceTransport from '../../transport/bleDeviceTransport.js'; -import AiroticProtocol from './airtonicProtocol.js'; +import AiroticProtocol from './airoticProtocol.js'; import MessageResponseHandler from '../messageResponseHandler.js'; -import StrDeviceAttribute from '../../attribute/strDeviceAttribute.js'; -import { DeviceAttributeModifier } from '../../attribute/deviceAttribute.js'; -import KnownDeviceRegistry from '../../knownDeviceRegistry.js'; -import BoolDeviceAttribute from '../../attribute/boolDeviceAttribute.js'; -import FloatDeviceAttribute from '../../attribute/floatDeviceAttribute.js'; import BleDeviceProvider from '../../provider/bleDeviceProvider.js'; -import { hsvByteToRgb } from '../../../util/color.js'; +import AiroticDeviceFactory from './airoticDeviceFactory.js'; export default class AiroticDeviceProvider extends BleDeviceProvider { @@ -23,12 +18,12 @@ export default class AiroticDeviceProvider extends BleDeviceProvider { @@ -54,64 +49,13 @@ export default class AiroticDeviceProvider extends BleDeviceProvider): Promise { diff --git a/src/device/protocol/airotic/airtonicProtocol.ts b/src/device/protocol/airotic/airoticProtocol.ts similarity index 100% rename from src/device/protocol/airotic/airtonicProtocol.ts rename to src/device/protocol/airotic/airoticProtocol.ts diff --git a/src/device/protocol/zc95/Zc95SerialReader.ts b/src/device/protocol/zc95/Zc95SerialReader.ts deleted file mode 100644 index 1ffa4df..0000000 --- a/src/device/protocol/zc95/Zc95SerialReader.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { EventEmitter } from 'events'; - -const STX = 0x02; -const ETX = 0x03; -const MAX_MESSAGE_SIZE = 4096; - -export default class Zc95SerialReader extends EventEmitter { - private state: 'IDLE' | 'RECV' = 'IDLE'; - private message: number[] = []; - - public constructor() { - super(); - } - - public onData(data: Buffer): void { - for (const byte of data) { - if (this.state === 'IDLE') { - if (byte === STX) { - this.state = 'RECV'; - this.message = []; - } - } else if (this.state === 'RECV') { - if (byte === ETX) { - const messageString = Buffer.from(this.message).toString('utf-8'); - this.emit('receive', messageString); - this.state = 'IDLE'; - } else { - this.message.push(byte); - - if (this.message.length >= MAX_MESSAGE_SIZE) { - this.emit('error', new Error(`Frame size exceeded maximum of ${MAX_MESSAGE_SIZE} bytes`)); - this.state = 'IDLE'; - this.message = []; - } - } - } - } - } -} diff --git a/src/serviceMap.ts b/src/serviceMap.ts index 3554fc7..53f2339 100644 --- a/src/serviceMap.ts +++ b/src/serviceMap.ts @@ -50,6 +50,7 @@ import Zc95SerialDeviceProvider from './device/protocol/zc95/zc95SerialDevicePro import EStim2bSerialDeviceProvider from './device/protocol/estim2b/estim2bSerialDeviceProvider.js'; import ButtplugIoWebsocketDeviceProvider from './device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.js'; import AiroticDeviceProvider from './device/protocol/airotic/airoticDeviceProvider.js'; +import AiroticDeviceFactory from './device/protocol/airotic/airoticDeviceFactory.js'; import KnownDeviceRegistry from './device/knownDeviceRegistry.js'; @@ -68,6 +69,7 @@ type ServiceMap = { 'device.serial.factory.slvCtrlPlus': SlvCtrlPlusDeviceFactory, 'device.factory.zc95': Zc95DeviceFactory, 'device.factory.estim2b': Estim2bDeviceFactory, + 'device.factory.airotic': AiroticDeviceFactory, 'device.provider.factory.slvCtrlPlusSerial': DeviceProviderFactory, 'device.provider.factory.zc95Serial': DeviceProviderFactory, 'device.provider.factory.estim2bSerial': DeviceProviderFactory, diff --git a/src/serviceProvider/deviceServiceProvider.ts b/src/serviceProvider/deviceServiceProvider.ts index df31d59..698e431 100644 --- a/src/serviceProvider/deviceServiceProvider.ts +++ b/src/serviceProvider/deviceServiceProvider.ts @@ -38,6 +38,7 @@ import EStim2bSerialDeviceProvider from '../device/protocol/estim2b/estim2bSeria import Estim2bDeviceFactory from '../device/protocol/estim2b/estim2bDeviceFactory.js'; import BleObserver from '../device/transport/bleObserver.js'; import AiroticDeviceProvider from '../device/protocol/airotic/airoticDeviceProvider.js'; +import AiroticDeviceFactory from '../device/protocol/airotic/airoticDeviceFactory.js'; import DeviceProviderFactory from '../device/provider/deviceProviderFactory.js'; import { DeviceId } from '../device/deviceId.js'; import KnownDeviceRegistry from '../device/knownDeviceRegistry.js'; @@ -125,6 +126,13 @@ export default class DeviceServiceProvider implements ServiceProvider new AiroticDeviceFactory( + container.get('factory.date'), + container.get('factory.eventEmitter'), + container.get('device.knownDeviceRegistry'), + container.get('logger.default'), + )); + container.set('device.provider.factory.virtual', () => new VirtualDeviceProviderFactory( container.get('device.manager'), container.get('factory.eventEmitter'), @@ -233,7 +241,7 @@ export default class DeviceServiceProvider implements ServiceProvider {