Skip to content

Commit

Permalink
feat: implement remaining api
Browse files Browse the repository at this point in the history
  • Loading branch information
Julusian committed Nov 5, 2021
1 parent 4847aac commit 29f166c
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 100 deletions.
118 changes: 100 additions & 18 deletions packages/core/src/device.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as EventEmitter from 'eventemitter3'
import { WheelEvent } from '.'

import { HIDDevice } from './hid'
import {
Expand All @@ -7,36 +8,48 @@ import {
XenceQuickKeysDisplayBrightness,
XenceQuickKeysEvents,
XenceQuickKeysOrientation,
XenceQuickKeysWheelSpeed,
} from './types'

const keyCount = 10

export class XenceQuickKeysDevice extends EventEmitter<XenceQuickKeysEvents> implements XenceQuickKeys {
protected readonly device: HIDDevice
// private readonly options: Readonly<OpenStreamDeckOptions>
// private readonly keyState: boolean[]
private readonly keyState: boolean[]

constructor(device: HIDDevice) {
super()

this.device = device

// this.keyState = new Array(keyCount).fill(false)

this.device.on('input', (_data) => {
// TODO
// for (let keyIndex = 0; keyIndex < keyCount; keyIndex++) {
// const keyPressed = Boolean(data[keyIndex])
// const stateChanged = keyPressed !== this.keyState[keyIndex]
// if (stateChanged) {
// this.keyState[keyIndex] = keyPressed
// if (keyPressed) {
// this.emit('down', keyIndex)
// } else {
// this.emit('up', keyIndex)
// }
// }
// }
this.keyState = new Array(keyCount).fill(false)

this.device.on('data', (reportId, data) => {
console.log(reportId, data)
if (reportId === 0x02 && data.readUInt8(0) === 0xf0) {
const wheelByte = data.readUInt8(6)
if (wheelByte > 0) {
if (wheelByte & 0x01) {
this.emit('wheel', WheelEvent.Right)
} else if (wheelByte & 0x02) {
this.emit('wheel', WheelEvent.Left)
}
} else {
const keys = data.readUInt16LE(1)
for (let keyIndex = 0; keyIndex < keyCount; keyIndex++) {
const keyPressed = (keys & (1 << keyIndex)) > 0
const stateChanged = keyPressed !== this.keyState[keyIndex]
if (stateChanged) {
this.keyState[keyIndex] = keyPressed
if (keyPressed) {
this.emit('down', keyIndex)
} else {
this.emit('up', keyIndex)
}
}
}
}
}
})

this.subscribeToKeyEvents().catch((e) => {
Expand Down Expand Up @@ -152,4 +165,73 @@ export class XenceQuickKeysDevice extends EventEmitter<XenceQuickKeysEvents> imp

return this.device.sendReports([buffer])
}

public async setWheelSpeed(speed: XenceQuickKeysWheelSpeed): Promise<void> {
if (!Object.values(XenceQuickKeysWheelSpeed).includes(speed)) {
throw new TypeError('Expected a valid speed')
}

const buffer = Buffer.alloc(32)
buffer.writeUInt8(0x02, 0)
buffer.writeUInt8(0xb4, 1)
buffer.writeUInt8(0x04, 2)
buffer.writeUInt8(0x01, 3)
buffer.writeUInt8(0x01, 4)
buffer.writeUInt8(speed, 5)

this.insertHeader(buffer)

return this.device.sendReports([buffer])
}

public async setSleepTimeout(minutes: number): Promise<void> {
if (minutes < 0 || minutes > 255) {
throw new TypeError('Expected a valid number of minutes')
}

const buffer = Buffer.alloc(32)
buffer.writeUInt8(0x02, 0)
buffer.writeUInt8(0xb4, 1)
buffer.writeUInt8(0x08, 2)
buffer.writeUInt8(0x01, 3)
buffer.writeUInt8(minutes, 4)

this.insertHeader(buffer)

return this.device.sendReports([buffer])
}

public async showOverlayText(duration: number, text: string): Promise<void> {
if (duration <= 0 || duration > 255) throw new TypeError('Expected a valid number of seconds')

if (typeof text !== 'string' || text.length > 32)
throw new TypeError(`Expected a valid overlay text of up to 32 characters`)

const buffers = [
this.createOverlayChunk(0x05, duration, text.substr(0, 8), false),
this.createOverlayChunk(0x06, duration, text.substr(8, 8), text.length > 16),
]

for (let offset = 16; offset < text.length; offset += 8) {
buffers.push(this.createOverlayChunk(0x06, duration, text.substr(offset, 8), text.length > offset + 8))
}

return this.device.sendReports(buffers)
}

private createOverlayChunk(specialByte: number, duration: number, chars: string, hasMore: boolean): Buffer {
const buffer = Buffer.alloc(32)
buffer.writeUInt8(0x02, 0)
buffer.writeUInt8(0xb1, 1)
buffer.writeUInt8(specialByte, 2)
buffer.writeUInt8(duration, 3)
buffer.writeUInt8(chars.length * 2, 5)
buffer.writeUInt8(hasMore ? 0x01 : 0x00, 6)

this.insertHeader(buffer)

buffer.write(chars, 16, 'utf16le')

return buffer
}
}
2 changes: 1 addition & 1 deletion packages/core/src/hid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface HIDDevice {
dataKeyOffset?: number

on(event: 'error', handler: (data: any) => void): this
on(event: 'input', handler: (keys: number[]) => void): this
on(event: 'data', handler: (reportId: number, data: Buffer) => void): this

close(): Promise<void>

Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
XenceQuickKeysDisplayBrightness,
XenceQuickKeysEvents,
XenceQuickKeysOrientation,
XenceQuickKeysWheelSpeed,
} from './types'

/**
Expand Down Expand Up @@ -43,6 +44,18 @@ export class XenceQuickKeysProxy implements XenceQuickKeys {
return this.device.setDisplayBrightness(brightness)
}

public async setWheelSpeed(speed: XenceQuickKeysWheelSpeed): Promise<void> {
return this.device.setWheelSpeed(speed)
}

public async setSleepTimeout(minutes: number): Promise<void> {
return this.device.setSleepTimeout(minutes)
}

public async showOverlayText(duration: number, text: string): Promise<void> {
return this.device.showOverlayText(duration, text)
}

/**
* EventEmitter
*/
Expand Down
36 changes: 30 additions & 6 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ export enum XenceQuickKeysDisplayBrightness {
Full = 3,
}

export enum XenceQuickKeysWheelSpeed {
Slowest = 5,
Slower = 4,
Normal = 3,
Faster = 2,
Fastest = 1,
}

export interface XenceQuickKeys extends EventEmitter<XenceQuickKeysEvents> {
/**
* Checks if a keyIndex is valid. Throws an error on failure
Expand Down Expand Up @@ -71,12 +79,28 @@ export interface XenceQuickKeys extends EventEmitter<XenceQuickKeysEvents> {
*/
setDisplayBrightness(brightness: XenceQuickKeysDisplayBrightness): Promise<void>

// /**
// * Sets the brightness of the keys on the Stream Deck
// *
// * @param {number} percentage The percentage brightness
// */
// setBrightness(percentage: number): Promise<void>
/**
* Set the speed of the wheel
*
* @param {number} speed The speed of the wheel 5-1
*/
setWheelSpeed(speed: XenceQuickKeysWheelSpeed): Promise<void>

/**
* Set the sleep timeout. The device will go into a sleep mode after this period.
* In minutes
*
* @param {number} minutes The timeout in minutes
*/
setSleepTimeout(minutes: number): Promise<void>

/**
* Show a line of text over the whole display, for a period of time
*
* @param {number} duration How long to show for (seconds)
* @param {string} text The text to display. Up to 32 characters
*/
showOverlayText(duration: number, text: string): Promise<void>

// /**
// * Get firmware version from Stream Deck
Expand Down
8 changes: 2 additions & 6 deletions packages/node/src/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,8 @@ export class NodeHIDDevice extends EventEmitter implements HIDDevice {
this.device = new HID.HID(deviceInfo.path)
this.device.on('error', (error) => this.emit('error', error))

this.device.on('data', (data) => {
// Button press
if (data[0] === 0x01) {
const keyData = data.slice(this.dataKeyOffset || 0, data.length - 1)
this.emit('input', keyData)
}
this.device.on('data', (data: Buffer) => {
this.emit('data', data.readUInt8(0), data.slice(1))
})
}

Expand Down
9 changes: 8 additions & 1 deletion packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import * as HID from 'node-hid'
import { NodeHIDDevice, XenceQuickKeysInfo } from './device'
import { XenceQuickKeysNode } from './wrapper'

export { KeyIndex, XenceQuickKeys } from '@xencelabs-quick-keys/core'
export {
KeyIndex,
XenceQuickKeys,
XenceQuickKeysOrientation,
WheelEvent,
XenceQuickKeysWheelSpeed,
XenceQuickKeysDisplayBrightness,
} from '@xencelabs-quick-keys/core'

const DEVICE_INTERFACE = 2

Expand Down
51 changes: 40 additions & 11 deletions packages/webhid-demo/public/index.html
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
<html>

<head>
<style>
.pressed {
background-color: pink;
}

.btn {
border: 1px solid #000;
display: block;
width: 40px;
height: 40px;
text-align: center;
}

</style>
</head>

<body>
<h1>Xencelabs Quick Keys WebHID demo</h1>
<h3>Based on <a href="https://github.com/Julusian/node-xencelabs-quick-keys/">@xencelabs-quick-keys</a> <a
href="https://www.npmjs.com/package/@xencelabs-quick-keys/webhid">npm</a></h3>
<h3>
Getting started
</h3>
<p>Requires Chrome and either:</p>
<ul>
<li>Chrome v89+</li>
<li>Go to chrome://flags and enable "Experimental Web Platform features", then restart your
browser</li>
</ul>
<p>Requires Chrome v89+</p>
<h3>
Note: For linux, you need to ensure udev is setup with the correct device permissions for the hidraw driver.
</h3>
Expand All @@ -24,7 +30,17 @@ <h3>
</p>

<p>
<input type="range" min="1" max="100" value="60" id="brightness-range" />
Wheel Red: <input type="range" min="0" max="255" value="255" id="red-range" />
</p>
<p>
Wheel Green: <input type="range" min="0" max="255" value="0" id="green-range" />
</p>
<p>
Wheel Blue: <input type="range" min="0" max="255" value="0" id="blue-range" />
</p>

<p>
Wheel Speed: <input type="range" min="1" max="5" value="3" id="speed-range" />
</p>

<p>
Expand All @@ -50,9 +66,12 @@ <h3>
<p>
<table>
<tr>
<td colspan="4">Note: these are orientation for 0&#176;</td>
<td colspan="5">Note: these are orientation for 0&#176;</td>
</tr>
<tr>
<td rowspan="2">
<div class="btn" data-id="8"></div>
</td>
<td>
<input class="textlabel" value="one" data-id="0" />
</td>
Expand All @@ -65,6 +84,9 @@ <h3>
<td>
<input class="textlabel" value="four" data-id="3" />
</td>
<td rowspan="2">
<div class="btn" id="counter" data-id="9">0</div>
</td>
</tr>
<tr>
<td>
Expand All @@ -83,6 +105,13 @@ <h3>
</table>
</p>

<p>
Overlay:
<input id="overlay-duration" type="number" min="0" max="255" value="2" />
<input id="overlay-text" type="text" value="This is an overlay!" />
<button id="overlay-show">Show</button>
</p>

<div id="log" style="white-space: pre; background: #ffffff;"></div>

<script src="app.js" type="text/javascript"></script>
Expand Down

0 comments on commit 29f166c

Please sign in to comment.