Skip to content

Commit

Permalink
Merge branch 'feat/flush'
Browse files Browse the repository at this point in the history
  • Loading branch information
nytamin committed Dec 9, 2024
2 parents 55b93c3 + f71bd32 commit 4f96cab
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 5 deletions.
3 changes: 3 additions & 0 deletions packages/core/src/genericHIDDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ export interface HIDDevice {

write(data: number[]): void

/** Returns a promise which settles when all writes has completed */
flush(): Promise<void>

close(): Promise<void>
}
6 changes: 6 additions & 0 deletions packages/core/src/xkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,12 @@ export class XKeys extends EventEmitter {
public writeData(message: HIDMessage): void {
this._write(message)
}
/**
* Returns a Promise that settles when all writes have been completed
*/
public async flush(): Promise<void> {
await this.device.flush()
}

/** (Internal function) Called when there has been detected that the device has been disconnected */
public async _handleDeviceDisconnected(): Promise<void> {
Expand Down
1 change: 1 addition & 0 deletions packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"dependencies": {
"@xkeys-lib/core": "3.2.0",
"node-hid": "^3.0.0",
"p-queue": "^6.6.2",
"tslib": "^2.4.0"
},
"optionalDependencies": {
Expand Down
5 changes: 4 additions & 1 deletion packages/node/src/__mocks__/node-hid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ let mockWriteHandler: undefined | ((hid: HIDAsync, message: number[]) => void) =
export function setMockWriteHandler(handler: (hid: HIDAsync, message: number[]) => void) {
mockWriteHandler = handler
}
export function resetMockWriteHandler() {
mockWriteHandler = undefined
}
let mockDevices: Device[] = []
export function mockSetDevices(devices: Device[]) {
mockDevices = devices
Expand Down Expand Up @@ -58,7 +61,7 @@ export class HIDAsync extends EventEmitter {
throw new Error('Mock not implemented.')
}
async write(message: number[]): Promise<number> {
this.mockWriteHandler?.(this, message)
await this.mockWriteHandler?.(this, message)
return 0
}
async setNonBlocking(_noBlock: boolean): Promise<void> {
Expand Down
5 changes: 5 additions & 0 deletions packages/node/src/__tests__/recordings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ describe('Recorded tests', () => {
expect(HID.setMockWriteHandler).toBeTruthy()
})
beforeEach(() => {})
afterEach(() => {
HIDMock.resetMockWriteHandler()
})

const dirPath = './src/__tests__/recordings/'

Expand Down Expand Up @@ -133,6 +136,8 @@ describe('Recorded tests', () => {
// @ts-expect-error hack
xkeysDevice[action.method](...action.arguments)

await xkeysDevice.flush()

expect(getSentData()).toEqual(action.sentData)
resetSentData()
} catch (err) {
Expand Down
3 changes: 3 additions & 0 deletions packages/node/src/__tests__/watcher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { NodeHIDDevice, XKeys, XKeysWatcher } from '..'
import { handleXkeysMessages, sleep, sleepTicks } from './lib'

describe('XKeysWatcher', () => {
afterEach(() => {
HIDMock.resetMockWriteHandler()
})
test('Detect device (w polling)', async () => {
const POLL_INTERVAL = 10
NodeHIDDevice.CLOSE_WAIT_TIME = 0 // We can override this to speed up the unit tests
Expand Down
127 changes: 126 additions & 1 deletion packages/node/src/__tests__/xkeys.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as HID from 'node-hid'
import * as HIDMock from '../__mocks__/node-hid'
import { setupXkeysPanel, XKeys } from '../'
import { getSentData, handleXkeysMessages, resetSentData } from './lib'
import { getSentData, handleXkeysMessages, resetSentData, sleep } from './lib'

describe('Unit tests', () => {
afterEach(() => {
HIDMock.resetMockWriteHandler()
})
test('calculateDelta', () => {
expect(XKeys.calculateDelta(100, 100)).toBe(0)
expect(XKeys.calculateDelta(110, 100)).toBe(10)
Expand Down Expand Up @@ -40,101 +43,223 @@ describe('Unit tests', () => {
expect(myXkeysPanel.info).toMatchSnapshot()
resetSentData()
myXkeysPanel.getButtons()
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setIndicatorLED(5, true)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setIndicatorLED(5, false)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()

myXkeysPanel.setIndicatorLED(5, true, true)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()

myXkeysPanel.setBacklight(5, '59f')
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setBacklight(5, '5599ff')
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setBacklight(5, '#5599ff')
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setBacklight(5, { r: 45, g: 210, b: 255 })
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setBacklight(5, true)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setBacklight(5, false)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setBacklight(5, null)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setBacklight(5, null)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setBacklight(5, '59f', true)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()

myXkeysPanel.setAllBacklights('59f')
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setAllBacklights('5599ff')
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setAllBacklights('#5599ff')
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setAllBacklights({ r: 45, g: 210, b: 255 })
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setAllBacklights(true)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setAllBacklights(false)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setAllBacklights(null)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setAllBacklights(null)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()

myXkeysPanel.toggleAllBacklights()
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setBacklightIntensity(100)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setBacklightIntensity(0, 255)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.saveBackLights()
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()

myXkeysPanel.setFrequency(127)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.setUnitId(42)
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
myXkeysPanel.rebootDevice()
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()
// expect(myXkeysPanel.writeLcdDisplay(line: number, displayChar: string, backlight: boolean)
await myXkeysPanel.flush()
// expect(getSentData()).toMatchSnapshot()
// resetSentData()

myXkeysPanel.writeData([0, 1, 2, 3, 4])
await myXkeysPanel.flush()
expect(getSentData()).toMatchSnapshot()
resetSentData()

expect(onError).toHaveBeenCalledTimes(0)
})
test('flush()', async () => {
const hidDevice = {
vendorId: XKeys.vendorId,
productId: 1029,
interface: 0,
path: 'mockPath',
} as HID.Device

const mockWriteStart = jest.fn()
const mockWriteEnd = jest.fn()
HIDMock.setMockWriteHandler(async (hid, message) => {
mockWriteStart()
await sleep(10)
mockWriteEnd()
handleXkeysMessages(hid, message)
})

const myXkeysPanel = await setupXkeysPanel(hidDevice)

const errorListener = jest.fn(console.error)
myXkeysPanel.on('error', errorListener)

mockWriteStart.mockClear()
mockWriteEnd.mockClear()

myXkeysPanel.toggleAllBacklights()

expect(mockWriteStart).toBeCalledTimes(1)
expect(mockWriteEnd).toBeCalledTimes(0) // Should not have been called yet

// cleanup:
await myXkeysPanel.flush() // waits for all writes to finish

expect(mockWriteEnd).toBeCalledTimes(1)

await myXkeysPanel.close() // close the device.
myXkeysPanel.off('error', errorListener)

expect(errorListener).toHaveBeenCalledTimes(0)
})
test('flush() with error', async () => {
const hidDevice = {
vendorId: XKeys.vendorId,
productId: 1029,
interface: 0,
path: 'mockPath',
} as HID.Device

const mockWriteStart = jest.fn()
const mockWriteEnd = jest.fn()
HIDMock.setMockWriteHandler(async (hid, message) => {
mockWriteStart()
await sleep(10)
mockWriteEnd()
// console.log('message', message)

if (message[0] === 0 && message[1] === 184) {
// toggleAllBacklights
throw new Error('Mock error')
}

handleXkeysMessages(hid, message)
})

const myXkeysPanel = await setupXkeysPanel(hidDevice)

const errorListener = jest.fn((e) => {
if (`${e}`.includes('Mock error')) return // ignore
console.error(e)
})
myXkeysPanel.on('error', errorListener)

mockWriteStart.mockClear()
mockWriteEnd.mockClear()

myXkeysPanel.toggleAllBacklights()

expect(mockWriteStart).toBeCalledTimes(1)
expect(errorListener).toBeCalledTimes(0) // Should not have been called yet

// cleanup:
await myXkeysPanel.flush() // waits for all writes to finish

expect(errorListener).toBeCalledTimes(1)
errorListener.mockClear()

await myXkeysPanel.close() // close the device.
myXkeysPanel.off('error', errorListener)

expect(errorListener).toHaveBeenCalledTimes(0)
})
})
14 changes: 11 additions & 3 deletions packages/node/src/node-hid-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/unbound-method */
import { HIDDevice } from '@xkeys-lib/core'
import { EventEmitter } from 'events'
import Queue from 'p-queue'
import * as HID from 'node-hid'

/**
Expand All @@ -10,6 +11,8 @@ import * as HID from 'node-hid'
export class NodeHIDDevice extends EventEmitter implements HIDDevice {
static CLOSE_WAIT_TIME = 300

private readonly writeQueue = new Queue({ concurrency: 1 })

constructor(private device: HID.HIDAsync) {
super()

Expand All @@ -18,9 +21,11 @@ export class NodeHIDDevice extends EventEmitter implements HIDDevice {
}

public write(data: number[]): void {
this.device.write(data).catch((err) => {
this.emit('error', err)
})
this.writeQueue
.add(async () => this.device.write(data))
.catch((err) => {
this.emit('error', err)
})
}

public async close(): Promise<void> {
Expand All @@ -34,6 +39,9 @@ export class NodeHIDDevice extends EventEmitter implements HIDDevice {
this.device.removeListener('error', this._handleError)
this.device.removeListener('data', this._handleData)
}
public async flush(): Promise<void> {
await this.writeQueue.onIdle()
}

private _handleData = (data: Buffer) => {
this.emit('data', data)
Expand Down
3 changes: 3 additions & 0 deletions packages/webhid/src/web-hid-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export class WebHIDDevice extends EventEmitter implements CoreHIDDevice {
this.emit('error', err)
})
}
public async flush(): Promise<void> {
await this.reportQueue.onIdle()
}

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

0 comments on commit 4f96cab

Please sign in to comment.