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

Add flush() method #115

Merged
merged 4 commits into from
Dec 9, 2024
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
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

Comment on lines +202 to +203
Copy link
Contributor

Choose a reason for hiding this comment

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

you could use jest fake timers instead, and advance the timers here by 10ms to make the test a bit more guaranteed

// 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)
})
})
16 changes: 13 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,13 @@ 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 () => {
await this.device.write(data)
})
Comment on lines +25 to +27
Copy link
Contributor

Choose a reason for hiding this comment

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

small nit: you could directly return the promise here

Suggested change
.add(async () => {
await this.device.write(data)
})
.add(() => this.device.write(data))

.catch((err) => {
this.emit('error', err)
})
}

public async close(): Promise<void> {
Expand All @@ -34,6 +41,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
Loading