Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/device/protocol/airotic/airoticDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
110 changes: 110 additions & 0 deletions src/device/protocol/airotic/airoticDeviceFactory.ts
Original file line number Diff line number Diff line change
@@ -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<AiroticProtocol>,
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];
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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}`,
};
}
}
70 changes: 7 additions & 63 deletions src/device/protocol/airotic/airoticDeviceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AiroticDevice>
{
Expand All @@ -23,12 +18,12 @@ export default class AiroticDeviceProvider extends BleDeviceProvider<AiroticDevi
private static readonly UART_RX_CHAR_UUID = '6e400002b5a3f393e0a9e50e24dcca9e';
private static readonly UART_TX_CHAR_UUID = '6e400003b5a3f393e0a9e50e24dcca9e';

private readonly knownDeviceRegistry: KnownDeviceRegistry;
private readonly deviceFactory: AiroticDeviceFactory;

public constructor(deviceManager: DeviceManager, knownDeviceRegistry: KnownDeviceRegistry, eventEmitter: EventEmitter, logger: Logger) {
public constructor(deviceManager: DeviceManager, deviceFactory: AiroticDeviceFactory, eventEmitter: EventEmitter, logger: Logger) {
super(deviceManager, eventEmitter, logger.child({ name: AiroticDeviceProvider.name }));

this.knownDeviceRegistry = knownDeviceRegistry;
this.deviceFactory = deviceFactory;
}

public override async init(): Promise<void> {
Expand All @@ -54,64 +49,13 @@ export default class AiroticDeviceProvider extends BleDeviceProvider<AiroticDevi
return undefined;
}

const knownDevice = this.knownDeviceRegistry.resolve(
return this.deviceFactory.create(
deviceInfo.id,
'airotic',
AiroticDeviceProvider.providerName,
deviceInfo.peripheral.advertisement.localName ?? `Airotic ${deviceInfo.id}`,
);

const advertisedColors = this.parseAdvertisedColors(deviceInfo.peripheral.advertisement.manufacturerData);

const device = new AiroticDevice(
knownDevice.id,
knownDevice.name,
AiroticDeviceProvider.providerName,
deviceInfo.peripheral,
transport,
messageResponseHandler,
new Date(),
true,
{
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),
},
{},
new EventEmitter(),
this.logger,
AiroticDeviceProvider.providerName,
);

this.knownDeviceRegistry.persist(knownDevice);

return device;
}

/**
* 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 = colorTarget.h, 4 = colorTarget.s, 5 = colorStart.h, 6 = colorStart.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}`,
};
}

private async doHandshake(messageResponseHandler: MessageResponseHandler<AiroticProtocol>): Promise<boolean> {
Expand Down
39 changes: 0 additions & 39 deletions src/device/protocol/zc95/Zc95SerialReader.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/serviceMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';


Expand All @@ -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<SlvCtrlPlusSerialDeviceProvider>,
'device.provider.factory.zc95Serial': DeviceProviderFactory<Zc95SerialDeviceProvider>,
'device.provider.factory.estim2bSerial': DeviceProviderFactory<EStim2bSerialDeviceProvider>,
Expand Down
10 changes: 9 additions & 1 deletion src/serviceProvider/deviceServiceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -125,6 +126,13 @@ export default class DeviceServiceProvider implements ServiceProvider<ServiceMap
container.get('logger.default'),
));

container.set('device.factory.airotic', () => 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'),
Expand Down Expand Up @@ -233,7 +241,7 @@ export default class DeviceServiceProvider implements ServiceProvider<ServiceMap
return new GenericDeviceProviderFactory(
AiroticDeviceProvider,
container.get('device.manager'),
container.get('device.knownDeviceRegistry'),
container.get('device.factory.airotic'),
container.get('factory.eventEmitter').create(),
container.get('logger.default'),
);
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/device/protocol/airotic/airoticDevice.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Peripheral } from '@stoprocent/noble';
import EventEmitter from 'events';
import { DeviceId } from '../../../../../src/device/deviceId.js';
import AiroticDevice, { AiroticDeviceAttributes } from '../../../../../src/device/protocol/airotic/airoticDevice.js';
import AiroticProtocol from '../../../../../src/device/protocol/airotic/airtonicProtocol.js';
import AiroticProtocol from '../../../../../src/device/protocol/airotic/airoticProtocol.js';
import MessageResponseHandler from '../../../../../src/device/protocol/messageResponseHandler.js';
import StrDeviceAttribute from '../../../../../src/device/attribute/strDeviceAttribute.js';
import BoolDeviceAttribute from '../../../../../src/device/attribute/boolDeviceAttribute.js';
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/device/protocol/airotic/airoticProtocol.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import AiroticProtocol from '../../../../../src/device/protocol/airotic/airtonicProtocol.js';
import AiroticProtocol from '../../../../../src/device/protocol/airotic/airoticProtocol.js';
import { expectToBeSuccessfulDecodeResult } from '../../../helper/protocol.js';

describe('AiroticProtocol', () => {
Expand Down
Loading