From f7c3a869e8820f856831aad576ce7978dfb9d75c Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 24 May 2021 21:10:53 +0200 Subject: [PATCH] feat: add feature: "Automatic UnitId mode" --- packages/core/src/api.ts | 1 + packages/core/src/xkeys.ts | 58 +++++++++++++++++++++++++----------- packages/node/src/watcher.ts | 57 ++++++++++++++++++++++++++++++++--- 3 files changed, 94 insertions(+), 22 deletions(-) diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index 4f3e219..949bbe1 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -62,6 +62,7 @@ export interface XKeysEvents { tbar: (index: number, value: number, eventMetadata: EventMetadata) => void disconnected: () => void + reconnected: () => void error: (err: any) => void } export interface XKeysInfo { diff --git a/packages/core/src/xkeys.ts b/packages/core/src/xkeys.ts index 16aa4f7..d5f40f8 100644 --- a/packages/core/src/xkeys.ts +++ b/packages/core/src/xkeys.ts @@ -19,7 +19,7 @@ export declare interface XKeys { } export class XKeys extends EventEmitter { - private product: Product + private product: Product & { productId: number; interface: number } /** All button states */ private _buttonStates: ButtonStates = new Map() @@ -35,7 +35,6 @@ export class XKeys extends EventEmitter { private receivedGenerateDataResolve?: () => void private _initialized = false - private _hidDevice: { productId: number; interface: number } private _unidId = 0 // is set after init() private _firmwareVersion = 0 // is set after init() private _disconnected = false @@ -48,14 +47,13 @@ export class XKeys extends EventEmitter { constructor( // public readonly devicePath: string, private device: HIDDevice, - deviceInfo: { - product: string | undefined - productId: number - interface: number | null // null means "anything goes", used when interface isn't available - } + private deviceInfo: DeviceInfo ) { super() + this.product = this._setupDevice(deviceInfo) + } + private _setupDevice(deviceInfo: DeviceInfo) { const findProdct = (): { product: Product; productId: number; interface: number } => { for (const product of Object.values(PRODUCTS)) { for (const hidDevice of product.hidDevices) { @@ -77,12 +75,6 @@ export class XKeys extends EventEmitter { ) } const found = findProdct() - this.product = found.product - - this._hidDevice = { - productId: found.productId, - interface: found.interface, - } this.device.on('data', (data: Buffer) => { if (data.readUInt8(1) === 214) { @@ -247,13 +239,19 @@ export class XKeys extends EventEmitter { this.device.on('error', (err) => { if ((err + '').match(/could not read from/)) { // The device has been disconnected - this.handleDeviceDisconnected().catch((error) => { + this._handleDeviceDisconnected().catch((error) => { this.emit('error', error) }) } else { this.emit('error', err) } }) + + return { + ...found.product, + productId: found.productId, + interface: found.interface, + } } /** Initialize the device. This ensures that the essential information from the device about its state has been received. */ @@ -275,7 +273,7 @@ export class XKeys extends EventEmitter { } /** Closes the device. Subsequent commands will raise errors. */ public async close(): Promise { - await this.handleDeviceDisconnected() + await this._handleDeviceDisconnected() } /** Firmware version of the device */ @@ -294,8 +292,8 @@ export class XKeys extends EventEmitter { return literal({ name: this.product.name, - productId: this._hidDevice.productId, - interface: this._hidDevice.interface, + productId: this.product.productId, + interface: this.product.interface, unitId: this.unitId, firmwareVersion: this._firmwareVersion, // added this imporant to defend against older firmware bugs @@ -525,13 +523,32 @@ export class XKeys extends EventEmitter { } /** (Internal function) Called when there has been detected that the device has been disconnected */ - public async handleDeviceDisconnected(): Promise { + public async _handleDeviceDisconnected(): Promise { if (!this._disconnected) { this._disconnected = true await this.device.close() this.emit('disconnected') } } + /** (Internal function) Called when there has been detected that a device has been reconnected */ + public async _handleDeviceReconnected(device: HIDDevice, deviceInfo: DeviceInfo): Promise { + if (this._disconnected) { + this._disconnected = false + + // Re-vitalize: + this.device = device + this.product = this._setupDevice(deviceInfo) + await this.init() + + this.emit('reconnected') + } + } + public _getHIDDevice(): HIDDevice { + return this.device + } + public _getDeviceInfo(): DeviceInfo { + return this.deviceInfo + } /** * Writes a Buffer to the X-keys device * @@ -658,3 +675,8 @@ export class XKeys extends EventEmitter { } } type HIDMessage = (string | number)[] +interface DeviceInfo { + product: string | undefined + productId: number + interface: number | null // null means "anything goes", used when interface isn't available +} diff --git a/packages/node/src/watcher.ts b/packages/node/src/watcher.ts index c8a814f..bbc4f27 100644 --- a/packages/node/src/watcher.ts +++ b/packages/node/src/watcher.ts @@ -67,8 +67,11 @@ export class XKeysWatcher extends EventEmitter { public debug = false /** A list of the devices we've called setupXkeysPanels for */ private setupXkeysPanels: XKeys[] = [] + private prevConnectedIdentifiers: { [key: string]: XKeys } = {} + /** Unique unitIds grouped into productId groups. */ + private uniqueIds = new Map() - constructor() { + constructor(private options?: XKeysWatcherOptions) { super() watcherCount++ @@ -206,8 +209,33 @@ export class XKeysWatcher extends EventEmitter { // Store for future reference: this.seenDevicePaths[devicePath].xkeys = xkeysPanel - // Emit to the consumer: - this.emit('connected', xkeysPanel) + if (this.options?.automaticUnitIdMode) { + if (xkeysPanel.unitId === 0) { + // if it is 0, we assume that it's new from the factory and can be safely changed + xkeysPanel.setUnitId(this._getNextUniqueId(xkeysPanel)) // the lookup-cache is stored either in memory, or preferrably on disk + } + // the PID+UID pair is enough to uniquely identify a panel. + const uniqueIdentifier = `${xkeysPanel.info.productId}_${xkeysPanel.unitId}` + const previousXKeysPanel = this.prevConnectedIdentifiers[uniqueIdentifier] + if (previousXKeysPanel) { + // This panel has been connected before. + + // We want the XKeys-instance to emit a 'reconnected' event. + // This means that we kill off the newly created xkeysPanel, and + + previousXKeysPanel._handleDeviceReconnected( + xkeysPanel._getHIDDevice(), + xkeysPanel._getDeviceInfo() + ) + } else { + // It seems that this panel hasn't been connected before + this.emit('connected', xkeysPanel) + this.prevConnectedIdentifiers[uniqueIdentifier] = xkeysPanel + } + } else { + // Default behaviour: + this.emit('connected', xkeysPanel) + } } else { this.handleRemovedDevice(xkeysPanel) } @@ -216,10 +244,31 @@ export class XKeysWatcher extends EventEmitter { this.emit('error', err) }) } + private _getNextUniqueId(xkeysPanel: XKeys): number { + let nextId = this.uniqueIds.get(xkeysPanel.info.productId) + if (!nextId) { + nextId = 32 // Starting at 32 + } else { + nextId++ + } + if (nextId > 255) throw new Error('No more unique ids available!') + + this.uniqueIds.set(xkeysPanel.info.productId, nextId) + + return nextId + } private handleRemovedDevice(xkeysPanel: XKeys) { - xkeysPanel.handleDeviceDisconnected() + xkeysPanel._handleDeviceDisconnected() } private debugLog(...args: any[]) { if (this.debug) console.log(...args) } } +export interface XKeysWatcherOptions { + /** + * This activates the "Automatic UnitId mode", which enables several features: + * First, any x-keys panel with unitId===0 will be issued a (pseudo unique) unitId upon connection, in order for it to be uniquely identified. + * This allows for the connection-events to work a bit differently, mainly enabling the "reconnected"-event for when a panel has been disconnected, then reconnected again. + */ + automaticUnitIdMode: true +}