Skip to content

Commit

Permalink
Merge pull request #80 from Julusian/feat/node-hid-async
Browse files Browse the repository at this point in the history
feat: use async node-hid
  • Loading branch information
nytamin authored Dec 6, 2023
2 parents bcfb831 + 190d4a1 commit 0358da8
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 264 deletions.
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)
})
}

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

0 comments on commit 0358da8

Please sign in to comment.