Skip to content

Commit

Permalink
feat: use node-hid 3.0.0 (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
Julusian authored Dec 1, 2023
1 parent 27998bd commit 6ce9a0f
Show file tree
Hide file tree
Showing 4 changed files with 556 additions and 473 deletions.
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@
"node": "^18.15"
},
"dependencies": {
"@elgato-stream-deck/node": "^5.7.3",
"@elgato-stream-deck/node": "^6.0.0",
"@julusian/image-rs": "^0.2.1",
"@julusian/jpeg-turbo": "^2.1.0",
"@julusian/skia-canvas": "^1.0.5",
"@loupedeck/node": "^1.0.0",
"@xencelabs-quick-keys/node": "^0.4.0",
"@xencelabs-quick-keys/node": "^1.0.0",
"electron-about-window": "^1.15.2",
"electron-prompt": "^1.7.0",
"electron-store": "^8.1.0",
Expand All @@ -69,13 +69,13 @@
"koa-body": "^6.0.1",
"koa-router": "^12.0.1",
"meow": "^9.0.0",
"node-hid": "npm:@julusian/hid@2.5.0-3",
"node-hid": "^3.0.0",
"semver": "^7.5.4",
"tslib": "^2.6.2",
"usb": "^2.11.0"
},
"resolutions": {
"node-hid": "npm:@julusian/hid@2.5.0-3"
"node-hid": "^3.0.0"
},
"prettier": "@sofie-automation/code-standard-preset/.prettierrc.json",
"lint-staged": {
Expand Down
26 changes: 26 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteCli
private _supportsCombinedEncoders = false
private _supportsBitmapResolution = false

private _registeredDevices = new Set<string>()
private _pendingDevices = new Map<string, number>() // Time submitted

public forceSplitEncoders = false

public get host(): string {
Expand Down Expand Up @@ -141,6 +144,9 @@ export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteCli
this.emit('log', 'Connection closed')
}

this._registeredDevices.clear()
this._pendingDevices.clear()

if (this._connected) {
this.emit('disconnected')
}
Expand Down Expand Up @@ -168,6 +174,9 @@ export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteCli
this.emit('log', 'Connected')
}

this._registeredDevices.clear()
this._pendingDevices.clear()

this._connected = true
this._pingUnackedCount = 0
this.receiveBuffer = ''
Expand Down Expand Up @@ -392,6 +401,9 @@ export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteCli
return
}

this._registeredDevices.add(params.DEVICEID)
this._pendingDevices.delete(params.DEVICEID)

this.emit('newDevice', { deviceId: params.DEVICEID })
}

Expand All @@ -417,7 +429,18 @@ export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteCli
}

