diff --git a/src/device/device.ts b/src/device/device.ts index cd2fa0f6..812b74a0 100644 --- a/src/device/device.ts +++ b/src/device/device.ts @@ -1,5 +1,8 @@ import {Exclude, Expose} from "class-transformer"; import DeviceState from "./deviceState.js"; +import DeviceProviderEvent from "./provider/deviceProviderEvent.js"; +import EventEmitter from "events"; +import DeviceEvent from "./deviceEvent.js"; @Exclude() export default abstract class Device @@ -28,24 +31,57 @@ export default abstract class Device @Expose() protected lastRefresh: Date; + private readonly eventEmitter: EventEmitter; + + private refreshTimer: NodeJS.Timeout; + protected constructor( deviceId: string, deviceName: string, provider: string, connectedSince: Date, - controllable: boolean + controllable: boolean, + eventEmitter: EventEmitter ) { this.deviceId = deviceId; this.deviceName = deviceName; this.provider = provider; this.connectedSince = connectedSince; this.controllable = controllable; + this.eventEmitter = eventEmitter; this.state = DeviceState.ready; } public abstract refreshData(): Promise; - public updateLastRefresh(): void + public start(): void + { + const deviceStatusUpdater = () => { + if (this.getState === DeviceState.busy) { + return; + } + + this.refreshData().then(() => this.updateLastRefresh()).catch( + (e: Error) => this.eventEmitter.emit(DeviceEvent.deviceError, this, e) + ); + + this.eventEmitter.emit(DeviceEvent.deviceRefreshed, this); + }; + + this.refreshTimer = setInterval(deviceStatusUpdater, this.getRefreshInterval); + + setTimeout(deviceStatusUpdater, 0); // Immediately execute async + } + + public stop(): void + { + if (undefined !== this.refreshTimer) { + clearInterval(this.refreshTimer); + this.refreshTimer = undefined; + } + } + + protected updateLastRefresh(): void { this.lastRefresh = new Date(); } @@ -72,4 +108,11 @@ export default abstract class Device public get getState(): DeviceState { return this.state; } + + public on(event: DeviceEvent, listener: ((device: Device) => void)|((device: Device, e: Error) => void)): this + { + this.eventEmitter.on(event, listener); + + return this; + } } diff --git a/src/device/deviceEvent.ts b/src/device/deviceEvent.ts new file mode 100644 index 00000000..81f9ea7b --- /dev/null +++ b/src/device/deviceEvent.ts @@ -0,0 +1,8 @@ +enum DeviceEvent { + deviceConnected = "deviceConnected", + deviceDisconnected = "deviceDisconnected", + deviceRefreshed = "deviceRefreshed", + deviceError = "deviceError", +} + +export default DeviceEvent; diff --git a/src/device/deviceManager.ts b/src/device/deviceManager.ts index 1e63851c..e8bd91d3 100644 --- a/src/device/deviceManager.ts +++ b/src/device/deviceManager.ts @@ -3,33 +3,43 @@ import EventEmitter from "events"; import DeviceProvider from "./provider/deviceProvider.js"; import DeviceManagerEvent from "./deviceManagerEvent.js"; import DeviceProviderEvent from "./provider/deviceProviderEvent.js"; +import DeviceEvent from "./deviceEvent.js"; +import Logger from "../logging/Logger.js"; export default class DeviceManager { - private eventEmitter: EventEmitter; + private readonly eventEmitter: EventEmitter; private connectedDevices: Map; private deviceProviders: DeviceProvider[] = []; - public constructor(eventEmitter: EventEmitter, connectedDevices: Map) { + private readonly logger: Logger; + + public constructor(eventEmitter: EventEmitter, connectedDevices: Map, logger: Logger) { this.eventEmitter = eventEmitter; this.connectedDevices = connectedDevices; + this.logger = logger; } - public registerDeviceProvider(deviceProvider: DeviceProvider): void + public async registerDeviceProvider(deviceProvider: DeviceProvider): Promise { deviceProvider.on(DeviceProviderEvent.deviceConnected, (device: Device) => this.addDevice(device)); deviceProvider.on(DeviceProviderEvent.deviceDisconnected, (device: Device) => this.removeDevice(device)); - deviceProvider.on(DeviceProviderEvent.deviceRefreshed, (device: Device) => this.refreshDevice(device)); this.deviceProviders.push(deviceProvider); - void deviceProvider.init(this); + await deviceProvider.init(this); } public addDevice(device: Device): void { + device.on(DeviceEvent.deviceError, (d: Device, e: Error) => { + this.logger.error(`Error for device ${d.getDeviceId}: ${e.message}`, e); + }); + + device.on(DeviceEvent.deviceRefreshed, (d: Device) => this.refreshDevice(d)) + this.connectedDevices.set(device.getDeviceId, device); this.eventEmitter.emit(DeviceManagerEvent.deviceConnected, device); } @@ -40,7 +50,7 @@ export default class DeviceManager this.eventEmitter.emit(DeviceManagerEvent.deviceDisconnected, device); } - public refreshDevice(device: Device) + private refreshDevice(device: Device) { this.eventEmitter.emit(DeviceManagerEvent.deviceRefreshed, device); } @@ -61,4 +71,11 @@ export default class DeviceManager { this.eventEmitter.on(event, listener); } + + public async close(): Promise + { + for (const p of this.deviceProviders) { + await p.close(); + } + } } diff --git a/src/device/protocol/buttplugIo/buttplugIoDevice.ts b/src/device/protocol/buttplugIo/buttplugIoDevice.ts index 29edf9f3..108a7dc9 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDevice.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDevice.ts @@ -6,6 +6,7 @@ import GenericDeviceAttributeDiscriminator from "../../../serialization/discriminator/genericDeviceAttributeDiscriminator.js"; import RangeGenericDeviceAttribute from "../../attribute/rangeGenericDeviceAttribute.js"; import BoolGenericDeviceAttribute from "../../attribute/boolGenericDeviceAttribute.js"; +import EventEmitter from "events"; @Exclude() export default class ButtplugIoDevice extends Device @@ -29,9 +30,10 @@ export default class ButtplugIoDevice extends Device provider: string, connectedSince: Date, buttplugClientDevice: ButtplugClientDevice, - attributes: GenericDeviceAttribute[] + attributes: GenericDeviceAttribute[], + eventEmitter: EventEmitter, ) { - super(deviceId, deviceName, provider, connectedSince, true); + super(deviceId, deviceName, provider, connectedSince, true, eventEmitter); this.buttplugClientDevice = buttplugClientDevice; this.attributes = attributes; this.deviceModel = deviceModel; @@ -43,8 +45,6 @@ export default class ButtplugIoDevice extends Device const value = await this.buttplugClientDevice.sensorRead(sensor.Index, sensor.SensorType); this.data[`${sensor.SensorType}-${sensor.Index}`] = value[0]; } - - this.updateLastRefresh(); } private initData(attributes: GenericDeviceAttribute[]): void diff --git a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts index c52c5c82..841dafb3 100644 --- a/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts +++ b/src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts @@ -9,6 +9,7 @@ import FloatGenericDeviceAttribute from "../../attribute/floatGenericDeviceAttri import RangeGenericDeviceAttribute from "../../attribute/rangeGenericDeviceAttribute.js"; import BoolGenericDeviceAttribute from "../../attribute/boolGenericDeviceAttribute.js"; import DateFactory from "../../../factory/dateFactory.js"; +import EventEmitter from "events"; export default class ButtplugIoDeviceFactory @@ -21,11 +22,14 @@ export default class ButtplugIoDeviceFactory private readonly logger: Logger; - public constructor(uuidFactory: UuidFactory, dateFactory: DateFactory, settings: Settings, logger: Logger) { + private readonly eventEmitter: EventEmitter; + + public constructor(uuidFactory: UuidFactory, dateFactory: DateFactory, settings: Settings, logger: Logger, eventEmitter: EventEmitter) { this.uuidFactory = uuidFactory; this.dateFactory = dateFactory; this.settings = settings; this.logger = logger; + this.eventEmitter = eventEmitter; } public create(buttplugDevice: ButtplugClientDevice, provider: string, useDeviceNameAsId: boolean): ButtplugIoDevice { @@ -41,6 +45,7 @@ export default class ButtplugIoDeviceFactory this.dateFactory.now(), buttplugDevice, deviceAttrs, + this.eventEmitter, ); if (null === device) { diff --git a/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts b/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts index 8957e0d7..0988f74f 100644 --- a/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts +++ b/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts @@ -11,7 +11,6 @@ export default class ButtplugIoWebsocketDeviceProvider extends DeviceProvider public static readonly name = 'buttplugIoWebsocket'; private connectedDevices: Map = new Map(); - private deviceUpdaters: Map = new Map(); private buttplugConnector: ButtplugNodeWebsocketClientConnector; private buttplugClient: ButtplugClient; @@ -66,6 +65,12 @@ export default class ButtplugIoWebsocketDeviceProvider extends DeviceProvider }); } + public async close(): Promise { + this.connectedDevices.forEach(d => d.stop()); + + await this.buttplugClient.disconnect(); + } + private connectToServer(): void { if (this.buttplugClient.connected) { return; @@ -101,10 +106,10 @@ export default class ButtplugIoWebsocketDeviceProvider extends DeviceProvider try { const device = this.buttplugIoDeviceFactory.create(buttplugDevice, ButtplugIoWebsocketDeviceProvider.name, this.useDeviceNameAsId); - const deviceStatusUpdaterInterval = this.initDeviceStatusUpdater(device); + + device.start(); this.connectedDevices.set(buttplugDevice.index, device); - this.deviceUpdaters.set(buttplugDevice.index, deviceStatusUpdaterInterval); this.eventEmitter.emit(DeviceProviderEvent.deviceConnected, device); @@ -117,9 +122,9 @@ export default class ButtplugIoWebsocketDeviceProvider extends DeviceProvider private removeButtplugIoDevice(buttplugDevice: ButtplugClientDevice): void { const device = this.connectedDevices.get(buttplugDevice.index); - const deviceUpdaterInterval = this.deviceUpdaters.get(buttplugDevice.index); - clearInterval(deviceUpdaterInterval); + device.stop(); + this.eventEmitter.emit(DeviceProviderEvent.deviceDisconnected, device); this.connectedDevices.delete(buttplugDevice.index); diff --git a/src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts b/src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts index 5ff6abf8..a9e0c075 100644 --- a/src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts +++ b/src/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.ts @@ -5,6 +5,7 @@ import GenericDeviceAttributeDiscriminator from "../../../serialization/discrimi import DeviceState from "../../deviceState.js"; import DeviceTransport from "../../transport/deviceTransport.js"; import SlvCtrlPlusMessageParser from "./slvCtrlPlusMessageParser.js"; +import EventEmitter from "events"; @Exclude() export default class GenericSlvCtrlPlusDevice extends SlvCtrlPlusDevice @@ -37,9 +38,10 @@ export default class GenericSlvCtrlPlusDevice extends SlvCtrlPlusDevice connectedSince: Date, transport: DeviceTransport, protocolVersion: number, - attributes: GenericDeviceAttribute[] + attributes: GenericDeviceAttribute[], + eventEmitter: EventEmitter, ) { - super(deviceId, deviceName, provider, connectedSince, transport, false); + super(deviceId, deviceName, provider, connectedSince, transport, false, eventEmitter); this.deviceModel = deviceModel; this.fwVersion = fwVersion; @@ -64,8 +66,6 @@ export default class GenericSlvCtrlPlusDevice extends SlvCtrlPlusDevice this.data[attrKey] = ('' !== dataObj[attrKey]) ? attrDef.fromString(dataObj[attrKey]) : null; } - - this.updateLastRefresh(); } public async setAttribute(attributeName: string, value: string|number|boolean|null): Promise { diff --git a/src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts b/src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts index c620f9c3..76b18997 100644 --- a/src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts +++ b/src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts @@ -1,6 +1,7 @@ import {Exclude} from "class-transformer"; import Device from "../../device.js"; import DeviceTransport from "../../transport/deviceTransport.js"; +import EventEmitter from "events"; @Exclude() export default abstract class SlvCtrlPlusDevice extends Device @@ -13,9 +14,10 @@ export default abstract class SlvCtrlPlusDevice extends Device provider: string, connectedSince: Date, transport: DeviceTransport, - controllable: boolean + controllable: boolean, + eventEmitter: EventEmitter, ) { - super(deviceId, deviceName, provider, connectedSince, controllable); + super(deviceId, deviceName, provider, connectedSince, controllable, eventEmitter); this.transport = transport; } diff --git a/src/device/protocol/slvCtrlPlus/slvCtrlPlusDeviceFactory.ts b/src/device/protocol/slvCtrlPlus/slvCtrlPlusDeviceFactory.ts index 459b1e39..bae3f2d9 100644 --- a/src/device/protocol/slvCtrlPlus/slvCtrlPlusDeviceFactory.ts +++ b/src/device/protocol/slvCtrlPlus/slvCtrlPlusDeviceFactory.ts @@ -8,6 +8,7 @@ import DateFactory from "../../../factory/dateFactory.js"; import DeviceTransport from "../../transport/deviceTransport.js"; import SlvCtrlPlusMessageParser from "./slvCtrlPlusMessageParser.js"; import Logger from "../../../logging/Logger.js"; +import EventEmitter from "events"; export default class SlvCtrlPlusDeviceFactory { @@ -21,18 +22,22 @@ export default class SlvCtrlPlusDeviceFactory private readonly logger: Logger; + private readonly eventEmitter: EventEmitter; + public constructor( uuidFactory: UuidFactory, dateFactory: DateFactory, settings: Settings, nameGenerator: DeviceNameGenerator, - logger: Logger + logger: Logger, + eventEmitter: EventEmitter, ) { this.uuidFactory = uuidFactory; this.dateFactory = dateFactory; this.settings = settings; this.nameGenerator = nameGenerator; this.logger = logger; + this.eventEmitter = eventEmitter; } public async create(deviceInfoStr: string, transport: DeviceTransport, provider: string): Promise { @@ -52,7 +57,8 @@ export default class SlvCtrlPlusDeviceFactory this.dateFactory.now(), transport, Number(protocolVersion), - deviceAttrs + deviceAttrs, + this.eventEmitter, ); this.settings.addKnownDevice(knownDevice); diff --git a/src/device/protocol/slvCtrlPlus/slvCtrlPlusSerialDeviceProvider.ts b/src/device/protocol/slvCtrlPlus/slvCtrlPlusSerialDeviceProvider.ts index 6880c959..e8615eb9 100644 --- a/src/device/protocol/slvCtrlPlus/slvCtrlPlusSerialDeviceProvider.ts +++ b/src/device/protocol/slvCtrlPlus/slvCtrlPlusSerialDeviceProvider.ts @@ -24,6 +24,8 @@ export default class SlvCtrlPlusSerialDeviceProvider extends DeviceProvider private readonly deviceTransportFactory: SerialDeviceTransportFactory; + private discoverInterval: NodeJS.Timeout; + public constructor( eventEmitter: EventEmitter, deviceFactory: SlvCtrlPlusDeviceFactory, @@ -39,11 +41,24 @@ export default class SlvCtrlPlusSerialDeviceProvider extends DeviceProvider { return new Promise((resolve) => { // Scan for new SlvCtrl+ protocol serial devices every 3 seconds - setInterval(() => { this.discoverSerialDevices().catch((e: Error) => this.logger.error(e.message, e)) }, 3000); + this.discoverInterval = setInterval(() => { + this.discoverSerialDevices().catch((e: Error) => this.logger.error(e.message, e)) + }, 3000); resolve(); }) } + public async close(): Promise { + return new Promise((resolve) => { + if (undefined !== this.discoverInterval) { + clearInterval(this.discoverInterval); + this.discoverInterval = undefined; + } + + resolve(); + }); + } + private async discoverSerialDevices(): Promise { const foundDevices: Map = new Map(); @@ -149,7 +164,8 @@ export default class SlvCtrlPlusSerialDeviceProvider extends DeviceProvider transport, SlvCtrlPlusSerialDeviceProvider.name ); - const deviceStatusUpdaterInterval = this.initDeviceStatusUpdater(device); + + device.start(); this.connectedDevices.set(device.getDeviceId, device); @@ -159,7 +175,7 @@ export default class SlvCtrlPlusSerialDeviceProvider extends DeviceProvider this.logger.info('Connected devices: ' + this.connectedDevices.size.toString()); port.on('close', () => { - clearInterval(deviceStatusUpdaterInterval); + device.stop(); this.connectedDevices.delete(device.getDeviceId); this.eventEmitter.emit(DeviceProviderEvent.deviceDisconnected, device); diff --git a/src/device/provider/deviceProvider.ts b/src/device/provider/deviceProvider.ts index e0a89fda..148aac2a 100644 --- a/src/device/provider/deviceProvider.ts +++ b/src/device/provider/deviceProvider.ts @@ -2,7 +2,6 @@ import DeviceManager from "../deviceManager.js"; import EventEmitter from "events"; import Device from "../device.js"; import DeviceProviderEvent from "./deviceProviderEvent.js"; -import DeviceState from "../deviceState.js"; import Logger from "../../logging/Logger.js"; export default abstract class DeviceProvider @@ -18,32 +17,12 @@ export default abstract class DeviceProvider public abstract init(deviceManager: DeviceManager): Promise; + public abstract close(): Promise; + public on(event: DeviceProviderEvent, listener: (device: Device) => void): DeviceProvider { this.eventEmitter.on(event, listener); return this; } - - protected initDeviceStatusUpdater(device: Device): NodeJS.Timeout - { - const deviceStatusUpdater = () => { - if (device.getState === DeviceState.busy) { - this.logger.trace(`Device not refreshed since it's currently busy: ${device.getDeviceId}`) - return; - } - - device.refreshData().catch( - (e: Error) => this.logger.error(`device: ${device.getDeviceId} -> status -> failed: ${e.message}`) - ); - - this.eventEmitter.emit(DeviceProviderEvent.deviceRefreshed, device); - - this.logger.trace(`Device refreshed: ${device.getDeviceId}`) - }; - - deviceStatusUpdater(); - - return setInterval(deviceStatusUpdater, device.getRefreshInterval); - } } diff --git a/src/device/provider/deviceProviderLoader.ts b/src/device/provider/deviceProviderLoader.ts index 6bf44956..8b98e306 100644 --- a/src/device/provider/deviceProviderLoader.ts +++ b/src/device/provider/deviceProviderLoader.ts @@ -25,7 +25,7 @@ export default class DeviceProviderLoader this.logger = logger; } - public loadFromSettings(): void + public async loadFromSettings(): Promise { const configuredDeviceSources = this.settings.getDeviceSources(); @@ -40,7 +40,7 @@ export default class DeviceProviderLoader const factory = this.factories.get(deviceSource.type); const provider = factory.create(deviceSource.config); - this.deviceManager.registerDeviceProvider(provider); + await this.deviceManager.registerDeviceProvider(provider); } } } diff --git a/src/index.ts b/src/index.ts index 10846891..46b6a657 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,7 +48,11 @@ const io = container.get('server.websocket'); const deviceManager = container.get('device.manager'); const scriptRuntime = container.get('automation.scriptRuntime'); -container.get('device.provider.loader').loadFromSettings(); +try { + await container.get('device.provider.loader').loadFromSettings(); +} catch (e: unknown) { + logger.error(`Error loading device sources: ${(e as Error).message}`) +} // Middlewares app diff --git a/src/serviceMap.ts b/src/serviceMap.ts index 25eb1119..91462364 100644 --- a/src/serviceMap.ts +++ b/src/serviceMap.ts @@ -30,6 +30,7 @@ import GetLogController from "./controller/automation/getLogController.js"; import RunScriptController from "./controller/automation/runScriptController.js"; import StopScriptController from "./controller/automation/stopScriptController.js"; import StatusScriptController from "./controller/automation/statusScriptController.js"; +import EventEmitter from "events"; /* eslint-disable @typescript-eslint/naming-convention */ type ServiceMap = { @@ -42,6 +43,7 @@ type ServiceMap = { /* serverServiceProvider */ 'server.websocket': Server, + 'event.emitter': EventEmitter, /* deviceServiceProvider */ 'device.manager': DeviceManager, diff --git a/src/serviceProvider/serverServiceProvider.ts b/src/serviceProvider/serverServiceProvider.ts index 85e1c5cf..00c9cd29 100644 --- a/src/serviceProvider/serverServiceProvider.ts +++ b/src/serviceProvider/serverServiceProvider.ts @@ -2,6 +2,7 @@ import { Pimple, ServiceProvider } from '@timesplinter/pimple'; import * as http from 'http' import {Server} from "socket.io"; import ServiceMap from "../serviceMap.js"; +import EventEmitter from "events"; export default class ServerServiceProvider implements ServiceProvider { @@ -20,5 +21,9 @@ export default class ServerServiceProvider implements ServiceProvider { + return new EventEmitter(); + }) } }