Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use async node-hid #80

Merged
merged 9 commits into from
Dec 6, 2023
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 packages/node-record-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"@xkeys-lib/core": "3.0.1",
"readline": "^1.3.0",
"tslib": "^2.4.0",
"xkeys": "3.0.1"
"xkeys": "3.1.0-0"
},
"optionalDependencies": {
"find": "^0.3.0",
Expand Down
9 changes: 6 additions & 3 deletions packages/node/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "xkeys",
"version": "3.0.1",
"version": "3.1.0-0",
"description": "An npm module for interfacing with the X-keys panels in Node.js",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand All @@ -26,6 +26,10 @@
{
"name": "Andreas Reich",
"url": "https://github.com/cyraxx"
},
{
"name": "Julian Waller",
"url": "https://github.com/Julusian"
}
],
"scripts": {
Expand All @@ -50,9 +54,8 @@
"controller"
],
"dependencies": {
"@types/node-hid": "^1.3.1",
"@xkeys-lib/core": "3.0.1",
"node-hid": "^2.1.1",
"node-hid": "^3.0.0",
"tslib": "^2.4.0"
},
"optionalDependencies": {
Expand Down
65 changes: 33 additions & 32 deletions packages/node/src/__mocks__/node-hid.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,64 @@
import { EventEmitter } from 'events'
import type { Device } from 'node-hid'
import { XKEYS_VENDOR_ID } from '..'

export interface Device {
vendorId: number
productId: number
path?: string
serialNumber?: string
manufacturer?: string
product?: string
release: number
interface: number
usagePage?: number
usage?: number
}

let mockWriteHandler: undefined | ((hid: HID, message: number[]) => void) = undefined
export function setMockWriteHandler(handler: (hid: HID, message: number[]) => void) {
let mockWriteHandler: undefined | ((hid: HIDAsync, message: number[]) => void) = undefined
export function setMockWriteHandler(handler: (hid: HIDAsync, message: number[]) => void) {
mockWriteHandler = handler
}

// export class HID extends EventEmitter {
export class HID extends EventEmitter {
export class HIDAsync extends EventEmitter {
private mockWriteHandler

static async open(path: string): Promise<HIDAsync> {
return new HIDAsync(path)
}

constructor(_path: string) {
super()
this.mockWriteHandler = mockWriteHandler
}
// constructor(vid: number, pid: number);
close(): void {
// void
}
pause(): void {
async close(): Promise<void> {
// void
}
read(_callback: (err: any, data: number[]) => void): void {
async pause(): Promise<void> {
// void
}
readSync(): number[] {
return []
async read(_timeOut?: number): Promise<Buffer | undefined> {
return undefined
}
readTimeout(_timeOut: number): number[] {
return []
}
sendFeatureReport(_data: number[]): number {
async sendFeatureReport(_data: number[]): Promise<number> {
return 0
}
getFeatureReport(_reportIdd: number, _reportLength: number): number[] {
return []
async getFeatureReport(_reportIdd: number, _reportLength: number): Promise<Buffer> {
return Buffer.alloc(0)
}
resume(): void {
async resume(): Promise<void> {
// void
}
write(message: number[]): number {
async write(message: number[]): Promise<number> {
this.mockWriteHandler?.(this, message)
return 0
}
setNonBlocking(_noBlock: boolean): void {
async setNonBlocking(_noBlock: boolean): Promise<void> {
// void
}

async generateDeviceInfo(): Promise<Device> {
// HACK: For typings
return this.getDeviceInfo()
}

async getDeviceInfo(): Promise<Device> {
return {
vendorId: XKEYS_VENDOR_ID,
productId: 0,
release: 0,
interface: 0,
}
}
}
export function devices(): Device[] {
return []
Expand Down
2 changes: 1 addition & 1 deletion packages/node/src/__tests__/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export function getSentData() {
return sentData
}

export function handleXkeysMessages(hid: HID.HID, message: number[]) {
export function handleXkeysMessages(hid: HID.HIDAsync, message: number[]) {
// Replies to a few of the messages that are sent to the XKeys

sentData.push(Buffer.from(message).toString('hex'))
Expand Down
11 changes: 1 addition & 10 deletions packages/node/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,11 @@ import type * as HID from 'node-hid'
* This file contains internal convenience functions
*/

export function isHID_Device(device: HID.Device | HID.HID | string): device is HID.Device {
export function isHID_Device(device: HID.Device | HID.HID | HID.HIDAsync | string): device is HID.Device {
return (
typeof device === 'object' &&
(device as HID.Device).vendorId !== undefined &&
(device as HID.Device).productId !== undefined &&
(device as HID.Device).interface !== undefined
)
}
type HID_HID = HID.HID & { devicePath: string }
export function isHID_HID(device: HID.Device | HID.HID | string): device is HID_HID {
return (
typeof device === 'object' &&
(device as HID_HID).write !== undefined &&
(device as HID_HID).getFeatureReport !== undefined &&
(device as HID_HID).devicePath !== undefined // yes, HID.HID exposes this, we're using that
)
}
152 changes: 89 additions & 63 deletions packages/node/src/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@ import { PRODUCTS } from '@xkeys-lib/core'
import * as HID from 'node-hid'
import { NodeHIDDevice } from './node-hid-wrapper'

import { isHID_Device, isHID_HID } from './lib'
import { isHID_Device } from './lib'

import { HID_Device } from './api'

/** Sets up a connection to a HID device (the X-keys panel) */
/**
* Sets up a connection to a HID device (the X-keys panel)
*
* If called without arguments, it will select any connected X-keys panel.
*/
export function setupXkeysPanel(): Promise<XKeys>
export function setupXkeysPanel(HIDDevice: HID.Device): Promise<XKeys>
export function setupXkeysPanel(HIDDevice: HID.HID): Promise<XKeys>
export function setupXkeysPanel(HIDAsync: HID.HIDAsync): Promise<XKeys>
export function setupXkeysPanel(devicePath: string): Promise<XKeys>
export async function setupXkeysPanel(devicePathOrHIDDevice?: HID.Device | HID.HID | string): Promise<XKeys> {
export async function setupXkeysPanel(
devicePathOrHIDDevice?: HID.Device | HID.HID | HID.HIDAsync | string
): Promise<XKeys> {
let devicePath: string
let device: HID.HID
let device: HID.HIDAsync | undefined
let deviceInfo:
| {
product: string | undefined
Expand All @@ -23,76 +29,96 @@ export async function setupXkeysPanel(devicePathOrHIDDevice?: HID.Device | HID.H
}
| undefined

if (!devicePathOrHIDDevice) {
// Device not provided, will then select any connected device:
const connectedXkeys = listAllConnectedPanels()
if (!connectedXkeys.length) {
throw new Error('Could not find any connected X-keys panels.')
}
// Just select the first one:
devicePath = connectedXkeys[0].path
device = new HID.HID(devicePath)

deviceInfo = {
product: connectedXkeys[0].product,
productId: connectedXkeys[0].productId,
interface: connectedXkeys[0].interface,
}
} else if (isHID_Device(devicePathOrHIDDevice)) {
// is HID.Device
try {
if (!devicePathOrHIDDevice) {
// Device not provided, will then select any connected device:
const connectedXkeys = listAllConnectedPanels()
if (!connectedXkeys.length) {
throw new Error('Could not find any connected X-keys panels.')
}
// Just select the first one:
devicePath = connectedXkeys[0].path
device = await HID.HIDAsync.open(devicePath)

deviceInfo = {
product: connectedXkeys[0].product,
productId: connectedXkeys[0].productId,
interface: connectedXkeys[0].interface,
}
} else if (isHID_Device(devicePathOrHIDDevice)) {
// is HID.Device

if (!devicePathOrHIDDevice.path) throw new Error('HID.Device path not set!')
if (!devicePathOrHIDDevice.path) throw new Error('HID.Device path not set!')

devicePath = devicePathOrHIDDevice.path
device = new HID.HID(devicePath)
devicePath = devicePathOrHIDDevice.path
device = await HID.HIDAsync.open(devicePath)

deviceInfo = {
product: devicePathOrHIDDevice.product,
productId: devicePathOrHIDDevice.productId,
interface: devicePathOrHIDDevice.interface,
deviceInfo = {
product: devicePathOrHIDDevice.product,
productId: devicePathOrHIDDevice.productId,
interface: devicePathOrHIDDevice.interface,
}
} else if (typeof devicePathOrHIDDevice === 'string') {
// is string (path)

devicePath = devicePathOrHIDDevice
device = await HID.HIDAsync.open(devicePath)
// deviceInfo is set later
} else if (devicePathOrHIDDevice instanceof HID.HID) {
// Can't use this, since devicePath is missing
throw new Error(
'HID.HID not supported as argument to setupXkeysPanel, use HID.devices() to find the device and provide that instead.'
)
} else if (devicePathOrHIDDevice instanceof HID.HIDAsync) {
// @ts-expect-error getDeviceInfo missing in typings
const dInfo = await devicePathOrHIDDevice.getDeviceInfo()

if (!dInfo.path)
throw new Error(
// Can't use this, we need a path to the device
'HID.HIDAsync device did not provide a path, so its not supported as argument to setupXkeysPanel, use HID.devicesAsync() to find the device and provide that instead.'
)

devicePath = dInfo.path
device = devicePathOrHIDDevice

deviceInfo = {
product: dInfo.product,
productId: dInfo.productId,
interface: dInfo.interface,
}
} else {
throw new Error('setupXkeysPanel: invalid arguments')
}
} else if (isHID_HID(devicePathOrHIDDevice)) {
// is HID.HID

device = devicePathOrHIDDevice
devicePath = devicePathOrHIDDevice.devicePath
// deviceInfo is set later
} else if (typeof devicePathOrHIDDevice === 'string') {
// is string (path)

devicePath = devicePathOrHIDDevice
device = new HID.HID(devicePath)
// deviceInfo is set later
} else {
throw new Error('setupXkeysPanel: invalid arguments')
}

if (!deviceInfo) {
// Look through HID.devices(), bevause HID.Device contains the productId
for (const hidDevice of HID.devices()) {
if (hidDevice.path === devicePath) {
deviceInfo = {
product: hidDevice.product,
productId: hidDevice.productId,
interface: hidDevice.interface,
}
break
if (!deviceInfo) {
// @ts-expect-error getDeviceInfo missing in typings
const nodeHidInfo: HID.Device = await device.getDeviceInfo()
// Look through HID.devices(), because HID.Device contains the productId
deviceInfo = {
product: nodeHidInfo.product,
productId: nodeHidInfo.productId,
interface: nodeHidInfo.interface,
}
}
}

if (!device) throw new Error('Error setting up X-keys: device not found')
if (!devicePath) throw new Error('Error setting up X-keys: devicePath not found')
if (!deviceInfo) throw new Error('Error setting up X-keys: deviceInfo not found')
if (!device) throw new Error('Error setting up X-keys: device not found')
if (!devicePath) throw new Error('Error setting up X-keys: devicePath not found')
if (!deviceInfo) throw new Error('Error setting up X-keys: deviceInfo not found')

const deviceWrap = new NodeHIDDevice(device)
const deviceWrap = new NodeHIDDevice(device)

const xkeys = new XKeys(deviceWrap, deviceInfo, devicePath)
const xkeys = new XKeys(deviceWrap, deviceInfo, devicePath)

// Wait for the device to initialize:
await xkeys.init()
// Wait for the device to initialize:
await xkeys.init()

return xkeys
return xkeys
} catch (e) {
if (device) await device.close().catch(() => null) // Suppress error

throw e
}
}
/** Returns a list of all connected X-keys-HID-devices */
export function listAllConnectedPanels(): HID_Device[] {
Expand Down
8 changes: 5 additions & 3 deletions packages/node/src/node-hid-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as HID from 'node-hid'
* This translates it into the common format (@see HIDDevice) defined by @xkeys-lib/core
*/
export class NodeHIDDevice extends EventEmitter implements HIDDevice {
constructor(private device: HID.HID) {
constructor(private device: HID.HIDAsync) {
super()
this._handleData = this._handleData.bind(this)
this._handleError = this._handleError.bind(this)
Expand All @@ -18,11 +18,13 @@ export class NodeHIDDevice extends EventEmitter implements HIDDevice {
}

public write(data: number[]): void {
this.device.write(data)
this.device.write(data).catch((err) => {
this.emit('error', err)
})
Comment on lines +21 to +23
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a question: Is it ok to write like this? ie we don't have to worry about concurrency or add some promise queue here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm actually yeah, this does need to be put in a queue.
close might need to be put in the same queue too.

}

public async close(): Promise<void> {
this.device.close()
await this.device.close()

// For some unknown reason, we need to wait a bit before returning because it
// appears that the HID-device isn't actually closed properly until after a short while.
Expand Down
Loading