public addDevice(deviceId: string, productName: string, props: DeviceRegisterProps): void {
if (this._registeredDevices.has(deviceId)) {
throw new Error('Device is already registered')
}

const pendingTime = this._pendingDevices.get(deviceId)
if (pendingTime && pendingTime < Date.now() - 10000) {
throw new Error('Device is already being added')
}

if (this._connected && this.socket) {
this._pendingDevices.set(deviceId, Date.now())

this.socket.write(
`ADD-DEVICE DEVICEID=${deviceId} PRODUCT_NAME="${productName}" KEYS_TOTAL=${
props.keysTotal
Expand All @@ -430,6 +453,9 @@ export class CompanionSatelliteClient extends EventEmitter<CompanionSatelliteCli

public removeDevice(deviceId: string): void {
if (this._connected && this.socket) {
this._registeredDevices.delete(deviceId)
this._pendingDevices.delete(deviceId)

this.socket.write(`REMOVE-DEVICE DEVICEID=${deviceId}\n`)
}
}
Expand Down
178 changes: 113 additions & 65 deletions src/devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,18 @@ HID.devices()

export class DeviceManager {
private readonly devices: Map<DeviceId, WrappedDevice>
private readonly pendingDevices: Set<DeviceId>
private readonly client: CompanionSatelliteClient
private readonly cardGenerator: CardGenerator

private statusString: string
private scanIsRunning = false
private scanPending = false

constructor(client: CompanionSatelliteClient) {
this.client = client
this.devices = new Map()
this.pendingDevices = new Set()
this.cardGenerator = new CardGenerator()

usb.on('attach', (dev) => {
Expand Down Expand Up @@ -178,6 +182,8 @@ export class DeviceManager {
setTimeout(() => {
const dev = this.devices.get(deviceId)
if (dev) {
console.log('try add', deviceId)

// Make sure device knows what the client is capable of
dev.updateCapabilities(this.client.capabilities)

Expand Down Expand Up @@ -233,8 +239,8 @@ export class DeviceManager {
public syncCapabilitiesAndRegisterAllDevices(): void {
console.log('registerAll', Array.from(this.devices.keys()))
for (const device of this.devices.values()) {
// If it is already in the process of initialising, core will give us back the same id twice, so we dont need to track it
// if (!devices2.find((d) => d[1] === serial)) { // TODO - do something here?
// If it is still in the process of initialising skip it
if (this.pendingDevices.has(device.deviceId)) continue

// Indicate on device
device.showStatus(this.client.host, this.statusString)
Expand All @@ -244,63 +250,84 @@ export class DeviceManager {

// Re-init device
this.client.addDevice(device.deviceId, device.productName, device.getRegisterProps())

// }
}

this.scanDevices()
}

public scanDevices(): void {
const devices = HID.devices()
for (const device of devices) {
const sdInfo = getStreamDeckDeviceInfo(device)
if (sdInfo && sdInfo.serialNumber) {
this.tryAddStreamdeck(sdInfo.path, sdInfo.serialNumber)
} else if (
device.path &&
device.serialNumber &&
device.vendorId === Infinitton.VENDOR_ID &&
Infinitton.PRODUCT_IDS.includes(device.productId)
) {
this.tryAddInfinitton(device.path, device.serialNumber)
}
if (this.scanIsRunning) {
this.scanPending = true
return
}

XencelabsQuickKeysManagerInstance.openDevicesFromArray(devices).catch((e) => {
console.error(`Quick keys scan failed: ${e}`)
})
this.scanIsRunning = true
this.scanPending = false

Promise.allSettled([
HID.devicesAsync()
.then(async (devices) => {
for (const device of devices) {
const sdInfo = getStreamDeckDeviceInfo(device)
if (sdInfo && sdInfo.serialNumber) {
this.tryAddStreamdeck(sdInfo.path, sdInfo.serialNumber)
} else if (
device.path &&
device.serialNumber &&
device.vendorId === Infinitton.VENDOR_ID &&
Infinitton.PRODUCT_IDS.includes(device.productId)
) {
this.tryAddInfinitton(device.path, device.serialNumber)
}
}

listLoupedecks()
.then((devs) => {
for (const dev of devs) {
if (
dev.serialNumber &&
(dev.model === LoupedeckModelId.LoupedeckLive ||
dev.model === LoupedeckModelId.RazerStreamController)
) {
this.tryAddLoupedeck(dev.path, dev.serialNumber, LoupedeckLiveWrapper)
} else if (dev.serialNumber && dev.model === LoupedeckModelId.LoupedeckLiveS) {
this.tryAddLoupedeck(dev.path, dev.serialNumber, LoupedeckLiveSWrapper)
} else if (dev.serialNumber && dev.model === LoupedeckModelId.RazerStreamControllerX) {
this.tryAddLoupedeck(dev.path, dev.serialNumber, RazerStreamControllerXWrapper)
await XencelabsQuickKeysManagerInstance.openDevicesFromArray(devices)
})
.catch((e) => {
console.error(`HID scan failed: ${e}`)
}),
listLoupedecks()
.then(async (devs) => {
for (const dev of devs) {
if (
dev.serialNumber &&
(dev.model === LoupedeckModelId.LoupedeckLive ||
dev.model === LoupedeckModelId.RazerStreamController)
) {
this.tryAddLoupedeck(dev.path, dev.serialNumber, LoupedeckLiveWrapper)
} else if (dev.serialNumber && dev.model === LoupedeckModelId.LoupedeckLiveS) {
this.tryAddLoupedeck(dev.path, dev.serialNumber, LoupedeckLiveSWrapper)
} else if (dev.serialNumber && dev.model === LoupedeckModelId.RazerStreamControllerX) {
this.tryAddLoupedeck(dev.path, dev.serialNumber, RazerStreamControllerXWrapper)
}
}
}
})
.catch((e) => {
console.error(`Loupedeck scan failed: ${e}`)
})
})
.catch((e) => {
console.error(`Loupedeck scan failed: ${e}`)
}),
]).finally(() => {
this.scanIsRunning = false

if (this.scanPending) {
this.scanDevices()
}
})
}

private canAddDevice(deviceId: string): boolean {
return !this.pendingDevices.has(deviceId) && !this.devices.has(deviceId)
}

private tryAddLoupedeck(
path: string,
serial: string,
wrapperClass: new (deviceId: string, device: LoupedeckDevice, cardGenerator: CardGenerator) => WrappedDevice
) {
if (!this.devices.has(serial)) {
if (this.canAddDevice(serial)) {
console.log(`adding new device: ${path}`)
console.log(`existing = ${JSON.stringify(Array.from(this.devices.keys()))}`)

this.pendingDevices.add(serial)
openLoupedeck(path)
.then(async (ld) => {
try {
Expand All @@ -319,29 +346,39 @@ export class DeviceManager {
.catch((e) => {
console.log(`Open "${path}" failed: ${e}`)
})
.finally(() => {
this.pendingDevices.delete(serial)
})
}
}

private tryAddStreamdeck(path: string, serial: string) {
try {
if (!this.devices.has(serial)) {
console.log(`adding new device: ${path}`)
console.log(`existing = ${JSON.stringify(Array.from(this.devices.keys()))}`)
if (this.canAddDevice(serial)) {
console.log(`adding new device: ${path}`)
console.log(`existing = ${JSON.stringify(Array.from(this.devices.keys()))}`)

const sd = openStreamDeck(path)
sd.on('error', (e) => {
console.error('device error', e)
this.cleanupDeviceById(serial)
})
this.pendingDevices.add(serial)
openStreamDeck(path)
.then(async (sd) => {
try {
sd.on('error', (e) => {
console.error('device error', e)
this.cleanupDeviceById(serial)
})

const devInfo = new StreamDeckWrapper(serial, sd, this.cardGenerator)
this.tryAddDeviceInner(serial, devInfo).catch((e) => {
const devInfo = new StreamDeckWrapper(serial, sd, this.cardGenerator)
await this.tryAddDeviceInner(serial, devInfo)
} catch (e) {
console.log(`Open "${path}" failed: ${e}`)
sd.close().catch(() => null)
}
})
.catch((e) => {
console.log(`Open "${path}" failed: ${e}`)
sd.close().catch(() => null)
})
}
} catch (e) {
console.log(`Open "${path}" failed: ${e}`)
.finally(() => {
this.pendingDevices.delete(serial)
})
}
}

Expand All @@ -355,49 +392,60 @@ export class DeviceManager {
// return val2
// }

private tryAddQuickKeys(surface: XencelabsQuickKeys) {
private tryAddQuickKeys(surface: XencelabsQuickKeys): void {
// TODO - support no deviceId for wired devices
if (!surface.deviceId) return

try {
const deviceId = surface.deviceId
if (!this.devices.has(deviceId)) {
if (this.canAddDevice(deviceId)) {
console.log(`adding new device: ${deviceId}`)
console.log(`existing = ${JSON.stringify(Array.from(this.devices.keys()))}`)

this.pendingDevices.add(deviceId)

// TODO - this is race prone..
surface.on('error', (e) => {
console.error('device error', e)
this.cleanupDeviceById(deviceId)
})

const devInfo = new QuickKeysWrapper(deviceId, surface)
this.tryAddDeviceInner(deviceId, devInfo).catch((e) => {
console.log(`Open "${surface.deviceId}" failed: ${e}`)
})
this.tryAddDeviceInner(deviceId, devInfo)
.catch((e) => {
console.log(`Open "${surface.deviceId}" failed: ${e}`)
})
.finally(() => {
this.pendingDevices.delete(deviceId)
})
}
} catch (e) {
console.log(`Open "${surface.deviceId}" failed: ${e}`)
}
}

private tryAddInfinitton(path: string, serial: string) {
private tryAddInfinitton(path: string, serial: string): void {
try {
if (!this.devices.has(serial)) {
if (this.canAddDevice(serial)) {
console.log(`adding new device: ${path}`)
console.log(`existing = ${JSON.stringify(Array.from(this.devices.keys()))}`)

this.pendingDevices.add(serial)
const panel = new Infinitton(path)
panel.on('error', (e) => {
console.error('device error', e)
this.cleanupDeviceById(serial)
})

const devInfo = new InfinittonWrapper(serial, panel, this.cardGenerator)
this.tryAddDeviceInner(serial, devInfo).catch((e) => {
console.log(`Open "${path}" failed: ${e}`)
panel.close()
})
this.tryAddDeviceInner(serial, devInfo)
.catch((e) => {
console.log(`Open "${path}" failed: ${e}`)
panel.close()
})
.finally(() => {
this.pendingDevices.delete(serial)
})
}
} catch (e) {
console.log(`Open "${path}" failed: ${e}`)
Expand Down
Loading

0 comments on commit 6ce9a0f

Please sign in to comment.