├── .gitattributes ├── .prettierignore ├── .editorconfig ├── packages ├── webhid-demo │ ├── Dockerfile │ ├── README.md │ ├── tsconfig.json │ ├── package.json │ ├── public │ │ └── index.html │ ├── LICENSE │ ├── webpack.config.js │ ├── src │ │ └── app.ts │ └── CHANGELOG.md ├── core │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── src │ │ ├── index.ts │ │ ├── genericHIDDevice.ts │ │ ├── lib.ts │ │ ├── api.ts │ │ └── watcher.ts │ ├── jest.config.js │ ├── README.md │ ├── LICENSE │ ├── package.json │ └── CHANGELOG.md ├── node-record-test │ ├── README.md │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── jest.config.js │ ├── src │ │ ├── lib.ts │ │ └── index.ts │ ├── LICENSE │ ├── scripts │ │ └── copy-natives.js │ ├── package.json │ └── CHANGELOG.md ├── node │ ├── tsconfig.json │ ├── src │ │ ├── api.ts │ │ ├── index.ts │ │ ├── lib.ts │ │ ├── __tests__ │ │ │ ├── products.spec.ts │ │ │ ├── watcher.spec.ts │ │ │ ├── lib.ts │ │ │ ├── recordings │ │ │ │ ├── 1080_XK-3 Foot Pedal.json │ │ │ │ ├── 1224_XK-3 Switch Interface.json │ │ │ │ ├── 1127_XK-4 Stick.json │ │ │ │ ├── 1130_XK-8 Stick.json │ │ │ │ └── 1049_XK-16 Stick.json │ │ │ ├── recordings.spec.ts │ │ │ ├── __snapshots__ │ │ │ │ └── xkeys.spec.ts.snap │ │ │ └── xkeys.spec.ts │ │ ├── node-hid-wrapper.ts │ │ ├── __mocks__ │ │ │ └── node-hid.ts │ │ ├── watcher.ts │ │ └── methods.ts │ ├── tsconfig.build.json │ ├── README.md │ ├── jest.config.js │ ├── examples │ │ ├── reset-unitId.js │ │ ├── simply-connect.js │ │ ├── using-node-hid.js │ │ ├── multiple-panels.js │ │ └── basic-log-all-events.js │ ├── LICENSE │ ├── package.json │ └── CHANGELOG.md └── webhid │ ├── src │ ├── index.ts │ ├── web-hid-wrapper.ts │ ├── watcher.ts │ ├── globalConnectListener.ts │ └── methods.ts │ ├── README.md │ ├── tsconfig.json │ ├── jest.config.js │ ├── tsconfig.build.json │ ├── LICENSE │ ├── package.json │ └── CHANGELOG.md ├── lerna.json ├── tsconfig.json ├── .eslintrc.js ├── .gitignore ├── tsconfig.build.json ├── jest.config.base.js ├── .github └── workflows │ ├── publish-demo.yml │ ├── publish-nightly.yml │ ├── lint-and-test.yml │ ├── publish-prerelease.yml │ └── publish-release.yml ├── LICENSE ├── scripts └── install-ci.js ├── package.json └── CHANGELOG.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | */package.json -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = tab 3 | 4 | [*.{cs,js,ts,json}] 5 | indent_size = 4 -------------------------------------------------------------------------------- /packages/webhid-demo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | COPY dist /usr/share/nginx/html 4 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./src/**/*" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/node-record-test/README.md: -------------------------------------------------------------------------------- 1 | # X-keys - Node.js-recorder 2 | 3 | This folder contains a Node.js application 4 | -------------------------------------------------------------------------------- /packages/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./src/**/*" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/node-record-test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./src/**/*" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/webhid-demo/README.md: -------------------------------------------------------------------------------- 1 | # X-keys - WebHID 2 | 3 | This folder contains an example implementation for the WebHID-version of the xkeys-library. 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/**" 4 | ], 5 | "npmClient": "yarn", 6 | "useWorkspaces": true, 7 | "version": "3.3.0" 8 | } 9 | -------------------------------------------------------------------------------- /packages/webhid/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@xkeys-lib/core' 2 | 3 | export * from './methods' 4 | export * from './watcher' 5 | export * from './web-hid-wrapper' 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "exclude": ["node_modules/**"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/node/src/api.ts: -------------------------------------------------------------------------------- 1 | import type * as HID from 'node-hid' 2 | 3 | /** HID.Device but with .path guaranteed */ 4 | export type HID_Device = HID.Device & { path: string } 5 | -------------------------------------------------------------------------------- /packages/node/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@xkeys-lib/core' 2 | 3 | export * from './api' 4 | export * from './node-hid-wrapper' 5 | export * from './watcher' 6 | export * from './methods' 7 | -------------------------------------------------------------------------------- /packages/node/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "./src/**/*" 5 | ], 6 | "compilerOptions": { 7 | "outDir": "./dist" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "./src/**/*" 5 | ], 6 | "compilerOptions": { 7 | "outDir": "./dist", 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib' 2 | export * from './api' 3 | export * from './products' 4 | export * from './watcher' 5 | export * from './genericHIDDevice' 6 | export { XKeys } from './xkeys' 7 | -------------------------------------------------------------------------------- /packages/node-record-test/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "./src/**/*" 5 | ], 6 | "compilerOptions": { 7 | "outDir": "./dist", 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/node/README.md: -------------------------------------------------------------------------------- 1 | # X-keys - Node.js 2 | 3 | This package contains the Node.js-specific implementation of the X-keys library. 4 | 5 | See documentation at [https://github.com/SuperFlyTV/xkeys](https://github.com/SuperFlyTV/xkeys). 6 | -------------------------------------------------------------------------------- /packages/webhid/README.md: -------------------------------------------------------------------------------- 1 | # X-keys - WebHID 2 | 3 | This package contains the WebHID-specific implementation of the X-keys library (to be used in browsers). 4 | 5 | See documentation at [https://github.com/SuperFlyTV/xkeys](https://github.com/SuperFlyTV/xkeys). 6 | -------------------------------------------------------------------------------- /packages/webhid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./src/**/*" 5 | ], 6 | "compilerOptions": { 7 | "lib": [ 8 | "es2015", 9 | "dom" 10 | ], 11 | "types": [ 12 | "jest", 13 | "w3c-web-hid" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: './node_modules/@sofie-automation/code-standard-preset/eslint/main', 3 | env: { 4 | node: true, 5 | jest: true, 6 | }, 7 | ignorePatterns: ['**/dist/**/*', '**/__tests__/**/*', '**/__mocks__/**/*', '**/examples/**/*', '**/scripts/**/*'], 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line node/no-unpublished-require 2 | const base = require('../../jest.config.base') 3 | const packageJson = require('./package') 4 | 5 | module.exports = { 6 | ...base, 7 | name: packageJson.name, 8 | displayName: packageJson.name, 9 | } 10 | -------------------------------------------------------------------------------- /packages/node/jest.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line node/no-unpublished-require 2 | const base = require('../../jest.config.base') 3 | const packageJson = require('./package') 4 | 5 | module.exports = { 6 | ...base, 7 | name: packageJson.name, 8 | displayName: packageJson.name, 9 | } 10 | -------------------------------------------------------------------------------- /packages/webhid/jest.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line node/no-unpublished-require 2 | const base = require('../../jest.config.base') 3 | const packageJson = require('./package') 4 | 5 | module.exports = { 6 | ...base, 7 | name: packageJson.name, 8 | displayName: packageJson.name, 9 | } 10 | -------------------------------------------------------------------------------- /packages/node-record-test/jest.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line node/no-unpublished-require 2 | const base = require('../../jest.config.base') 3 | const packageJson = require('./package') 4 | 5 | module.exports = { 6 | ...base, 7 | name: packageJson.name, 8 | displayName: packageJson.name, 9 | } 10 | -------------------------------------------------------------------------------- /packages/webhid-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./src/**/*" 5 | ], 6 | "compilerOptions": { 7 | "outDir": "./dist", 8 | "lib": [ 9 | "es6", 10 | "dom" 11 | ], 12 | "types": [ 13 | "jest", 14 | "w3c-web-hid" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/webhid/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "./src/**/*" 5 | ], 6 | "compilerOptions": { 7 | "outDir": "./dist", 8 | "lib": [ 9 | "es2015", 10 | "dom" 11 | ], 12 | "types": [ 13 | "jest", 14 | "w3c-web-hid" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # X-keys - Core 2 | 3 | This package contains the platform-agnostic parts of the X-keys library. 4 | 5 | *You should not be importing this package directly, instead you'll want to use one of the wrapper libraries to provide the appropriate HID bindings for your target platform, see [https://github.com/SuperFlyTV/xkeys](https://github.com/SuperFlyTV/xkeys)* 6 | 7 | -------------------------------------------------------------------------------- /packages/node-record-test/src/lib.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import { promisify } from 'util' 3 | 4 | export const fsAccess = promisify(fs.access) 5 | export const fsWriteFile = promisify(fs.writeFile) 6 | 7 | export async function exists(path: string): Promise { 8 | try { 9 | await fsAccess(path) 10 | return true 11 | } catch (err) { 12 | return false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # production 12 | build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .vscode/* 25 | **/dist/* 26 | deploy/ 27 | lerna-debug.log 28 | -------------------------------------------------------------------------------- /packages/node/src/lib.ts: -------------------------------------------------------------------------------- 1 | import type * as HID from 'node-hid' 2 | /* 3 | * This file contains internal convenience functions 4 | */ 5 | 6 | export function isHID_Device(device: HID.Device | HID.HID | HID.HIDAsync | string): device is HID.Device { 7 | return ( 8 | typeof device === 'object' && 9 | (device as HID.Device).vendorId !== undefined && 10 | (device as HID.Device).productId !== undefined && 11 | (device as HID.Device).interface !== undefined 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/genericHIDDevice.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The expected interface for a HIDDevice. 3 | * This is to be implemented by any wrapping libraries to translate their platform specific devices into a common and simpler form 4 | */ 5 | export interface HIDDevice { 6 | on(event: 'error', handler: (data: any) => void): this 7 | on(event: 'data', handler: (data: Buffer) => void): this 8 | 9 | write(data: number[]): void 10 | 11 | /** Returns a promise which settles when all writes has completed */ 12 | flush(): Promise 13 | 14 | close(): Promise 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sofie-automation/code-standard-preset/ts/tsconfig.lib", 3 | "exclude": [ 4 | "node_modules/**", 5 | "**/__tests__", 6 | "**/__mocks__" 7 | ], 8 | "include": [ 9 | "/src/**/*" 10 | ], 11 | "compilerOptions": { 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "paths": { 15 | "*": [ 16 | "./node_modules/*" 17 | ] 18 | }, 19 | "types": [ 20 | "node" 21 | ], 22 | "importHelpers": false, 23 | "declarationMap": true, 24 | // Target: node 10 25 | "target": "es2018", 26 | "lib": [ 27 | "es2018" 28 | ], 29 | "skipLibCheck": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /jest.config.base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | projects: [''], 4 | preset: 'ts-jest', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: 'tsconfig.json', 8 | }, 9 | }, 10 | moduleFileExtensions: ['js', 'ts'], 11 | transform: { 12 | '^.+\\.(ts|tsx)$': 'ts-jest', 13 | }, 14 | testMatch: ['**/__tests__/**/*.spec.(ts|js)'], 15 | testEnvironment: 'node', 16 | coverageThreshold: { 17 | global: { 18 | branches: 100, 19 | functions: 100, 20 | lines: 100, 21 | statements: 100, 22 | }, 23 | }, 24 | coverageDirectory: './coverage/', 25 | coverageDirectory: '/coverage/', 26 | collectCoverage: false, 27 | // verbose: true, 28 | } 29 | -------------------------------------------------------------------------------- /packages/node/examples/reset-unitId.js: -------------------------------------------------------------------------------- 1 | const { XKeysWatcher } = require('xkeys') 2 | 3 | /* 4 | This example looks up all connected X-keys panels 5 | and resets the unitId of them 6 | */ 7 | 8 | const watcher = new XKeysWatcher() 9 | watcher.on('error', (e) => { 10 | console.log('Error in XKeysWatcher', e) 11 | }) 12 | 13 | watcher.on('connected', (xkeysPanel) => { 14 | console.log( 15 | `Connected to "${xkeysPanel.info.name}", it has productId=${xkeysPanel.info.productId}, unitId=${xkeysPanel.info.unitId}` 16 | ) 17 | 18 | if (xkeysPanel.info.unitId !== 0) { 19 | console.log('Resetting unitId...') 20 | xkeysPanel.setUnitId(0) 21 | console.log('Resetting unitId done') 22 | } else { 23 | console.log('unitId is already 0') 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /packages/webhid-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xkeys-lib/webhid-demo", 3 | "version": "3.3.0", 4 | "private": true, 5 | "license": "MIT", 6 | "homepage": "https://github.com/SuperFlyTV/xkeys#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/SuperFlyTV/xkeys.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/SuperFlyTV/xkeys/issues" 13 | }, 14 | "author": { 15 | "name": "Johan Nyman", 16 | "email": "johan@superfly.tv", 17 | "url": "https://github.com/nytamin" 18 | }, 19 | "scripts": { 20 | "start": "webpack serve --mode development --color", 21 | "build": "rimraf dist && cross-env NODE_ENV=production webpack --progress" 22 | }, 23 | "dependencies": { 24 | "buffer": "^6.0.3", 25 | "xkeys-webhid": "3.3.0" 26 | }, 27 | "devDependencies": { 28 | "copy-webpack-plugin": "^7.0.0", 29 | "ts-loader": "^8.3.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/publish-demo.yml: -------------------------------------------------------------------------------- 1 | name: Publish the WebHID demo to pages 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Allows you to run this workflow manually from the Actions tab 6 | workflow_dispatch: 7 | 8 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 9 | jobs: 10 | publish-demo: 11 | name: Publish demo to Github Pages 12 | runs-on: ubuntu-latest 13 | continue-on-error: false 14 | timeout-minutes: 15 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js 14.x 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 16.x 22 | - name: Prepare build 23 | run: | 24 | yarn install 25 | yarn build 26 | env: 27 | CI: true 28 | - name: Publish 29 | uses: peaceiris/actions-gh-pages@v3 30 | with: 31 | github_token: ${{ secrets.GITHUB_TOKEN }} 32 | publish_dir: ./packages/webhid-demo/dist 33 | -------------------------------------------------------------------------------- /packages/webhid-demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | X-keys WebHID demo 6 | 7 | 8 | 9 |

X-keys WebHID demo

10 |

11 | Based on @xkeys 12 |

13 |

14 | Getting started 15 |

16 |

Requirements:

17 |
    18 |
  • Chrome > 89 (enabled by default)
  • 19 |
  • Chrome < 89: Go to chrome://flags and enable "Experimental Web Platform features", then restart your 20 | browser
  • 21 |
22 |

23 | Note: For linux, you need to ensure udev is setup with the correct device permissions for the hidraw driver. 24 |

25 |

26 | 27 |

28 |

29 |

Connected Devices:

30 |

31 |

32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 SuperFly.tv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 SuperFly.tv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/node/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 SuperFly.tv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/webhid/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 SuperFly.tv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/webhid-demo/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 SuperFly.tv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/node-record-test/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 SuperFly.tv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/products.spec.ts: -------------------------------------------------------------------------------- 1 | import { BackLightType, PRODUCTS } from '@xkeys-lib/core' 2 | 3 | describe('products.ts', () => { 4 | test('productIds should be unique', async () => { 5 | const productIds = new Map() 6 | for (const product of Object.values(PRODUCTS)) { 7 | for (const hidDevice of product.hidDevices) { 8 | const productId: number = hidDevice[0] 9 | const productInterface: number = hidDevice[1] 10 | 11 | const idPair = `${productId}-${productInterface}` 12 | // console.log('idPair', idPair) 13 | try { 14 | expect(productIds.has(idPair)).toBeFalsy() 15 | } catch (err) { 16 | console.log('productid', idPair, productIds.get(idPair)) 17 | throw err 18 | } 19 | productIds.set(idPair, product.name) 20 | } 21 | } 22 | }) 23 | test('verify integrity', async () => { 24 | for (const product of Object.values(PRODUCTS)) { 25 | try { 26 | expect(product.hidDevices.length).toBeGreaterThanOrEqual(1) 27 | 28 | if (product.backLightType === BackLightType.LEGACY) { 29 | expect(product.backLight2offset).toBeTruthy() 30 | } 31 | } catch (err) { 32 | console.log(`Error in product "${product.name}"`) 33 | throw err 34 | } 35 | } 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /packages/webhid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xkeys-webhid", 3 | "version": "3.3.0", 4 | "description": "An npm module for interfacing with the X-keys panels in a browser", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "license": "MIT", 8 | "homepage": "https://github.com/SuperFlyTV/xkeys", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/SuperFlyTV/xkeys.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/SuperFlyTV/xkeys/issues" 15 | }, 16 | "author": { 17 | "name": "Johan Nyman", 18 | "email": "johan@superfly.tv", 19 | "url": "https://github.com/nytamin" 20 | }, 21 | "scripts": { 22 | "build": "rimraf dist && yarn build:main", 23 | "build:main": "tsc -p tsconfig.build.json" 24 | }, 25 | "files": [ 26 | "dist/**" 27 | ], 28 | "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", 29 | "keywords": [ 30 | "xkeys", 31 | "x-keys", 32 | "hid", 33 | "usb", 34 | "hardware", 35 | "interface", 36 | "controller", 37 | "webhid" 38 | ], 39 | "dependencies": { 40 | "@types/w3c-web-hid": "^1.0.3", 41 | "@xkeys-lib/core": "3.3.0", 42 | "buffer": "^6.0.3", 43 | "p-queue": "^6.6.2" 44 | }, 45 | "engines": { 46 | "node": ">=14" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/node/examples/simply-connect.js: -------------------------------------------------------------------------------- 1 | const { setupXkeysPanel, listAllConnectedPanels } = require('xkeys') 2 | 3 | /* 4 | This example shows how to use XKeys.setupXkeysPanel() 5 | directly, instead of going via XKeysWatcher() 6 | */ 7 | 8 | // Connect to an xkeys-panel: 9 | setupXkeysPanel() 10 | .then((xkeysPanel) => { 11 | xkeysPanel.on('disconnected', () => { 12 | console.log(`X-keys panel of type ${xkeysPanel.info.name} was disconnected`) 13 | // Clean up stuff 14 | xkeysPanel.removeAllListeners() 15 | }) 16 | xkeysPanel.on('error', (...errs) => { 17 | console.log('X-keys error:', ...errs) 18 | }) 19 | 20 | xkeysPanel.on('down', (keyIndex, metadata) => { 21 | console.log('Button pressed', keyIndex, metadata) 22 | }) 23 | 24 | // ... 25 | }) 26 | .catch(console.log) // Handle error 27 | 28 | // List and connect to all xkeys-panels: 29 | listAllConnectedPanels().forEach((connectedPanel) => { 30 | setupXkeysPanel(connectedPanel) 31 | .then((xkeysPanel) => { 32 | console.log(`Connected to ${xkeysPanel.info.name}`) 33 | 34 | xkeysPanel.on('down', (keyIndex, metadata) => { 35 | console.log('Button pressed ', keyIndex, metadata) 36 | 37 | // Light up a button when pressed: 38 | xkeysPanel.setBacklight(keyIndex, 'red') 39 | }) 40 | }) 41 | .catch(console.log) // Handle error 42 | }) 43 | -------------------------------------------------------------------------------- /packages/node/examples/using-node-hid.js: -------------------------------------------------------------------------------- 1 | const HID = require('node-hid') 2 | const { setupXkeysPanel, XKeys } = require('xkeys') 3 | 4 | /* 5 | This example shows how to use node-hid to list all connected usb devices, then 6 | connecting to any supported X-keys panels. 7 | */ 8 | 9 | Promise.resolve().then(async () => { 10 | 11 | // List all connected usb devices: 12 | const devices = await HID.devicesAsync() 13 | 14 | for (const device of devices) { 15 | 16 | // Filter for supported X-keys devices: 17 | if (XKeys.filterDevice(device)) { 18 | 19 | console.log('Connecting to X-keys device:', device.product) 20 | 21 | setupXkeysPanel(device) 22 | .then((xkeysPanel) => { 23 | xkeysPanel.on('disconnected', () => { 24 | console.log(`X-keys panel of type ${xkeysPanel.info.name} was disconnected`) 25 | // Clean up stuff 26 | xkeysPanel.removeAllListeners() 27 | }) 28 | xkeysPanel.on('error', (...errs) => { 29 | console.log('X-keys error:', ...errs) 30 | }) 31 | 32 | xkeysPanel.on('down', (keyIndex, metadata) => { 33 | console.log('Button pressed', keyIndex, metadata) 34 | }) 35 | 36 | // ... 37 | }) 38 | .catch(console.log) // Handle error 39 | 40 | } else { 41 | // is not an X-keys device 42 | console.log('Not a supported X-keys device:', device.product || device.productId) 43 | } 44 | 45 | } 46 | 47 | }).catch(console.log) 48 | 49 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xkeys-lib/core", 3 | "version": "3.3.0", 4 | "description": "NPM package to interact with the X-keys panels", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "license": "MIT", 8 | "homepage": "https://github.com/SuperFlyTV/xkeys", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/SuperFlyTV/xkeys.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/SuperFlyTV/xkeys/issues" 15 | }, 16 | "author": { 17 | "name": "Johan Nyman", 18 | "email": "johan@superfly.tv", 19 | "url": "https://github.com/nytamin" 20 | }, 21 | "contributors": [ 22 | { 23 | "name": "Michael Hetherington", 24 | "url": "https://xkeys.com" 25 | }, 26 | { 27 | "name": "Andreas Reich", 28 | "url": "https://github.com/cyraxx" 29 | } 30 | ], 31 | "scripts": { 32 | "build": "rimraf dist && yarn build:main", 33 | "build:main": "tsc -p tsconfig.build.json", 34 | "__test": "jest" 35 | }, 36 | "files": [ 37 | "dist/**" 38 | ], 39 | "engines": { 40 | "node": ">=14" 41 | }, 42 | "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", 43 | "keywords": [ 44 | "xkeys", 45 | "x-keys", 46 | "hid", 47 | "usb", 48 | "hardware", 49 | "interface", 50 | "controller" 51 | ], 52 | "dependencies": { 53 | "tslib": "^2.4.0" 54 | }, 55 | "publishConfig": { 56 | "access": "public" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/node-record-test/scripts/copy-natives.js: -------------------------------------------------------------------------------- 1 | const find = require('find'); 2 | const os = require('os') 3 | const path = require('path') 4 | const fs = require('fs-extra') 5 | 6 | const arch = os.arch() 7 | const platform = os.platform() 8 | const prebuildType = process.argv[2] || `${platform}-${arch}` 9 | 10 | // function isFileForPlatform(filename) { 11 | // if (filename.indexOf(path.join('prebuilds', prebuildType)) !== -1) { 12 | // return true 13 | // } else { 14 | // return false 15 | // } 16 | // } 17 | 18 | const sourceDirName = path.join(__dirname, '../../../') 19 | // const targetDirName = path.join(__dirname, '../') 20 | 21 | console.log('Copy-Natives --------------') 22 | console.log('Looking for modules in', sourceDirName, 'for', prebuildType) 23 | // console.log('to copy into ', targetDirName) 24 | 25 | const modulesToCopy = new Map() 26 | find.file(/\.node$/, path.join(sourceDirName, 'node_modules'), (files) => { 27 | files.forEach(fullPath => { 28 | console.log(fullPath) 29 | if (fullPath.indexOf(sourceDirName) === 0) { 30 | console.log('a') 31 | const file = fullPath.substr(sourceDirName.length) 32 | 33 | const moduleName = file.match(/node_modules[\/\\]([^\\\/]+)/) 34 | if (moduleName) { 35 | modulesToCopy.set(moduleName[1], true) 36 | } 37 | } 38 | }) 39 | 40 | modulesToCopy.forEach((_, moduleName) => { 41 | console.log('copying', moduleName) 42 | 43 | fs.copySync(path.join(sourceDirName, 'node_modules', moduleName) , path.join('deploy/node_modules/', moduleName )) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /scripts/install-ci.js: -------------------------------------------------------------------------------- 1 | const { promisify } = require('util') 2 | const fs = require('fs') 3 | const { exec } = require('child_process') 4 | 5 | const fsReadFile = promisify(fs.readFile) 6 | const fsWriteFile = promisify(fs.writeFile) 7 | 8 | // This function installs all dependencies except node-hid 9 | // It is used during CI/tests, where the binaries aren't used anyway 10 | 11 | function run(command) { 12 | return new Promise((resolve, reject) => { 13 | console.log(command) 14 | exec(command, (err, stdout, stderr) => { 15 | if (stdout) console.log(stdout) 16 | if (stderr) console.log(stderr) 17 | if (err) reject(err) 18 | else { 19 | resolve() 20 | } 21 | }) 22 | }) 23 | } 24 | 25 | ;(async () => { 26 | const path = './package.json' 27 | const orgStr = await fsReadFile(path) 28 | try { 29 | const packageJson = JSON.parse(orgStr) 30 | 31 | if (!packageJson.optionalDependencies) packageJson.optionalDependencies = {} 32 | packageJson.optionalDependencies['node-hid'] = packageJson.dependencies['node-hid'] 33 | delete packageJson.dependencies['node-hid'] 34 | await fsWriteFile(path, JSON.stringify(packageJson, null, 2)) 35 | 36 | await run('yarn install --ignore-optional') 37 | 38 | // Restore: 39 | await fsWriteFile(path, orgStr) 40 | 41 | await run('yarn install --ignore-scripts') 42 | } catch (e) { 43 | // Restore: 44 | await fsWriteFile(path, orgStr) 45 | throw e 46 | } 47 | })().then( 48 | () => { 49 | // eslint-disable-next-line no-process-exit 50 | process.exit(0) 51 | }, 52 | (err) => { 53 | console.error(err) 54 | // eslint-disable-next-line no-process-exit 55 | process.exit(1) 56 | } 57 | ) 58 | -------------------------------------------------------------------------------- /packages/webhid/src/web-hid-wrapper.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/unbound-method */ 2 | import { HIDDevice as CoreHIDDevice } from '@xkeys-lib/core' 3 | import { EventEmitter } from 'events' 4 | import Queue from 'p-queue' 5 | import { Buffer as WebBuffer } from 'buffer' 6 | 7 | /** 8 | * The wrapped browser HIDDevice. 9 | * This translates it into the common format (@see CoreHIDDevice) defined by @xkeys-lib/core 10 | */ 11 | export class WebHIDDevice extends EventEmitter implements CoreHIDDevice { 12 | private readonly device: HIDDevice 13 | 14 | private readonly reportQueue = new Queue({ concurrency: 1 }) 15 | 16 | constructor(device: HIDDevice) { 17 | super() 18 | 19 | this.device = device 20 | 21 | this.device.addEventListener('inputreport', this._handleInputreport) 22 | this.device.addEventListener('error', this._handleError) 23 | } 24 | public write(data: number[]): void { 25 | this.reportQueue 26 | .add(async () => { 27 | await this.device.sendReport(data[0], new Uint8Array(data.slice(1))) 28 | }) 29 | .catch((err) => { 30 | this.emit('error', err) 31 | }) 32 | } 33 | public async flush(): Promise { 34 | await this.reportQueue.onIdle() 35 | } 36 | 37 | public async close(): Promise { 38 | await this.device.close() 39 | this.device.removeEventListener('inputreport', this._handleInputreport) 40 | this.device.removeEventListener('error', this._handleError) 41 | } 42 | private _handleInputreport = (event: HIDInputReportEvent) => { 43 | const buf = WebBuffer.from(event.data.buffer) 44 | this.emit('data', buf) 45 | } 46 | private _handleError = (error: any) => { 47 | this.emit('error', error) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xkeys", 3 | "version": "3.3.0", 4 | "description": "An npm module for interfacing with the X-keys panels in Node.js", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "license": "MIT", 8 | "homepage": "https://github.com/SuperFlyTV/xkeys", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/SuperFlyTV/xkeys.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/SuperFlyTV/xkeys/issues" 15 | }, 16 | "author": { 17 | "name": "Johan Nyman", 18 | "email": "johan@superfly.tv", 19 | "url": "https://github.com/nytamin" 20 | }, 21 | "contributors": [ 22 | { 23 | "name": "Michael Hetherington", 24 | "url": "https://xkeys.com" 25 | }, 26 | { 27 | "name": "Andreas Reich", 28 | "url": "https://github.com/cyraxx" 29 | }, 30 | { 31 | "name": "Julian Waller", 32 | "url": "https://github.com/Julusian" 33 | } 34 | ], 35 | "scripts": { 36 | "build": "rimraf dist && yarn build:main", 37 | "build:main": "tsc -p tsconfig.build.json", 38 | "test": "jest" 39 | }, 40 | "files": [ 41 | "dist/**" 42 | ], 43 | "engines": { 44 | "node": ">=14" 45 | }, 46 | "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", 47 | "keywords": [ 48 | "xkeys", 49 | "x-keys", 50 | "hid", 51 | "usb", 52 | "hardware", 53 | "interface", 54 | "controller" 55 | ], 56 | "dependencies": { 57 | "@xkeys-lib/core": "3.3.0", 58 | "node-hid": "^3.0.0", 59 | "p-queue": "^6.6.2", 60 | "tslib": "^2.4.0" 61 | }, 62 | "optionalDependencies": { 63 | "usb": "^2.5.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/node/src/node-hid-wrapper.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/unbound-method */ 2 | import { HIDDevice } from '@xkeys-lib/core' 3 | import { EventEmitter } from 'events' 4 | import Queue from 'p-queue' 5 | import * as HID from 'node-hid' 6 | 7 | /** 8 | * This class wraps the node-hid.HID Device. 9 | * This translates it into the common format (@see HIDDevice) defined by @xkeys-lib/core 10 | */ 11 | export class NodeHIDDevice extends EventEmitter implements HIDDevice { 12 | static CLOSE_WAIT_TIME = 300 13 | 14 | private readonly writeQueue = new Queue({ concurrency: 1 }) 15 | 16 | constructor(private device: HID.HIDAsync) { 17 | super() 18 | 19 | this.device.on('error', this._handleError) 20 | this.device.on('data', this._handleData) 21 | } 22 | 23 | public write(data: number[]): void { 24 | this.writeQueue 25 | .add(async () => this.device.write(data)) 26 | .catch((err) => { 27 | this.emit('error', err) 28 | }) 29 | } 30 | 31 | public async close(): Promise { 32 | await this.device.close() 33 | 34 | // For some unknown reason, we need to wait a bit before returning because it 35 | // appears that the HID-device isn't actually closed properly until after a short while. 36 | // (This issue has been observed in Electron, where a app.quit() causes the application to crash with "Exit status 3221226505".) 37 | await new Promise((resolve) => setTimeout(resolve, NodeHIDDevice.CLOSE_WAIT_TIME)) 38 | 39 | this.device.removeListener('error', this._handleError) 40 | this.device.removeListener('data', this._handleData) 41 | } 42 | public async flush(): Promise { 43 | await this.writeQueue.onIdle() 44 | } 45 | 46 | private _handleData = (data: Buffer) => { 47 | this.emit('data', data) 48 | } 49 | private _handleError = (error: any) => { 50 | this.emit('error', error) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/node-record-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xkeys-lib/record-test", 3 | "version": "3.3.0", 4 | "private": true, 5 | "description": "A script for recording tests", 6 | "main": "dist/index.js", 7 | "typings": "dist/index.d.ts", 8 | "license": "MIT", 9 | "homepage": "https://github.com/SuperFlyTV/xkeys", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/SuperFlyTV/xkeys.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/SuperFlyTV/xkeys/issues" 16 | }, 17 | "author": { 18 | "name": "Johan Nyman", 19 | "email": "johan@superfly.tv", 20 | "url": "https://github.com/nytamin" 21 | }, 22 | "contributors": [ 23 | { 24 | "name": "Michael Hetherington", 25 | "url": "https://xkeys.com" 26 | }, 27 | { 28 | "name": "Andreas Reich", 29 | "url": "https://github.com/cyraxx" 30 | } 31 | ], 32 | "scripts": { 33 | "build": "rimraf dist && yarn build:main", 34 | "build:main": "tsc -p tsconfig.build.json", 35 | "build-record-test": "npm run build && rimraf ./deploy/xkeys-nodejs-test-recorder.exe && nexe dist/record-test.js -t windows-x64-12.18.1 -o ./deploy/xkeys-nodejs-test-recorder.exe && node scripts/copy-natives.js win32-x64" 36 | }, 37 | "files": [ 38 | "dist/**" 39 | ], 40 | "engines": { 41 | "node": ">=14" 42 | }, 43 | "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", 44 | "keywords": [ 45 | "xkeys", 46 | "x-keys", 47 | "hid", 48 | "usb", 49 | "hardware", 50 | "interface", 51 | "controller" 52 | ], 53 | "dependencies": { 54 | "@xkeys-lib/core": "3.3.0", 55 | "readline": "^1.3.0", 56 | "tslib": "^2.4.0", 57 | "xkeys": "3.3.0" 58 | }, 59 | "optionalDependencies": { 60 | "find": "^0.3.0", 61 | "nexe": "^3.3.7" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/core/src/lib.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file contains internal convenience functions 3 | */ 4 | 5 | /** Convenience function to force the input to be of a certain type. */ 6 | export function literal(o: T): T { 7 | return o 8 | } 9 | 10 | export function describeEvent(event: string, args: any[]): string { 11 | const metadataStr = (metadata: any) => { 12 | if (typeof metadata !== 'object') return `${metadata}` 13 | if (metadata === null) return 'null' 14 | 15 | const strs: string[] = [] 16 | Object.entries(metadata).forEach(([key, value]) => { 17 | strs.push(`${key}: ${value}`) 18 | }) 19 | return strs.join(', ') 20 | } 21 | 22 | if (event === 'down') { 23 | const keyIndex = args[0] 24 | const metadata = args[1] 25 | return `Button ${keyIndex} pressed. Metadata: ${metadataStr(metadata)}` 26 | } else if (event === 'up') { 27 | const keyIndex = args[0] 28 | const metadata = args[1] 29 | return `Button ${keyIndex} released. Metadata: ${metadataStr(metadata)}` 30 | } else if (event === 'jog') { 31 | const index = args[0] 32 | const value = args[1] 33 | const metadata = args[2] 34 | return `Jog ( index ${index}) value: ${value}. Metadata: ${metadataStr(metadata)}` 35 | } else if (event === 'shuttle') { 36 | const index = args[0] 37 | const value = args[1] 38 | const metadata = args[2] 39 | return `Shuttle ( index ${index}) value: ${value}. Metadata: ${metadataStr(metadata)}` 40 | } else if (event === 'joystick') { 41 | const index = args[0] 42 | const value = JSON.stringify(args[1]) 43 | const metadata = args[2] 44 | return `Joystick ( index ${index}) value: ${value}. Metadata: ${metadataStr(metadata)}` 45 | } else if (event === 'tbar') { 46 | const index = args[0] 47 | const value = args[1] 48 | const metadata = args[2] 49 | return `T-bar ( index ${index}) value: ${value}. Metadata: ${metadataStr(metadata)}` 50 | } else if (event === 'disconnected') { 51 | return `Panel disconnected!` 52 | } 53 | 54 | throw new Error('Unhnandled event!') 55 | } 56 | -------------------------------------------------------------------------------- /packages/webhid-demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/no-extraneous-require, node/no-unpublished-require */ 2 | const path = require('path') 3 | const CopyWebpackPlugin = require('copy-webpack-plugin') 4 | const { ProvidePlugin } = require('webpack') 5 | 6 | module.exports = { 7 | // Where to fine the source code 8 | context: path.join(__dirname, '/src'), 9 | 10 | // No source map for production build 11 | devtool: 'source-map', 12 | 13 | entry: path.join(__dirname, '/src/app.ts'), 14 | 15 | optimization: { 16 | // We no not want to minimize our code. 17 | minimize: false, 18 | }, 19 | 20 | output: { 21 | // The destination file name concatenated with hash (generated whenever you change your code). 22 | // The hash is really useful to let the browser knows when it should get a new bundle 23 | // or use the one in cache 24 | filename: 'app.js', 25 | 26 | // The destination folder where to put the output bundle 27 | path: path.join(__dirname, '/dist'), 28 | 29 | // Wherever resource (css, js, img) you call , 30 | // or css, or img use this path as the root 31 | publicPath: '/', 32 | 33 | // At some point you'll have to debug your code, that's why I'm giving you 34 | // for free a source map file to make your life easier 35 | sourceMapFilename: 'main.map', 36 | }, 37 | resolve: { 38 | extensions: ['.tsx', '.ts', '.js'], 39 | }, 40 | devServer: { 41 | contentBase: path.join(__dirname, '/public'), 42 | // match the output path 43 | publicPath: '/', 44 | // match the output `publicPath` 45 | historyApiFallback: true, 46 | }, 47 | 48 | module: { 49 | rules: [ 50 | { 51 | test: /\.tsx?$/, 52 | loader: 'ts-loader', 53 | exclude: /node_modules/, 54 | }, 55 | ], 56 | }, 57 | 58 | plugins: [ 59 | new CopyWebpackPlugin({ 60 | patterns: [{ from: path.join(__dirname, '/public'), to: path.join(__dirname, '/dist') }], 61 | }), 62 | new ProvidePlugin({ 63 | Buffer: ['buffer', 'Buffer'], 64 | }), 65 | ], 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xkeys-monorepo", 3 | "version": "0.0.0", 4 | "repository": "https://github.com/SuperFlyTV/xkeys", 5 | "private": true, 6 | "author": "Johan Nyman ", 7 | "license": "MIT", 8 | "workspaces": [ 9 | "packages/*" 10 | ], 11 | "scripts": { 12 | "build": "lerna run build --stream", 13 | "build:core": "yarn build --scope=@xkeys-lib/core", 14 | "lint:raw": "lerna exec --stream -- eslint --ext .ts --ext .js --ext .tsx --ext .jsx --ignore-pattern dist", 15 | "lint": "yarn lint:raw .", 16 | "lintfix": "yarn lint --fix", 17 | "test": "lerna run test --stream", 18 | "typecheck": "lerna exec -- tsc --noEmit", 19 | "cov": "jest --coverage; 0 coverage/lcov-report/index.html", 20 | "cov-open": "open-cli coverage/lcov-report/index.html", 21 | "send-coverage": "jest && codecov", 22 | "release:bump-release": "lerna version --exact --conventional-commits --conventional-graduate --no-push", 23 | "release:bump-prerelease": "lerna version --exact --conventional-commits --conventional-prerelease --no-push", 24 | "build-record-test": "cd packages/node-record-test && yarn build-record-test", 25 | "lerna:version": "lerna version --exact", 26 | "lerna:publish": "lerna publish", 27 | "lerna": "lerna" 28 | }, 29 | "devDependencies": { 30 | "@sofie-automation/code-standard-preset": "^2.5.2", 31 | "@types/jest": "^26.0.20", 32 | "cross-env": "^7.0.3", 33 | "jest": "^26.6.3", 34 | "lerna": "^4.0.0", 35 | "rimraf": "^3.0.2", 36 | "ts-jest": "^26.5.6", 37 | "typescript": "~4.5", 38 | "webpack": "^5.74.0", 39 | "webpack-cli": "^4.10.0", 40 | "webpack-dev-server": "^3.11.2" 41 | }, 42 | "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", 43 | "lint-staged": { 44 | "*.{js,css,json,md,scss}": [ 45 | "prettier --write" 46 | ], 47 | "*.{ts,tsx}": [ 48 | "yarn lint --fix" 49 | ] 50 | }, 51 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 52 | } 53 | -------------------------------------------------------------------------------- /packages/webhid/src/watcher.ts: -------------------------------------------------------------------------------- 1 | import { GenericXKeysWatcher, XKeys, XKeysWatcherOptions } from '@xkeys-lib/core' 2 | import { getOpenedXKeysPanels, setupXkeysPanel } from './methods' 3 | import { GlobalConnectListener } from './globalConnectListener' 4 | /** 5 | * Set up a watcher for newly connected X-keys panels. 6 | * Note: It is highly recommended to set up a listener for the disconnected event on the X-keys panel, to clean up after a disconnected device. 7 | */ 8 | export class XKeysWatcher extends GenericXKeysWatcher { 9 | private eventListeners: { stop: () => void }[] = [] 10 | private pollingInterval: NodeJS.Timeout | undefined = undefined 11 | 12 | constructor(options?: XKeysWatcherOptions) { 13 | super(options) 14 | 15 | if (!this.options.usePolling) { 16 | this.eventListeners.push(GlobalConnectListener.listenForAnyDisconnect(this.handleConnectEvent)) 17 | this.eventListeners.push(GlobalConnectListener.listenForAnyConnect(this.handleConnectEvent)) 18 | } else { 19 | this.pollingInterval = setInterval(() => { 20 | this.triggerUpdateConnectedDevices(false) 21 | }, this.options.pollingInterval) 22 | } 23 | } 24 | 25 | /** 26 | * Stop the watcher 27 | * @param closeAllDevices Set to false in order to NOT close all devices. Use this if you only want to stop the watching. Defaults to true 28 | */ 29 | public async stop(closeAllDevices = true): Promise { 30 | this.eventListeners.forEach((listener) => listener.stop()) 31 | 32 | if (this.pollingInterval) { 33 | clearInterval(this.pollingInterval) 34 | this.pollingInterval = undefined 35 | } 36 | 37 | await super.stop(closeAllDevices) 38 | } 39 | 40 | protected async getConnectedDevices(): Promise> { 41 | // Returns a Set of devicePaths of the connected devices 42 | return new Set(await getOpenedXKeysPanels()) 43 | } 44 | protected async setupXkeysPanel(device: HIDDevice): Promise { 45 | return setupXkeysPanel(device) 46 | } 47 | private handleConnectEvent = () => { 48 | // Called whenever a device is connected or disconnected 49 | 50 | if (!this.isActive) return 51 | 52 | this.triggerUpdateConnectedDevices(true) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/watcher.spec.ts: -------------------------------------------------------------------------------- 1 | import * as HID from 'node-hid' 2 | import * as HIDMock from '../__mocks__/node-hid' 3 | import { NodeHIDDevice, XKeys, XKeysWatcher } from '..' 4 | import { handleXkeysMessages, sleep, sleepTicks } from './lib' 5 | 6 | describe('XKeysWatcher', () => { 7 | afterEach(() => { 8 | HIDMock.resetMockWriteHandler() 9 | }) 10 | test('Detect device (w polling)', async () => { 11 | const POLL_INTERVAL = 10 12 | NodeHIDDevice.CLOSE_WAIT_TIME = 0 // We can override this to speed up the unit tests 13 | 14 | HIDMock.setMockWriteHandler(handleXkeysMessages) 15 | 16 | const onError = jest.fn((e) => { 17 | console.log('Error in XKeysWatcher', e) 18 | }) 19 | const onConnected = jest.fn((xkeys: XKeys) => { 20 | xkeys.on('disconnected', () => { 21 | onDisconnected() 22 | xkeys.removeAllListeners() 23 | }) 24 | }) 25 | const onDisconnected = jest.fn(() => {}) 26 | 27 | const watcher = new XKeysWatcher({ 28 | usePolling: true, 29 | pollingInterval: POLL_INTERVAL, 30 | }) 31 | watcher.on('error', onError) 32 | watcher.on('connected', onConnected) 33 | 34 | try { 35 | await sleep(POLL_INTERVAL * 2) 36 | expect(onConnected).toHaveBeenCalledTimes(0) 37 | 38 | // Add a device: 39 | { 40 | const hidDevice = { 41 | vendorId: XKeys.vendorId, 42 | productId: 1029, 43 | interface: 0, 44 | path: 'abc123', 45 | product: 'XK-24 MOCK', 46 | } as HID.Device 47 | 48 | HIDMock.mockSetDevices([hidDevice]) 49 | 50 | // Listen for the 'connected' event: 51 | await sleep(POLL_INTERVAL) 52 | expect(onConnected).toHaveBeenCalledTimes(1) 53 | } 54 | 55 | // Remove the device: 56 | { 57 | HIDMock.mockSetDevices([]) 58 | 59 | await sleepTicks(POLL_INTERVAL) 60 | expect(onDisconnected).toHaveBeenCalledTimes(1) 61 | } 62 | } catch (e) { 63 | throw e 64 | } finally { 65 | // Cleanup: 66 | await watcher.stop() 67 | } 68 | // Ensure the event handlers haven't been called again: 69 | await sleep(POLL_INTERVAL) 70 | expect(onDisconnected).toHaveBeenCalledTimes(1) 71 | expect(onConnected).toHaveBeenCalledTimes(1) 72 | 73 | expect(onError).toHaveBeenCalledTimes(0) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /packages/node/examples/multiple-panels.js: -------------------------------------------------------------------------------- 1 | const { XKeysWatcher } = require('xkeys') 2 | 3 | /* 4 | This example shows how multiple devices should be handled, using automaticUnitIdMode. 5 | 6 | The main reason to use automaticUnitIdMode is that it enables us to track re-connections of the same device, 7 | vs a new device being connected to the system. 8 | 9 | The best way to test how it works is to have 2 panels of the same type. Connect one, then disconnect it and 10 | notice the difference between reconnecting the same one vs a new one. 11 | 12 | To reset the unitId of the panels, run the reset-unitId.js example. 13 | */ 14 | 15 | /** A persistent memory to store data for connected panels */ 16 | const memory = {} 17 | 18 | // Set up the watcher for xkeys: 19 | const watcher = new XKeysWatcher({ 20 | automaticUnitIdMode: true, 21 | 22 | // If running on a system (such as some linux flavors) where the 'usb' library doesn't work, enable usePolling instead: 23 | // usePolling: true, 24 | // pollingInterval: 1000, 25 | }) 26 | watcher.on('error', (e) => { 27 | console.log('Error in XKeysWatcher', e) 28 | }) 29 | 30 | watcher.on('connected', (xkeysPanel) => { 31 | // This callback is called when a panel is initially connected. 32 | // It won't be called again on reconnection (use the 'reconnected' event instead). 33 | 34 | console.log(`A new X-keys panel of type ${xkeysPanel.info.name} connected`) 35 | 36 | const newName = 'HAL ' + (Object.keys(memory).length + 1) 37 | 38 | // Store the name in a persistent store: 39 | memory[xkeysPanel.uniqueId] = { 40 | name: newName, 41 | } 42 | console.log( 43 | `I'm going to call this panel "${newName}", it has productId=${xkeysPanel.info.productId}, unitId=${xkeysPanel.info.unitId}` 44 | ) 45 | 46 | xkeysPanel.on('disconnected', () => { 47 | console.log(`X-keys panel ${memory[xkeysPanel.uniqueId].name} was disconnected`) 48 | }) 49 | xkeysPanel.on('error', (...errs) => { 50 | console.log('X-keys error:', ...errs) 51 | }) 52 | 53 | xkeysPanel.on('reconnected', () => { 54 | console.log(`Hello again, ${memory[xkeysPanel.uniqueId].name}!`) 55 | }) 56 | 57 | // Listen to pressed buttons: 58 | xkeysPanel.on('down', (keyIndex, metadata) => { 59 | console.log(`Button ${keyIndex} pressed`) 60 | }) 61 | }) 62 | 63 | // To stop watching, call 64 | // watcher.stop().catch(console.error) 65 | -------------------------------------------------------------------------------- /packages/node/src/__mocks__/node-hid.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import type { Device } from 'node-hid' 3 | import { XKEYS_VENDOR_ID } from '..' 4 | 5 | let mockWriteHandler: undefined | ((hid: HIDAsync, message: number[]) => void) = undefined 6 | export function setMockWriteHandler(handler: (hid: HIDAsync, message: number[]) => void) { 7 | mockWriteHandler = handler 8 | } 9 | export function resetMockWriteHandler() { 10 | mockWriteHandler = undefined 11 | } 12 | let mockDevices: Device[] = [] 13 | export function mockSetDevices(devices: Device[]) { 14 | mockDevices = devices 15 | } 16 | 17 | // export class HID extends EventEmitter { 18 | export class HIDAsync extends EventEmitter { 19 | private mockWriteHandler 20 | 21 | static async open(path: string): Promise { 22 | return new HIDAsync(path) 23 | } 24 | 25 | private _deviceInfo: Device = { 26 | vendorId: XKEYS_VENDOR_ID, 27 | productId: 0, 28 | release: 0, 29 | interface: 0, 30 | product: 'N/A Mock', 31 | } 32 | 33 | constructor(path: string) { 34 | super() 35 | this.mockWriteHandler = mockWriteHandler 36 | 37 | const existingDevice = mockDevices.find((d) => d.path === path) 38 | if (existingDevice) { 39 | this._deviceInfo = existingDevice 40 | } 41 | } 42 | // constructor(vid: number, pid: number); 43 | async close(): Promise { 44 | // void 45 | } 46 | async pause(): Promise { 47 | // void 48 | throw new Error('Mock not implemented.') 49 | } 50 | async read(_timeOut?: number): Promise { 51 | return undefined 52 | } 53 | async sendFeatureReport(_data: number[]): Promise { 54 | return 0 55 | } 56 | async getFeatureReport(_reportIdd: number, _reportLength: number): Promise { 57 | return Buffer.alloc(0) 58 | } 59 | async resume(): Promise { 60 | // void 61 | throw new Error('Mock not implemented.') 62 | } 63 | async write(message: number[]): Promise { 64 | await this.mockWriteHandler?.(this, message) 65 | return 0 66 | } 67 | async setNonBlocking(_noBlock: boolean): Promise { 68 | // void 69 | throw new Error('Mock not implemented.') 70 | } 71 | 72 | async generateDeviceInfo(): Promise { 73 | // HACK: For typings 74 | return this.getDeviceInfo() 75 | } 76 | 77 | async getDeviceInfo(): Promise { 78 | return this._deviceInfo 79 | } 80 | } 81 | export function devices(): Device[] { 82 | return mockDevices 83 | } 84 | export function setDriverType(_type: 'hidraw' | 'libusb'): void { 85 | throw new Error('Mock not implemented.') 86 | // void 87 | } 88 | -------------------------------------------------------------------------------- /packages/node/examples/basic-log-all-events.js: -------------------------------------------------------------------------------- 1 | const { XKeysWatcher } = require('xkeys') 2 | 3 | /* 4 | This example connects to any connected x-keys panels and logs 5 | whenever a button is pressed or analog thing is moved 6 | */ 7 | 8 | // Set up the watcher for xkeys: 9 | const watcher = new XKeysWatcher({ 10 | // automaticUnitIdMode: false 11 | // usePolling: false 12 | // pollingInterval= 1000 13 | }) 14 | 15 | watcher.on('error', (e) => { 16 | console.log('Error in XKeysWatcher', e) 17 | }) 18 | watcher.on('connected', (xkeysPanel) => { 19 | console.log(`X-keys panel of type ${xkeysPanel.info.name} connected`) 20 | 21 | xkeysPanel.on('disconnected', () => { 22 | console.log(`X-keys panel of type ${xkeysPanel.info.name} was disconnected`) 23 | // Clean up stuff 24 | xkeysPanel.removeAllListeners() 25 | }) 26 | xkeysPanel.on('error', (...errs) => { 27 | console.log('X-keys error:', ...errs) 28 | }) 29 | 30 | // Listen to pressed buttons: 31 | xkeysPanel.on('down', (keyIndex, metadata) => { 32 | console.log('Button pressed ', keyIndex, metadata) 33 | 34 | // Light up a button when pressed: 35 | xkeysPanel.setBacklight(keyIndex, 'red') 36 | }) 37 | // Listen to released buttons: 38 | xkeysPanel.on('up', (keyIndex, metadata) => { 39 | console.log('Button released', keyIndex, metadata) 40 | 41 | // Turn off button light when released: 42 | xkeysPanel.setBacklight(keyIndex, false) 43 | }) 44 | 45 | // Listen to jog wheel changes: 46 | xkeysPanel.on('jog', (index, deltaPos, metadata) => { 47 | console.log(`Jog ${index} position has changed`, deltaPos, metadata) 48 | }) 49 | // Listen to shuttle changes: 50 | xkeysPanel.on('shuttle', (index, shuttlePos, metadata) => { 51 | console.log(`Shuttle ${index} position has changed`, shuttlePos, metadata) 52 | }) 53 | // Listen to joystick changes: 54 | 55 | xkeysPanel.on('joystick', (index, position, metadata) => { 56 | console.log(`Joystick ${index} position has changed`, position, metadata) // {x, y, z} 57 | }) 58 | // Listen to t-bar changes: 59 | xkeysPanel.on('tbar', (index, position, metadata) => { 60 | console.log(`T-bar ${index} position has changed`, position, metadata) 61 | }) 62 | // Listen to rotary changes: 63 | xkeysPanel.on('rotary', (index, position, metadata) => { 64 | console.log(`Rotary ${index} position has changed`, position, metadata) 65 | }) 66 | xkeysPanel.on('trackball', (index, position, metadata) => { 67 | console.log(`trackball ${index} position has changed`, position, metadata) // {x, y} 68 | }) 69 | }) 70 | 71 | // To stop watching, call 72 | // watcher.stop().catch(console.error) 73 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/lib.ts: -------------------------------------------------------------------------------- 1 | import * as HID from 'node-hid' 2 | 3 | /** Data sent to the panel */ 4 | let sentData: string[] = [] 5 | 6 | export function getSentData() { 7 | return sentData 8 | } 9 | 10 | export function handleXkeysMessages(hid: HID.HIDAsync, message: number[]) { 11 | // Replies to a few of the messages that are sent to the XKeys 12 | 13 | sentData.push(Buffer.from(message).toString('hex')) 14 | 15 | const firmVersion: number = 0 16 | const unitID: number = 0 17 | 18 | // Special case: 19 | if (message[1] === 214) { 20 | // getVersion 21 | // Reply with the version 22 | const data = Buffer.alloc(128) // length? 23 | data.writeUInt8(214, 1) 24 | data.writeUInt8(firmVersion, 10) 25 | hid.emit('data', data) 26 | return 27 | } 28 | let reply = false 29 | 30 | // Reply with full state: 31 | const values: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] // length? 32 | 33 | values[0] = unitID 34 | 35 | if (message[1] === 177) { 36 | // generateData 37 | values[1] += 2 // set the genData flag 38 | reply = true 39 | } 40 | 41 | if (reply) { 42 | const data = Buffer.alloc(128) // length? 43 | values.forEach((value, index) => { 44 | data.writeUInt8(value, index) 45 | }) 46 | hid.emit('data', data) 47 | } 48 | } 49 | export function resetSentData() { 50 | sentData = [] 51 | } 52 | /** Like sleep() but 1ms at a time, allows for the event loop to run promises, etc.. */ 53 | export async function sleepTicks(ms: number) { 54 | for (let i = 0; i < ms; i++) { 55 | await sleep(1) 56 | } 57 | } 58 | 59 | export async function sleep(ms: number) { 60 | return new Promise((resolve) => setTimeout(resolve, ms)) 61 | } 62 | 63 | declare global { 64 | namespace jest { 65 | interface Matchers { 66 | toBeObject(): R 67 | toBeWithinRange(a: number, b: number): R 68 | } 69 | } 70 | } 71 | expect.extend({ 72 | toBeObject(received) { 73 | return { 74 | message: () => `expected ${received} to be an object`, 75 | pass: typeof received == 'object', 76 | } 77 | }, 78 | toBeWithinRange(received, floor, ceiling) { 79 | if (typeof received !== 'number') { 80 | return { 81 | message: () => `expected ${received} to be a number`, 82 | pass: false, 83 | } 84 | } 85 | const pass = received >= floor && received <= ceiling 86 | if (pass) { 87 | return { 88 | message: () => `expected ${received} not to be within range ${floor} - ${ceiling}`, 89 | pass: true, 90 | } 91 | } else { 92 | return { 93 | message: () => `expected ${received} to be within range ${floor} - ${ceiling}`, 94 | pass: false, 95 | } 96 | } 97 | }, 98 | }) 99 | -------------------------------------------------------------------------------- /.github/workflows/publish-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Publish nightly to NPM 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Allows you to run this workflow manually from the Actions tab 6 | workflow_dispatch: 7 | 8 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 9 | jobs: 10 | publish: 11 | name: Publish to NPM (nightly) 12 | runs-on: ubuntu-latest 13 | continue-on-error: false 14 | timeout-minutes: 15 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js 16.x 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 16.x 22 | - name: Check if token is set 23 | id: check-npm-token 24 | run: | 25 | if [ -z "${{ secrets.NPM_TOKEN }}" ]; then 26 | echo "env NPM_TOKEN not set!" 27 | else 28 | echo "is-ok="1"" >> $GITHUB_OUTPUT 29 | fi 30 | - name: Prepare Environment 31 | if: ${{ steps.check-npm-token.outputs.is-ok }} 32 | run: | 33 | yarn 34 | env: 35 | CI: true 36 | - name: Get the Prerelease tag 37 | id: prerelease-tag 38 | uses: yuya-takeyama/docker-tag-from-github-ref-action@2b0614b1338c8f19dd9d3ea433ca9bc0cc7057ba 39 | with: 40 | remove-version-tag-prefix: false 41 | - name: Bump version to nightly 42 | if: ${{ steps.check-npm-token.outputs.is-ok }} 43 | run: | 44 | COMMIT_TIMESTAMP=$(git log -1 --pretty=format:%ct HEAD) 45 | COMMIT_DATE=$(date -d @$COMMIT_TIMESTAMP +%Y%m%d-%H%M%S) 46 | GIT_HASH=$(git rev-parse --short HEAD) 47 | PRERELEASE_TAG=0.0.0-nightly-$(echo "${{ steps.prerelease-tag.outputs.tag }}" | sed -r 's/[^a-z0-9]+/-/gi') 48 | yarn lerna:version $PRERELEASE_TAG-$COMMIT_DATE-$GIT_HASH --force-publish=* --no-changelog --no-push --no-git-tag-version --yes 49 | env: 50 | CI: true 51 | - name: Build 52 | if: ${{ steps.check-npm-token.outputs.is-ok }} 53 | run: | 54 | yarn build 55 | env: 56 | CI: true 57 | - name: Set .npmrc file 58 | if: ${{ steps.check-npm-token.outputs.is-ok }} 59 | run: | 60 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc 61 | npm whoami 62 | - name: Git commit 63 | if: ${{ steps.check-npm-token.outputs.is-ok }} 64 | run: | 65 | git config --global user.email "ci@github.com" 66 | git config --global user.name "Github CI" 67 | git add -A 68 | git commit -m "Make lerna happy" 69 | env: 70 | CI: true 71 | - name: Publish nightly to NPM 72 | if: ${{ steps.check-npm-token.outputs.is-ok }} 73 | run: yarn lerna:publish from-package --dist-tag nightly --no-verify-access --yes 74 | env: 75 | CI: true 76 | -------------------------------------------------------------------------------- /packages/webhid/src/globalConnectListener.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This class is used to register listener for connect and disconnect events for HID devices. 3 | * It allows for a few clever tricks, such as 4 | * * listenForDisconnectOnce() listens for a disconnect event for a specific device, and then removes the listener. 5 | * * handles a special case where the 'connect' event isn't fired when adding permissions for a HID device. 6 | */ 7 | export class GlobalConnectListener { 8 | private static anyConnectListeners = new Set<() => void>() 9 | private static anyDisconnectListeners = new Set<() => void>() 10 | private static disconnectListenersOnce = new Map void>() 11 | 12 | private static isSetup = false 13 | 14 | /** Add listener for any connect event */ 15 | static listenForAnyConnect(callback: () => void): { stop: () => void } { 16 | this.setup() 17 | this.anyConnectListeners.add(callback) 18 | return { 19 | stop: () => this.anyConnectListeners.delete(callback), 20 | } 21 | } 22 | /** Add listener for any disconnect event */ 23 | static listenForAnyDisconnect(callback: () => void): { stop: () => void } { 24 | this.setup() 25 | this.anyDisconnectListeners.add(callback) 26 | return { 27 | stop: () => this.anyDisconnectListeners.delete(callback), 28 | } 29 | } 30 | 31 | /** Add listener for disconnect event, for a HIDDevice. The callback will be fired once. */ 32 | static listenForDisconnectOnce(device: HIDDevice, callback: () => void): void { 33 | this.setup() 34 | this.disconnectListenersOnce.set(device, callback) 35 | } 36 | 37 | static notifyConnectedDevice(): void { 38 | this.handleConnect() 39 | } 40 | 41 | private static setup() { 42 | if (this.isSetup) return 43 | navigator.hid.addEventListener('disconnect', this.handleDisconnect) 44 | navigator.hid.addEventListener('connect', this.handleConnect) 45 | this.isSetup = true 46 | } 47 | private static handleDisconnect = (ev: HIDConnectionEvent) => { 48 | this.anyDisconnectListeners.forEach((callback) => callback()) 49 | 50 | this.disconnectListenersOnce.forEach((callback, device) => { 51 | if (device === ev.device) { 52 | callback() 53 | // Also remove the listener: 54 | this.disconnectListenersOnce.delete(device) 55 | } 56 | }) 57 | 58 | this.maybeTeardown() 59 | } 60 | private static handleConnect = () => { 61 | this.anyConnectListeners.forEach((callback) => callback()) 62 | } 63 | private static maybeTeardown() { 64 | if ( 65 | this.disconnectListenersOnce.size === 0 && 66 | this.anyDisconnectListeners.size === 0 && 67 | this.anyConnectListeners.size === 0 68 | ) { 69 | // If there are no listeners, we can teardown the global listener: 70 | this.teardown() 71 | } 72 | } 73 | private static teardown() { 74 | navigator.hid.removeEventListener('disconnect', this.handleDisconnect) 75 | navigator.hid.removeEventListener('connect', this.handleConnect) 76 | this.disconnectListenersOnce.clear() 77 | this.isSetup = false 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | 3 | # Controls when the action will run. 4 | on: 5 | push: 6 | branches: 7 | - '**' 8 | tags: 9 | - 'v**' 10 | pull_request: 11 | 12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 13 | jobs: 14 | lint: 15 | name: Linting 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 10 18 | steps: 19 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 20 | - uses: actions/checkout@v3 21 | - name: Use Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 16 25 | - name: Cache node_modules 26 | uses: actions/cache@v3 27 | with: 28 | path: '**/node_modules' 29 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 30 | 31 | - name: Prepare Environment 32 | run: | 33 | yarn 34 | yarn build 35 | env: 36 | CI: true 37 | - name: Run Linting 38 | run: | 39 | yarn lint 40 | env: 41 | CI: true 42 | 43 | # GitHub Action to automate the identification of common misspellings in text files. 44 | # https://github.com/codespell-project/actions-codespell 45 | # https://github.com/codespell-project/codespell 46 | codespell: 47 | name: Check for spelling errors 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v3 51 | with: 52 | persist-credentials: false 53 | - uses: codespell-project/actions-codespell@master 54 | with: 55 | check_filenames: true 56 | skip: "./.git,./yarn.lock" 57 | ignore_words_list: ans 58 | 59 | test: 60 | name: Test on node ${{ matrix.node_version }} and ${{ matrix.os }} 61 | runs-on: ${{ matrix.os }} 62 | strategy: 63 | matrix: 64 | node_version: ['14', '16', '18', '20'] 65 | os: [ubuntu-latest] # [windows-latest, macOS-latest] 66 | timeout-minutes: 10 67 | steps: 68 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 69 | - uses: actions/checkout@v3 70 | - name: Use Node.js ${{ matrix.node_version }} 71 | uses: actions/setup-node@v3 72 | with: 73 | node-version: ${{ matrix.node_version }} 74 | - name: Cache node_modules 75 | uses: actions/cache@v3 76 | with: 77 | path: '**/node_modules' 78 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 79 | 80 | - name: Prepare Environment 81 | if: matrix.node_version != 10 82 | run: | 83 | yarn 84 | yarn build 85 | env: 86 | CI: true 87 | - name: Prepare Environment (Node 10) 88 | if: matrix.node_version == 10 89 | run: | 90 | sudo apt-get update 91 | sudo apt-get install libudev-dev 92 | 93 | # yarn --prod 94 | 95 | yarn --ignore-engines 96 | yarn build 97 | env: 98 | CI: true 99 | 100 | - name: Run unit tests 101 | run: | 102 | yarn test 103 | env: 104 | CI: true 105 | -------------------------------------------------------------------------------- /.github/workflows/publish-prerelease.yml: -------------------------------------------------------------------------------- 1 | name: Publish Pre-release-version to NPM 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Allows you to run this workflow manually from the Actions tab 6 | workflow_dispatch: 7 | 8 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 9 | jobs: 10 | test: 11 | name: Test on node ${{ matrix.node_version }} and ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | node_version: ['14', '16', '18', '20'] 16 | os: [ubuntu-latest] # [windows-latest, macOS-latest] 17 | timeout-minutes: 10 18 | steps: 19 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 20 | - uses: actions/checkout@v3 21 | - name: Use Node.js ${{ matrix.node_version }} 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ matrix.node_version }} 25 | - name: Cache node_modules 26 | uses: actions/cache@v3 27 | with: 28 | path: '**/node_modules' 29 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 30 | 31 | - name: Prepare Environment 32 | if: matrix.node_version != 10 33 | run: | 34 | yarn 35 | yarn build 36 | env: 37 | CI: true 38 | - name: Prepare Environment (Node 10) 39 | if: matrix.node_version == 10 40 | run: | 41 | sudo apt-get update 42 | sudo apt-get install libudev-dev 43 | 44 | # yarn --prod 45 | 46 | yarn --ignore-engines 47 | yarn build 48 | env: 49 | CI: true 50 | 51 | - name: Run unit tests 52 | run: | 53 | yarn test 54 | env: 55 | CI: true 56 | 57 | publish: 58 | name: Publish to NPM (pre-release) 59 | runs-on: ubuntu-latest 60 | continue-on-error: false 61 | timeout-minutes: 15 62 | 63 | needs: 64 | - test 65 | 66 | steps: 67 | - uses: actions/checkout@v3 68 | - name: Use Node.js 16.x 69 | uses: actions/setup-node@v3 70 | with: 71 | node-version: 16.x 72 | - name: Check if token is set 73 | id: check-npm-token 74 | run: | 75 | if [ -z "${{ secrets.NPM_TOKEN }}" ]; then 76 | echo "env NPM_TOKEN not set!" 77 | else 78 | echo "is-ok="1"" >> $GITHUB_OUTPUT 79 | fi 80 | - name: Prepare Environment 81 | if: ${{ steps.check-npm-token.outputs.is-ok }} 82 | run: | 83 | yarn 84 | env: 85 | CI: true 86 | - name: Build 87 | if: ${{ steps.check-npm-token.outputs.is-ok }} 88 | run: | 89 | yarn build 90 | env: 91 | CI: true 92 | - name: Set .npmrc file 93 | if: ${{ steps.check-npm-token.outputs.is-ok }} 94 | run: | 95 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc 96 | npm whoami 97 | - name: Publish prerelease to NPM 98 | if: ${{ steps.check-npm-token.outputs.is-ok }} 99 | run: yarn lerna:publish from-package --dist-tag prerelease --no-verify-access --yes 100 | env: 101 | CI: true 102 | -------------------------------------------------------------------------------- /packages/webhid/src/methods.ts: -------------------------------------------------------------------------------- 1 | import { XKeys, XKEYS_VENDOR_ID } from '@xkeys-lib/core' 2 | import { WebHIDDevice } from './web-hid-wrapper' 3 | import { GlobalConnectListener } from './globalConnectListener' 4 | 5 | /** Prompts the user for which X-keys panel to select */ 6 | export async function requestXkeysPanels(): Promise { 7 | const allDevices = await navigator.hid.requestDevice({ 8 | filters: [ 9 | { 10 | vendorId: XKEYS_VENDOR_ID, 11 | }, 12 | ], 13 | }) 14 | const newDevices = allDevices.filter(isValidXkeysUsage) 15 | 16 | if (newDevices.length > 0) GlobalConnectListener.notifyConnectedDevice() // A fix for when the 'connect' event isn't fired 17 | return newDevices 18 | } 19 | /** 20 | * Reopen previously selected devices. 21 | * The browser remembers what the user previously allowed your site to access, and this will open those without the request dialog 22 | */ 23 | export async function getOpenedXKeysPanels(): Promise { 24 | const allDevices = await navigator.hid.getDevices() 25 | return allDevices.filter(isValidXkeysUsage) 26 | } 27 | 28 | function isValidXkeysUsage(device: HIDDevice): boolean { 29 | if (device.vendorId !== XKEYS_VENDOR_ID) return false 30 | 31 | return !!device.collections.find((collection) => { 32 | if (collection.usagePage !== 12) return false 33 | 34 | // Check the write-length of the device is > 20 35 | return !!collection.outputReports?.find((report) => !!report.items?.find((item) => item.reportCount ?? 0 > 20)) 36 | }) 37 | } 38 | 39 | /** Sets up a connection to a HID device (the X-keys panel) */ 40 | export async function setupXkeysPanel(browserDevice: HIDDevice): Promise { 41 | if (!browserDevice?.collections?.length) throw Error(`device collections is empty`) 42 | if (!isValidXkeysUsage(browserDevice)) throw new Error(`Device has incorrect usage/interface`) 43 | if (!browserDevice.productId) throw Error(`Device has no productId!`) 44 | 45 | const vendorId = browserDevice.vendorId 46 | const productId = browserDevice.productId 47 | 48 | if (!browserDevice.opened) { 49 | await browserDevice.open() 50 | } 51 | 52 | const deviceWrap = new WebHIDDevice(browserDevice) 53 | 54 | const xkeys = new XKeys( 55 | deviceWrap, 56 | { 57 | product: browserDevice.productName, 58 | vendorId: vendorId, 59 | productId: productId, 60 | interface: null, // todo: Check what to use here (collection.usage?) 61 | }, 62 | undefined 63 | ) 64 | 65 | // Setup listener for disconnect: 66 | GlobalConnectListener.listenForDisconnectOnce(browserDevice, () => { 67 | xkeys._handleDeviceDisconnected().catch((e) => { 68 | console.error(`Xkeys: Error handling disconnect:`, e) 69 | }) 70 | }) 71 | 72 | let alreadyRejected = false 73 | try { 74 | await new Promise((resolve, reject) => { 75 | const markRejected = (e: unknown) => { 76 | reject(e) 77 | alreadyRejected = true 78 | } 79 | const xkeysStopgapErrorHandler = (e: unknown) => { 80 | if (alreadyRejected) { 81 | console.error(`Xkeys: Error emitted after setup already rejected:`, e) 82 | return 83 | } 84 | 85 | markRejected(e) 86 | } 87 | 88 | // Handle all error events until the instance is returned 89 | xkeys.on('error', xkeysStopgapErrorHandler) 90 | 91 | // Wait for the device to initialize: 92 | xkeys 93 | .init() 94 | .then(() => { 95 | resolve() 96 | xkeys.removeListener('error', xkeysStopgapErrorHandler) 97 | }) 98 | .catch(markRejected) 99 | }) 100 | 101 | return xkeys 102 | } catch (e) { 103 | await deviceWrap.close() 104 | throw e 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /packages/webhid-demo/src/app.ts: -------------------------------------------------------------------------------- 1 | import { requestXkeysPanels, XKeys, XKeysWatcher } from 'xkeys-webhid' 2 | 3 | const connectedXkeys = new Set() 4 | 5 | function appendLog(str: string) { 6 | const logElm = document.getElementById('log') 7 | if (logElm) { 8 | logElm.textContent = `${str}\n${logElm.textContent}` 9 | } 10 | } 11 | 12 | function initialize() { 13 | // Set up the watcher for xkeys: 14 | const watcher = new XKeysWatcher({ 15 | // automaticUnitIdMode: false 16 | // usePolling: true, 17 | // pollingInterval= 1000 18 | }) 19 | watcher.on('error', (e) => { 20 | appendLog(`Error in XkeysWatcher: ${e}`) 21 | }) 22 | watcher.on('connected', (xkeys) => { 23 | connectedXkeys.add(xkeys) 24 | 25 | const id = xkeys.info.name 26 | 27 | appendLog(`${id}: Connected`) 28 | 29 | xkeys.on('disconnected', () => { 30 | appendLog(`${id}: Disconnected`) 31 | // Clean up stuff: 32 | xkeys.removeAllListeners() 33 | 34 | connectedXkeys.delete(xkeys) 35 | updateDeviceList() 36 | }) 37 | xkeys.on('error', (...errs) => { 38 | appendLog(`${id}: X-keys error: ${errs.join(',')}`) 39 | }) 40 | xkeys.on('down', (keyIndex: number) => { 41 | appendLog(`${id}: Button ${keyIndex} down`) 42 | xkeys.setBacklight(keyIndex, 'blue') 43 | }) 44 | xkeys.on('up', (keyIndex: number) => { 45 | appendLog(`${id}: Button ${keyIndex} up`) 46 | xkeys.setBacklight(keyIndex, null) 47 | }) 48 | xkeys.on('jog', (index, value) => { 49 | appendLog(`${id}: Jog #${index}: ${value}`) 50 | }) 51 | xkeys.on('joystick', (index, value) => { 52 | appendLog(`${id}: Joystick #${index}: ${JSON.stringify(value)}`) 53 | }) 54 | xkeys.on('shuttle', (index, value) => { 55 | appendLog(`${id}: Shuttle #${index}: ${value}`) 56 | }) 57 | xkeys.on('tbar', (index, value) => { 58 | appendLog(`${id}: T-bar #${index}: ${value}`) 59 | }) 60 | 61 | updateDeviceList() 62 | }) 63 | window.addEventListener('load', () => { 64 | appendLog('Page loaded') 65 | 66 | if (!navigator.hid) { 67 | appendLog('>>>>> WebHID not supported in this browser <<<<<') 68 | return 69 | } 70 | }) 71 | 72 | const consentButton = document.getElementById('consent-button') 73 | consentButton?.addEventListener('click', () => { 74 | // Prompt for a device 75 | 76 | appendLog('Asking user for permissions...') 77 | requestXkeysPanels() 78 | .then((devices) => { 79 | if (devices.length === 0) { 80 | appendLog('No device was selected') 81 | } else { 82 | for (const device of devices) { 83 | appendLog(`Access granted to "${device.productName}"`) 84 | } 85 | // Note The XKeysWatcher will now pick up the device automatically 86 | } 87 | }) 88 | .catch((error) => { 89 | appendLog(`No device access granted: ${error}`) 90 | }) 91 | }) 92 | } 93 | 94 | function updateDeviceList() { 95 | // Update the list of connected devices: 96 | 97 | const container = document.getElementById('devices') 98 | if (container) { 99 | container.innerHTML = '' 100 | 101 | if (connectedXkeys.size === 0) { 102 | container.innerHTML = 'No devices connected' 103 | } else { 104 | connectedXkeys.forEach((xkeys) => { 105 | const div = document.createElement('div') 106 | div.innerHTML = ` 107 | ${xkeys.info.name} 108 | ` 109 | const button = document.createElement('button') 110 | button.innerText = 'Close device' 111 | button.addEventListener('click', () => { 112 | appendLog(xkeys.info.name + ' Closing device') 113 | xkeys.close().catch(console.error) 114 | }) 115 | div.appendChild(button) 116 | 117 | container.appendChild(div) 118 | }) 119 | } 120 | } 121 | } 122 | 123 | initialize() 124 | -------------------------------------------------------------------------------- /packages/webhid-demo/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [3.3.0](https://github.com/SuperFlyTV/xkeys/compare/v3.2.0...v3.3.0) (2024-12-09) 7 | 8 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 9 | 10 | 11 | 12 | 13 | 14 | # [3.2.0](https://github.com/SuperFlyTV/xkeys/compare/v3.1.2...v3.2.0) (2024-08-26) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * add 'disconnect' event for the webHID device ([44179d3](https://github.com/SuperFlyTV/xkeys/commit/44179d374bccf730bd0caf9fee6605359f48cf03)) 20 | 21 | 22 | 23 | 24 | 25 | ## [3.1.2](https://github.com/SuperFlyTV/xkeys/compare/v3.1.1...v3.1.2) (2024-08-12) 26 | 27 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 28 | 29 | 30 | 31 | 32 | 33 | ## [3.1.1](https://github.com/SuperFlyTV/xkeys/compare/v3.1.0...v3.1.1) (2024-03-04) 34 | 35 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 36 | 37 | 38 | 39 | 40 | 41 | # [3.1.0](https://github.com/SuperFlyTV/xkeys/compare/v3.0.1...v3.1.0) (2024-01-11) 42 | 43 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 44 | 45 | 46 | 47 | 48 | 49 | ## [3.0.1](https://github.com/SuperFlyTV/xkeys/compare/v3.0.0...v3.0.1) (2023-11-02) 50 | 51 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 52 | 53 | 54 | 55 | 56 | 57 | # [3.0.0](https://github.com/SuperFlyTV/xkeys/compare/v2.4.0...v3.0.0) (2023-05-03) 58 | 59 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 60 | 61 | # [2.4.0](https://github.com/SuperFlyTV/xkeys/compare/v2.3.4...v2.4.0) (2022-10-26) 62 | 63 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 64 | 65 | ## [2.3.4](https://github.com/SuperFlyTV/xkeys/compare/v2.3.3...v2.3.4) (2022-06-06) 66 | 67 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 68 | 69 | ## [2.3.4](https://github.com/SuperFlyTV/xkeys/compare/v2.3.3...v2.3.4) (2022-06-06) 70 | 71 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 72 | 73 | ## [2.3.2](https://github.com/SuperFlyTV/xkeys/compare/v2.3.0...v2.3.2) (2021-12-12) 74 | 75 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 76 | 77 | # [2.3.0](https://github.com/SuperFlyTV/xkeys/compare/v2.2.1...v2.3.0) (2021-11-28) 78 | 79 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 80 | 81 | ## [2.2.1](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0...v2.2.1) (2021-09-22) 82 | 83 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 84 | 85 | # [2.2.0](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0-alpha.1...v2.2.0) (2021-09-08) 86 | 87 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 88 | 89 | # [2.2.0-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0-alpha.0...v2.2.0-alpha.1) (2021-09-06) 90 | 91 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 92 | 93 | # [2.2.0-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1...v2.2.0-alpha.0) (2021-09-06) 94 | 95 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 96 | 97 | ## [2.1.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1-alpha.1...v2.1.1) (2021-05-24) 98 | 99 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 100 | 101 | ## [2.1.1-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0...v2.1.1-alpha.0) (2021-05-23) 102 | 103 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 104 | 105 | # [2.1.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0-alpha.0...v2.1.0) (2021-05-15) 106 | 107 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 108 | 109 | # [2.1.0-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0-alpha.0...v2.1.0-alpha.1) (2021-05-10) 110 | 111 | **Note:** Version bump only for package @xkeys-lib/webhid-demo 112 | 113 | # [2.1.0-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.0.0...v2.1.0-alpha.0) (2021-05-10) 114 | 115 | ### Features 116 | 117 | - add package with web-HID support ([1f27199](https://github.com/SuperFlyTV/xkeys/commit/1f2719969faf93ba45a2bc767f64543fb9ffe6ea)) 118 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release-version to NPM 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Allows you to run this workflow manually from the Actions tab 6 | workflow_dispatch: 7 | 8 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 9 | jobs: 10 | lint: 11 | name: Linting 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 10 14 | steps: 15 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 16 | - uses: actions/checkout@v3 17 | - name: Use Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 16 21 | - name: Cache node_modules 22 | uses: actions/cache@v3 23 | with: 24 | path: '**/node_modules' 25 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 26 | 27 | - name: Prepare Environment 28 | run: | 29 | yarn 30 | yarn build 31 | env: 32 | CI: true 33 | - name: Run Linting 34 | run: | 35 | yarn lint 36 | env: 37 | CI: true 38 | 39 | test: 40 | name: Test on node ${{ matrix.node_version }} and ${{ matrix.os }} 41 | runs-on: ${{ matrix.os }} 42 | strategy: 43 | matrix: 44 | node_version: ['14', '16', '18', '20'] 45 | os: [ubuntu-latest] # [windows-latest, macOS-latest] 46 | timeout-minutes: 10 47 | steps: 48 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 49 | - uses: actions/checkout@v3 50 | - name: Use Node.js ${{ matrix.node_version }} 51 | uses: actions/setup-node@v3 52 | with: 53 | node-version: ${{ matrix.node_version }} 54 | - name: Cache node_modules 55 | uses: actions/cache@v3 56 | with: 57 | path: '**/node_modules' 58 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 59 | 60 | - name: Prepare Environment 61 | if: matrix.node_version != 10 62 | run: | 63 | yarn 64 | yarn build 65 | env: 66 | CI: true 67 | - name: Prepare Environment (Node 10) 68 | if: matrix.node_version == 10 69 | run: | 70 | sudo apt-get update 71 | sudo apt-get install libudev-dev 72 | 73 | # yarn --prod 74 | 75 | yarn --ignore-engines 76 | yarn build 77 | env: 78 | CI: true 79 | 80 | - name: Run unit tests 81 | run: | 82 | yarn test 83 | env: 84 | CI: true 85 | 86 | publish: 87 | name: Publish to NPM 88 | runs-on: ubuntu-latest 89 | continue-on-error: false 90 | timeout-minutes: 15 91 | 92 | # only run on master 93 | if: github.ref == 'refs/heads/master' 94 | 95 | needs: 96 | - lint 97 | - test 98 | 99 | steps: 100 | - uses: actions/checkout@v3 101 | - name: Use Node.js 16.x 102 | uses: actions/setup-node@v3 103 | with: 104 | node-version: 16.x 105 | - name: Check if token is set 106 | id: check-npm-token 107 | run: | 108 | if [ -z "${{ secrets.NPM_TOKEN }}" ]; then 109 | echo "env NPM_TOKEN not set!" 110 | else 111 | echo "is-ok="1"" >> $GITHUB_OUTPUT 112 | fi 113 | - name: Prepare Environment 114 | if: ${{ steps.check-npm-token.outputs.is-ok }} 115 | run: | 116 | yarn 117 | env: 118 | CI: true 119 | - name: Build 120 | if: ${{ steps.check-npm-token.outputs.is-ok }} 121 | run: | 122 | yarn build 123 | env: 124 | CI: true 125 | - name: Set .npmrc file 126 | if: ${{ steps.check-npm-token.outputs.is-ok }} 127 | run: | 128 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc 129 | npm whoami 130 | - name: Publish to NPM 131 | if: ${{ steps.check-npm-token.outputs.is-ok }} 132 | run: yarn lerna:publish from-package --no-verify-access --yes 133 | env: 134 | CI: true 135 | -------------------------------------------------------------------------------- /packages/node/src/watcher.ts: -------------------------------------------------------------------------------- 1 | import type { usb } from 'usb' 2 | import { XKeys, XKEYS_VENDOR_ID, GenericXKeysWatcher, XKeysWatcherOptions } from '@xkeys-lib/core' 3 | import { listAllConnectedPanels, setupXkeysPanel } from '.' 4 | 5 | /** 6 | * Set up a watcher for newly connected X-keys panels. 7 | * Note: It is highly recommended to set up a listener for the disconnected event on the X-keys panel, to clean up after a disconnected device. 8 | */ 9 | export class XKeysWatcher extends GenericXKeysWatcher { 10 | private pollingInterval: NodeJS.Timeout | undefined = undefined 11 | 12 | constructor(options?: XKeysWatcherOptions) { 13 | super(options) 14 | 15 | if (!this.options.usePolling) { 16 | // Watch for added devices: 17 | USBImport.USBDetect().on('attach', this.onAddedUSBDevice) 18 | USBImport.USBDetect().on('detach', this.onRemovedUSBDevice) 19 | } else { 20 | this.pollingInterval = setInterval(() => { 21 | this.triggerUpdateConnectedDevices(false) 22 | }, this.options.pollingInterval) 23 | } 24 | } 25 | /** 26 | * Stop the watcher 27 | * @param closeAllDevices Set to false in order to NOT close all devices. Use this if you only want to stop the watching. Defaults to true 28 | */ 29 | public async stop(closeAllDevices = true): Promise { 30 | if (!this.options.usePolling) { 31 | // Remove the listeners: 32 | USBImport.USBDetect().off('attach', this.onAddedUSBDevice) 33 | USBImport.USBDetect().off('detach', this.onRemovedUSBDevice) 34 | } 35 | 36 | if (this.pollingInterval) { 37 | clearInterval(this.pollingInterval) 38 | this.pollingInterval = undefined 39 | } 40 | 41 | await super.stop(closeAllDevices) 42 | } 43 | 44 | protected async getConnectedDevices(): Promise> { 45 | // Returns a Set of devicePaths of the connected devices 46 | const connectedDevices = new Set() 47 | 48 | for (const xkeysDevice of listAllConnectedPanels()) { 49 | if (xkeysDevice.path) { 50 | connectedDevices.add(xkeysDevice.path) 51 | } else { 52 | this.emit('error', `XKeysWatcher: Device missing path.`) 53 | } 54 | } 55 | return connectedDevices 56 | } 57 | protected async setupXkeysPanel(devicePath: string): Promise { 58 | return setupXkeysPanel(devicePath) 59 | } 60 | private onAddedUSBDevice = (device: usb.Device) => { 61 | // Called whenever a new USB device is added 62 | // Note: 63 | // There isn't a good way to relate the output from usb to node-hid devices 64 | // So we're just using the events to trigger a re-check for new devices and cache the seen devices 65 | if (!this.isActive) return 66 | if (device.deviceDescriptor.idVendor !== XKEYS_VENDOR_ID) return 67 | 68 | this.debugLog('onAddedUSBDevice') 69 | this.triggerUpdateConnectedDevices(true) 70 | } 71 | private onRemovedUSBDevice = (device: usb.Device) => { 72 | // Called whenever a new USB device is removed 73 | 74 | if (!this.isActive) return 75 | if (device.deviceDescriptor.idVendor !== XKEYS_VENDOR_ID) return 76 | this.debugLog('onRemovedUSBDevice') 77 | 78 | this.triggerUpdateConnectedDevices(true) 79 | } 80 | } 81 | 82 | class USBImport { 83 | private static USBImport: typeof usb | undefined 84 | private static hasTriedImport = false 85 | // Because usb is an optional dependency, we have to use in a somewhat messy way: 86 | static USBDetect(): typeof usb { 87 | if (this.USBImport) return this.USBImport 88 | 89 | if (!this.hasTriedImport) { 90 | this.hasTriedImport = true 91 | try { 92 | // eslint-disable-next-line @typescript-eslint/no-var-requires 93 | const usb: typeof import('usb') = require('usb') 94 | this.USBImport = usb.usb 95 | return this.USBImport 96 | } catch (err) { 97 | // It's not installed 98 | } 99 | } 100 | // else emit error: 101 | throw `XKeysWatcher requires the dependency "usb" to be installed, it might have been skipped due to your platform being unsupported (this is an issue with "usb", not the X-keys library). 102 | Possible solutions are: 103 | * You can try to install the dependency manually, by running "npm install usb". 104 | * Use the fallback "usePolling" functionality instead: new XKeysWatcher({ usePolling: true}) 105 | * Otherwise you can still connect to X-keys panels manually by using XKeys.setupXkeysPanel(). 106 | ` 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /packages/node-record-test/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [3.3.0](https://github.com/SuperFlyTV/xkeys/compare/v3.2.0...v3.3.0) (2024-12-09) 7 | 8 | **Note:** Version bump only for package @xkeys-lib/record-test 9 | 10 | 11 | 12 | 13 | 14 | # [3.2.0](https://github.com/SuperFlyTV/xkeys/compare/v3.1.2...v3.2.0) (2024-08-26) 15 | 16 | **Note:** Version bump only for package @xkeys-lib/record-test 17 | 18 | 19 | 20 | 21 | 22 | ## [3.1.2](https://github.com/SuperFlyTV/xkeys/compare/v3.1.1...v3.1.2) (2024-08-12) 23 | 24 | **Note:** Version bump only for package @xkeys-lib/record-test 25 | 26 | 27 | 28 | 29 | 30 | ## [3.1.1](https://github.com/SuperFlyTV/xkeys/compare/v3.1.0...v3.1.1) (2024-03-04) 31 | 32 | **Note:** Version bump only for package @xkeys-lib/record-test 33 | 34 | 35 | 36 | 37 | 38 | # [3.1.0](https://github.com/SuperFlyTV/xkeys/compare/v3.0.1...v3.1.0) (2024-01-11) 39 | 40 | **Note:** Version bump only for package @xkeys-lib/record-test 41 | 42 | 43 | 44 | 45 | 46 | ## [3.0.1](https://github.com/SuperFlyTV/xkeys/compare/v3.0.0...v3.0.1) (2023-11-02) 47 | 48 | **Note:** Version bump only for package @xkeys-lib/record-test 49 | 50 | 51 | 52 | 53 | 54 | # [3.0.0](https://github.com/SuperFlyTV/xkeys/compare/v2.4.0...v3.0.0) (2023-05-03) 55 | 56 | **Note:** Version bump only for package @xkeys-lib/record-test 57 | 58 | # [2.4.0](https://github.com/SuperFlyTV/xkeys/compare/v2.3.4...v2.4.0) (2022-10-26) 59 | 60 | **Note:** Version bump only for package @xkeys-lib/record-test 61 | 62 | ## [2.3.4](https://github.com/SuperFlyTV/xkeys/compare/v2.3.3...v2.3.4) (2022-06-06) 63 | 64 | **Note:** Version bump only for package @xkeys-lib/record-test 65 | 66 | ## [2.3.4](https://github.com/SuperFlyTV/xkeys/compare/v2.3.3...v2.3.4) (2022-06-06) 67 | 68 | **Note:** Version bump only for package @xkeys-lib/record-test 69 | 70 | ## [2.3.2](https://github.com/SuperFlyTV/xkeys/compare/v2.3.0...v2.3.2) (2021-12-12) 71 | 72 | **Note:** Version bump only for package @xkeys-lib/record-test 73 | 74 | # [2.3.0](https://github.com/SuperFlyTV/xkeys/compare/v2.2.1...v2.3.0) (2021-11-28) 75 | 76 | **Note:** Version bump only for package @xkeys-lib/record-test 77 | 78 | ## [2.2.1](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0...v2.2.1) (2021-09-22) 79 | 80 | **Note:** Version bump only for package @xkeys-lib/record-test 81 | 82 | # [2.2.0](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0-alpha.1...v2.2.0) (2021-09-08) 83 | 84 | **Note:** Version bump only for package @xkeys-lib/record-test 85 | 86 | # [2.2.0-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0-alpha.0...v2.2.0-alpha.1) (2021-09-06) 87 | 88 | **Note:** Version bump only for package @xkeys-lib/record-test 89 | 90 | # [2.2.0-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1...v2.2.0-alpha.0) (2021-09-06) 91 | 92 | **Note:** Version bump only for package @xkeys-lib/record-test 93 | 94 | ## [2.1.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1-alpha.1...v2.1.1) (2021-05-24) 95 | 96 | **Note:** Version bump only for package @xkeys-lib/record-test 97 | 98 | ## [2.1.1-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1-alpha.0...v2.1.1-alpha.1) (2021-05-23) 99 | 100 | **Note:** Version bump only for package @xkeys-lib/record-test 101 | 102 | ## [2.1.1-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0...v2.1.1-alpha.0) (2021-05-23) 103 | 104 | **Note:** Version bump only for package @xkeys-lib/record-test 105 | 106 | # [2.1.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0-alpha.0...v2.1.0) (2021-05-15) 107 | 108 | **Note:** Version bump only for package @xkeys-lib/record-test 109 | 110 | # [2.1.0-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0-alpha.0...v2.1.0-alpha.1) (2021-05-10) 111 | 112 | **Note:** Version bump only for package @xkeys-lib/record-test 113 | 114 | # [2.1.0-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.0.0...v2.1.0-alpha.0) (2021-05-10) 115 | 116 | ### Bug Fixes 117 | 118 | - publication-script for the node-record-test executable (wip) ([e4a8071](https://github.com/SuperFlyTV/xkeys/commit/e4a80719686048b010976d464adb6a40bf86b3c0)) 119 | - refactor repo into lerna mono-repo ([d5bffc1](https://github.com/SuperFlyTV/xkeys/commit/d5bffc1798e7c8e89ae9fcc4355afd438ea82d3a)) 120 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [3.3.0](https://github.com/SuperFlyTV/xkeys/compare/v3.2.0...v3.3.0) (2024-12-09) 7 | 8 | 9 | ### Features 10 | 11 | * add flush() method, resolves [#106](https://github.com/SuperFlyTV/xkeys/issues/106) ([f0ade46](https://github.com/SuperFlyTV/xkeys/commit/f0ade467a900500fdeaf55603ae729f136316746)) 12 | 13 | 14 | 15 | 16 | 17 | # [3.2.0](https://github.com/SuperFlyTV/xkeys/compare/v3.1.2...v3.2.0) (2024-08-26) 18 | 19 | 20 | ### Features 21 | 22 | * Add XkeysWatcher to WebHID version, rework XkeysWatcher to share code between node & webHID versions ([34bbd3c](https://github.com/SuperFlyTV/xkeys/commit/34bbd3cbd765d97f3d4f52690f78d4cfef5817a2)) 23 | 24 | 25 | 26 | 27 | 28 | ## [3.1.2](https://github.com/SuperFlyTV/xkeys/compare/v3.1.1...v3.1.2) (2024-08-12) 29 | 30 | **Note:** Version bump only for package @xkeys-lib/core 31 | 32 | 33 | 34 | 35 | 36 | ## [3.1.1](https://github.com/SuperFlyTV/xkeys/compare/v3.1.0...v3.1.1) (2024-03-04) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * clarify which github page. ([07d7b4f](https://github.com/SuperFlyTV/xkeys/commit/07d7b4f2402ffcaeeba375f5e7f74b4df9eb8de3)) 42 | 43 | 44 | 45 | 46 | 47 | # [3.1.0](https://github.com/SuperFlyTV/xkeys/compare/v3.0.1...v3.1.0) (2024-01-11) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * expose Xkeys.filterDevice() static method, used to filter for compatible X-keys devices when manually handling HID devices ([ab542a8](https://github.com/SuperFlyTV/xkeys/commit/ab542a8630c749f79cd21c4589eb263c6017ea99)) 53 | 54 | 55 | 56 | 57 | 58 | ## [3.0.1](https://github.com/SuperFlyTV/xkeys/compare/v3.0.0...v3.0.1) (2023-11-02) 59 | 60 | **Note:** Version bump only for package @xkeys-lib/core 61 | 62 | 63 | 64 | 65 | 66 | # [3.0.0](https://github.com/SuperFlyTV/xkeys/compare/v2.4.0...v3.0.0) (2023-05-03) 67 | 68 | ### Bug Fixes 69 | 70 | - issue with trackball ([5e2021a](https://github.com/SuperFlyTV/xkeys/commit/5e2021af49d12a7367d39f638c375210db343714)) 71 | 72 | # [2.4.0](https://github.com/SuperFlyTV/xkeys/compare/v2.3.4...v2.4.0) (2022-10-26) 73 | 74 | **Note:** Version bump only for package @xkeys-lib/core 75 | 76 | ## [2.3.4](https://github.com/SuperFlyTV/xkeys/compare/v2.3.3...v2.3.4) (2022-06-06) 77 | 78 | **Note:** Version bump only for package @xkeys-lib/core 79 | 80 | ## [2.3.2](https://github.com/SuperFlyTV/xkeys/compare/v2.3.0...v2.3.2) (2021-12-12) 81 | 82 | ### Bug Fixes 83 | 84 | - add XKeys.writeData() method, used for testing and development ([fba879c](https://github.com/SuperFlyTV/xkeys/commit/fba879c0f93ee64fbcdbd7faf5863998300c2016)) 85 | 86 | # [2.3.0](https://github.com/SuperFlyTV/xkeys/compare/v2.2.1...v2.3.0) (2021-11-28) 87 | 88 | **Note:** Version bump only for package @xkeys-lib/core 89 | 90 | ## [2.2.1](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0...v2.2.1) (2021-09-22) 91 | 92 | **Note:** Version bump only for package @xkeys-lib/core 93 | 94 | # [2.2.0](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0-alpha.1...v2.2.0) (2021-09-08) 95 | 96 | **Note:** Version bump only for package @xkeys-lib/core 97 | 98 | # [2.2.0-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0-alpha.0...v2.2.0-alpha.1) (2021-09-06) 99 | 100 | ### Bug Fixes 101 | 102 | - re-add devicePath ([349f6a9](https://github.com/SuperFlyTV/xkeys/commit/349f6a93ace9480e18d5ed695186920165fea6e7)) 103 | 104 | # [2.2.0-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1...v2.2.0-alpha.0) (2021-09-06) 105 | 106 | ### Features 107 | 108 | - add feature: "Automatic UnitId mode" ([f7c3a86](https://github.com/SuperFlyTV/xkeys/commit/f7c3a869e8820f856831aad576ce7978dfb9d75c)) 109 | - add XKeys.uniqueId property, to be used with automaticUnitIdMode ([a2e6d7a](https://github.com/SuperFlyTV/xkeys/commit/a2e6d7a6ec917d82bc2a71c1922c22c061232908)) 110 | 111 | ## [2.1.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1-alpha.1...v2.1.1) (2021-05-24) 112 | 113 | **Note:** Version bump only for package @xkeys-lib/core 114 | 115 | ## [2.1.1-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0...v2.1.1-alpha.0) (2021-05-23) 116 | 117 | **Note:** Version bump only for package @xkeys-lib/core 118 | 119 | # [2.1.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0-alpha.0...v2.1.0) (2021-05-15) 120 | 121 | **Note:** Version bump only for package @xkeys-lib/core 122 | 123 | # [2.1.0-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0-alpha.0...v2.1.0-alpha.1) (2021-05-10) 124 | 125 | **Note:** Version bump only for package @xkeys-lib/core 126 | 127 | # [2.1.0-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.0.0...v2.1.0-alpha.0) (2021-05-10) 128 | 129 | ### Bug Fixes 130 | 131 | - refactor repo into lerna mono-repo ([d5bffc1](https://github.com/SuperFlyTV/xkeys/commit/d5bffc1798e7c8e89ae9fcc4355afd438ea82d3a)) 132 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/recordings/1080_XK-3 Foot Pedal.json: -------------------------------------------------------------------------------- 1 | { 2 | "device": { 3 | "name": "Pi3 Matrix Board", 4 | "productId": 1080 5 | }, 6 | "info": { 7 | "name": "XK-3 Foot Pedal", 8 | "productId": 1080, 9 | "interface": 0, 10 | "unitId": 0, 11 | "firmwareVersion": 20, 12 | "colCount": 3, 13 | "rowCount": 1, 14 | "layout": [], 15 | "hasPS": true, 16 | "hasJoystick": 0, 17 | "hasJog": 0, 18 | "hasShuttle": 0, 19 | "hasTbar": 0, 20 | "hasLCD": false, 21 | "hasGPIO": false, 22 | "hasSerialData": false, 23 | "hasDMX": false 24 | }, 25 | "errors": [], 26 | "actions": [ 27 | { 28 | "sentData": [ 29 | "00b306010000000000000000000000000000000000000000000000000000000000000000" 30 | ], 31 | "method": "setIndicatorLED", 32 | "arguments": [ 33 | 1, 34 | true 35 | ], 36 | "anomaly": "" 37 | }, 38 | { 39 | "sentData": [ 40 | "00b306000000000000000000000000000000000000000000000000000000000000000000" 41 | ], 42 | "method": "setIndicatorLED", 43 | "arguments": [ 44 | 1, 45 | false 46 | ], 47 | "anomaly": "" 48 | }, 49 | { 50 | "sentData": [ 51 | "00b307010000000000000000000000000000000000000000000000000000000000000000" 52 | ], 53 | "method": "setIndicatorLED", 54 | "arguments": [ 55 | 2, 56 | true 57 | ], 58 | "anomaly": "" 59 | }, 60 | { 61 | "sentData": [ 62 | "00b307000000000000000000000000000000000000000000000000000000000000000000" 63 | ], 64 | "method": "setIndicatorLED", 65 | "arguments": [ 66 | 2, 67 | false 68 | ], 69 | "anomaly": "" 70 | }, 71 | { 72 | "sentData": [ 73 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 74 | "00b601ff0000000000000000000000000000000000000000000000000000000000000000" 75 | ], 76 | "method": "setAllBacklights", 77 | "arguments": [ 78 | "ffffff" 79 | ], 80 | "anomaly": "" 81 | }, 82 | { 83 | "sentData": [ 84 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 85 | "00b601000000000000000000000000000000000000000000000000000000000000000000" 86 | ], 87 | "method": "setAllBacklights", 88 | "arguments": [ 89 | "blue" 90 | ], 91 | "anomaly": "" 92 | }, 93 | { 94 | "sentData": [ 95 | "00b600000000000000000000000000000000000000000000000000000000000000000000", 96 | "00b601000000000000000000000000000000000000000000000000000000000000000000" 97 | ], 98 | "method": "setAllBacklights", 99 | "arguments": [ 100 | false 101 | ], 102 | "anomaly": "" 103 | }, 104 | { 105 | "sentData": [], 106 | "method": "setBacklight", 107 | "arguments": [ 108 | 1, 109 | "00f" 110 | ], 111 | "anomaly": "" 112 | }, 113 | { 114 | "sentData": [], 115 | "method": "setBacklight", 116 | "arguments": [ 117 | 1, 118 | "00f", 119 | true 120 | ], 121 | "anomaly": "" 122 | }, 123 | { 124 | "sentData": [], 125 | "method": "setBacklight", 126 | "arguments": [ 127 | 1, 128 | "000" 129 | ], 130 | "anomaly": "" 131 | } 132 | ], 133 | "events": [ 134 | { 135 | "data": [ 136 | "0001000000000000000000000000000000000000cea400000000000000000000" 137 | ], 138 | "description": "Button 0 pressed. Metadata: row: 0, col: 0, timestamp: 52900" 139 | }, 140 | { 141 | "data": [ 142 | "0001020000000000000000000000000000000000cea500000000000000000000" 143 | ], 144 | "description": "Button 2 pressed. Metadata: row: 1, col: 1, timestamp: 52901" 145 | }, 146 | { 147 | "data": [ 148 | "0001000000000000000000000000000000000000d68000000000000000000000" 149 | ], 150 | "description": "Button 2 released. Metadata: row: 1, col: 1, timestamp: 54912" 151 | }, 152 | { 153 | "data": [ 154 | "0001040000000000000000000000000000000000da1300000000000000000000" 155 | ], 156 | "description": "Button 3 pressed. Metadata: row: 1, col: 2, timestamp: 55827" 157 | }, 158 | { 159 | "data": [ 160 | "0001000000000000000000000000000000000000e4a900000000000000000000" 161 | ], 162 | "description": "Button 3 released. Metadata: row: 1, col: 2, timestamp: 58537" 163 | }, 164 | { 165 | "data": [ 166 | "0001080000000000000000000000000000000000eda100000000000000000000" 167 | ], 168 | "description": "Button 4 pressed. Metadata: row: 1, col: 3, timestamp: 60833" 169 | }, 170 | { 171 | "data": [ 172 | "0001000000000000000000000000000000000000f2b700000000000000000000" 173 | ], 174 | "description": "Button 4 released. Metadata: row: 1, col: 3, timestamp: 62135" 175 | } 176 | ] 177 | } 178 | -------------------------------------------------------------------------------- /packages/webhid/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [3.3.0](https://github.com/SuperFlyTV/xkeys/compare/v3.2.0...v3.3.0) (2024-12-09) 7 | 8 | 9 | ### Features 10 | 11 | * add flush() method, resolves [#106](https://github.com/SuperFlyTV/xkeys/issues/106) ([f0ade46](https://github.com/SuperFlyTV/xkeys/commit/f0ade467a900500fdeaf55603ae729f136316746)) 12 | 13 | 14 | 15 | 16 | 17 | # [3.2.0](https://github.com/SuperFlyTV/xkeys/compare/v3.1.2...v3.2.0) (2024-08-26) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * add 'disconnect' event for the webHID device ([44179d3](https://github.com/SuperFlyTV/xkeys/commit/44179d374bccf730bd0caf9fee6605359f48cf03)) 23 | 24 | 25 | ### Features 26 | 27 | * Add XkeysWatcher to WebHID version, rework XkeysWatcher to share code between node & webHID versions ([34bbd3c](https://github.com/SuperFlyTV/xkeys/commit/34bbd3cbd765d97f3d4f52690f78d4cfef5817a2)) 28 | 29 | 30 | 31 | 32 | 33 | ## [3.1.2](https://github.com/SuperFlyTV/xkeys/compare/v3.1.1...v3.1.2) (2024-08-12) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * event listeners in node-hid-wapper to follow style in web-hid-wrapper. ([ee1d6c6](https://github.com/SuperFlyTV/xkeys/commit/ee1d6c6c110ddb70fbdeafd389c9c4504ee17f8c)) 39 | 40 | 41 | 42 | 43 | 44 | ## [3.1.1](https://github.com/SuperFlyTV/xkeys/compare/v3.1.0...v3.1.1) (2024-03-04) 45 | 46 | **Note:** Version bump only for package xkeys-webhid 47 | 48 | 49 | 50 | 51 | 52 | # [3.1.0](https://github.com/SuperFlyTV/xkeys/compare/v3.0.1...v3.1.0) (2024-01-11) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * expose Xkeys.filterDevice() static method, used to filter for compatible X-keys devices when manually handling HID devices ([ab542a8](https://github.com/SuperFlyTV/xkeys/commit/ab542a8630c749f79cd21c4589eb263c6017ea99)) 58 | 59 | 60 | 61 | 62 | 63 | ## [3.0.1](https://github.com/SuperFlyTV/xkeys/compare/v3.0.0...v3.0.1) (2023-11-02) 64 | 65 | 66 | ### Bug Fixes 67 | 68 | * filter for correct usage/usagePage when finding xkeys devices ([8b9fdd1](https://github.com/SuperFlyTV/xkeys/commit/8b9fdd1eb69abf03cfbc67f5b503bd01a8623bc5)) 69 | * filter for correct usage/usagePage when finding xkeys devices ([68f3e86](https://github.com/SuperFlyTV/xkeys/commit/68f3e869139b2a846e2be4209f5201f7e4893494)) 70 | 71 | 72 | 73 | 74 | 75 | # [3.0.0](https://github.com/SuperFlyTV/xkeys/compare/v2.4.0...v3.0.0) (2023-05-03) 76 | 77 | **Note:** Version bump only for package xkeys-webhid 78 | 79 | # [2.4.0](https://github.com/SuperFlyTV/xkeys/compare/v2.3.4...v2.4.0) (2022-10-26) 80 | 81 | **Note:** Version bump only for package xkeys-webhid 82 | 83 | ## [2.3.4](https://github.com/SuperFlyTV/xkeys/compare/v2.3.3...v2.3.4) (2022-06-06) 84 | 85 | **Note:** Version bump only for package xkeys-webhid 86 | 87 | ## [2.3.4](https://github.com/SuperFlyTV/xkeys/compare/v2.3.3...v2.3.4) (2022-06-06) 88 | 89 | **Note:** Version bump only for package xkeys-webhid 90 | 91 | ## [2.3.2](https://github.com/SuperFlyTV/xkeys/compare/v2.3.0...v2.3.2) (2021-12-12) 92 | 93 | **Note:** Version bump only for package xkeys-webhid 94 | 95 | # [2.3.0](https://github.com/SuperFlyTV/xkeys/compare/v2.2.1...v2.3.0) (2021-11-28) 96 | 97 | **Note:** Version bump only for package xkeys-webhid 98 | 99 | ## [2.2.1](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0...v2.2.1) (2021-09-22) 100 | 101 | **Note:** Version bump only for package xkeys-webhid 102 | 103 | # [2.2.0](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0-alpha.1...v2.2.0) (2021-09-08) 104 | 105 | **Note:** Version bump only for package xkeys-webhid 106 | 107 | # [2.2.0-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0-alpha.0...v2.2.0-alpha.1) (2021-09-06) 108 | 109 | ### Bug Fixes 110 | 111 | - re-add devicePath ([349f6a9](https://github.com/SuperFlyTV/xkeys/commit/349f6a93ace9480e18d5ed695186920165fea6e7)) 112 | 113 | # [2.2.0-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1...v2.2.0-alpha.0) (2021-09-06) 114 | 115 | **Note:** Version bump only for package xkeys-webhid 116 | 117 | ## [2.1.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1-alpha.1...v2.1.1) (2021-05-24) 118 | 119 | **Note:** Version bump only for package xkeys-webhid 120 | 121 | ## [2.1.1-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0...v2.1.1-alpha.0) (2021-05-23) 122 | 123 | **Note:** Version bump only for package xkeys-webhid 124 | 125 | # [2.1.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0-alpha.0...v2.1.0) (2021-05-15) 126 | 127 | **Note:** Version bump only for package xkeys-webhid 128 | 129 | # [2.1.0-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0-alpha.0...v2.1.0-alpha.1) (2021-05-10) 130 | 131 | **Note:** Version bump only for package xkeys-webhid 132 | 133 | # [2.1.0-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.0.0...v2.1.0-alpha.0) (2021-05-10) 134 | 135 | ### Features 136 | 137 | - add package with web-HID support ([1f27199](https://github.com/SuperFlyTV/xkeys/commit/1f2719969faf93ba45a2bc767f64543fb9ffe6ea)) 138 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/recordings/1224_XK-3 Switch Interface.json: -------------------------------------------------------------------------------- 1 | { 2 | "device": { 3 | "name": "XK-3 Switch Interface Thumb", 4 | "productId": 1224 5 | }, 6 | "info": { 7 | "name": "XK-3 Switch Interface", 8 | "productId": 1224, 9 | "interface": 0, 10 | "unitId": 15, 11 | "firmwareVersion": 7, 12 | "colCount": 3, 13 | "rowCount": 1, 14 | "layout": [], 15 | "hasPS": false, 16 | "hasJoystick": 0, 17 | "hasJog": 0, 18 | "hasShuttle": 0, 19 | "hasTbar": 0, 20 | "hasLCD": false, 21 | "hasGPIO": false, 22 | "hasSerialData": false, 23 | "hasDMX": false 24 | }, 25 | "errors": [], 26 | "actions": [ 27 | { 28 | "sentData": [ 29 | "00b306010000000000000000000000000000000000000000000000000000000000000000" 30 | ], 31 | "method": "setIndicatorLED", 32 | "arguments": [ 33 | 1, 34 | true 35 | ], 36 | "anomaly": "" 37 | }, 38 | { 39 | "sentData": [ 40 | "00b306000000000000000000000000000000000000000000000000000000000000000000" 41 | ], 42 | "method": "setIndicatorLED", 43 | "arguments": [ 44 | 1, 45 | false 46 | ], 47 | "anomaly": "" 48 | }, 49 | { 50 | "sentData": [ 51 | "00b307010000000000000000000000000000000000000000000000000000000000000000" 52 | ], 53 | "method": "setIndicatorLED", 54 | "arguments": [ 55 | 2, 56 | true 57 | ], 58 | "anomaly": "" 59 | }, 60 | { 61 | "sentData": [ 62 | "00b307000000000000000000000000000000000000000000000000000000000000000000" 63 | ], 64 | "method": "setIndicatorLED", 65 | "arguments": [ 66 | 2, 67 | false 68 | ], 69 | "anomaly": "" 70 | }, 71 | { 72 | "sentData": [ 73 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 74 | "00b601ff0000000000000000000000000000000000000000000000000000000000000000" 75 | ], 76 | "method": "setAllBacklights", 77 | "arguments": [ 78 | "ffffff" 79 | ], 80 | "anomaly": "" 81 | }, 82 | { 83 | "sentData": [ 84 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 85 | "00b601000000000000000000000000000000000000000000000000000000000000000000" 86 | ], 87 | "method": "setAllBacklights", 88 | "arguments": [ 89 | "blue" 90 | ], 91 | "anomaly": "" 92 | }, 93 | { 94 | "sentData": [ 95 | "00b600000000000000000000000000000000000000000000000000000000000000000000", 96 | "00b601000000000000000000000000000000000000000000000000000000000000000000" 97 | ], 98 | "method": "setAllBacklights", 99 | "arguments": [ 100 | false 101 | ], 102 | "anomaly": "" 103 | }, 104 | { 105 | "sentData": [], 106 | "method": "setBacklight", 107 | "arguments": [ 108 | 1, 109 | "00f" 110 | ], 111 | "anomaly": "" 112 | }, 113 | { 114 | "sentData": [], 115 | "method": "setBacklight", 116 | "arguments": [ 117 | 1, 118 | "00f", 119 | true 120 | ], 121 | "anomaly": "" 122 | }, 123 | { 124 | "sentData": [], 125 | "method": "setBacklight", 126 | "arguments": [ 127 | 1, 128 | "000" 129 | ], 130 | "anomaly": "" 131 | } 132 | ], 133 | "events": [ 134 | { 135 | "data": [ 136 | "0f000c0000000100000000000000000000000000000000000000000000000000001a2401" 137 | ], 138 | "description": "Button 3 pressed. Metadata: row: 1, col: 0, timestamp: 6692" 139 | }, 140 | { 141 | "data": [ 142 | "0f001c0000000100000000000000000000000000000000000000000000000000008dce01" 143 | ], 144 | "description": "Button 5 pressed. Metadata: row: 1, col: 3, timestamp: 36302" 145 | }, 146 | { 147 | "data": [ 148 | "0f000c0000000100000000000000000000000000000000000000000000000000009a2401" 149 | ], 150 | "description": "Button 5 released. Metadata: row: 1, col: 3, timestamp: 39460" 151 | }, 152 | { 153 | "data": [ 154 | "0f000d0000000100000000000000000000000000000000000000000000000000009c9f01" 155 | ], 156 | "description": "Button 1 pressed. Metadata: row: 1, col: 1, timestamp: 40095" 157 | }, 158 | { 159 | "data": [ 160 | "0f000c000000010000000000000000000000000000000000000000000000000000a1e301" 161 | ], 162 | "description": "Button 1 released. Metadata: row: 1, col: 1, timestamp: 41443" 163 | }, 164 | { 165 | "data": [ 166 | "0f000e000000010000000000000000000000000000000000000000000000000000a6e701" 167 | ], 168 | "description": "Button 2 pressed. Metadata: row: 1, col: 2, timestamp: 42727" 169 | }, 170 | { 171 | "data": [ 172 | "0f000c000000010000000000000000000000000000000000000000000000000000ac1401" 173 | ], 174 | "description": "Button 2 released. Metadata: row: 1, col: 2, timestamp: 44052" 175 | } 176 | ] 177 | } 178 | -------------------------------------------------------------------------------- /packages/core/src/api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file contains public type interfaces. 3 | * If changing these, consider whether it might be a breaking change. 4 | */ 5 | 6 | export type ButtonStates = Map 7 | 8 | export interface AnalogStates { 9 | /** -127 to 127 */ 10 | jog: number[] 11 | /** -127 to 127 */ 12 | shuttle: number[] 13 | 14 | joystick: JoystickValue[] 15 | /** 0 to 255 */ 16 | tbar: number[] 17 | /** 0 to 255 */ 18 | rotary: number[] 19 | 20 | // todo: Implement these: 21 | // slider?: number[] // x with feedback 22 | trackball: TrackballValue[] 23 | // trackpad?: {x: number, y: number, z: number}[] // z: proximity/force 24 | } 25 | export interface JoystickValue { 26 | /** Joystick X (horizontal movement). -127 to 127 */ 27 | x: number 28 | /** Joystick Y (vertical movement), positive value is "up". -127 to 127 */ 29 | y: number 30 | /** 31 | * Joystick Z (twist of joystick) is a continuous value that rolls over to 0 after 255. 32 | * Note: Use .deltaZ instead 33 | */ 34 | z: number 35 | } 36 | export interface TrackballValue { 37 | /** X (delta horizontal movement). 2 byte */ 38 | x: number 39 | /** Y (delta vertical movement), positive value is "up". 2 byte */ 40 | y: number 41 | } 42 | export interface JoystickValueEmit extends JoystickValue { 43 | /** Joystick delta Z, a delta value that behaves properly when Z rolls over 255 to 0 */ 44 | deltaZ: number 45 | } 46 | export type Color = { r: number; g: number; b: number } 47 | 48 | export interface EventMetadata { 49 | /** 50 | * Timestamp of the event. Measured in milliseconds from when the device was last powered on. 51 | * The timestamp can be used as a more trustworthy source of time than the computer clock, as it's not affected by delays in the USB data handling. 52 | */ 53 | timestamp: number | undefined 54 | } 55 | export interface ButtonEventMetadata extends EventMetadata { 56 | /** Row of the button location*/ 57 | row: number 58 | /** Column of the button location */ 59 | col: number 60 | } 61 | export interface XKeysEvents { 62 | // Note: This interface defines strong typings for any events that are emitted by the XKeys class. 63 | 64 | down: (keyIndex: number, metadata: ButtonEventMetadata) => void 65 | up: (keyIndex: number, metadata: ButtonEventMetadata) => void 66 | 67 | jog: (index: number, value: number, eventMetadata: EventMetadata) => void 68 | shuttle: (index: number, value: number, eventMetadata: EventMetadata) => void 69 | joystick: (index: number, value: JoystickValueEmit, eventMetadata: EventMetadata) => void 70 | tbar: (index: number, value: number, eventMetadata: EventMetadata) => void 71 | trackball: (index: number, value: TrackballValue, eventMetadata: EventMetadata) => void 72 | rotary: (index: number, value: number, eventMetadata: EventMetadata) => void 73 | 74 | disconnected: () => void 75 | reconnected: () => void 76 | error: (err: any) => void 77 | } 78 | export interface XKeysInfo { 79 | /** Name of the device */ 80 | name: string 81 | 82 | /** Vendor id of the HID device */ 83 | vendorId: number 84 | /** Product id of the HID device */ 85 | productId: number 86 | /** Interface number of the HID device */ 87 | interface: number 88 | 89 | /** Unit id ("UID") of the device, is used to uniquely identify a certain panel, or panel type. 90 | * From factory it's set to 0, but it can be changed using xkeys.setUnitId() 91 | */ 92 | unitId: number 93 | /** firmware version of the device */ 94 | firmwareVersion: number 95 | 96 | /** The number of physical columns */ 97 | colCount: number 98 | /** The number of physical rows */ 99 | rowCount: number 100 | /** 101 | * Physical layout of the product. To be used to draw a visual representation of the X-keys 102 | * Note: Layout is a work-in-progress and it might/will change in the future. 103 | */ 104 | layout: { 105 | /** Name of the region */ 106 | name: string 107 | /** Index of the region */ 108 | index: number 109 | /** First row of the region (1-indexed) */ 110 | startRow: number 111 | /** First column of the region (1-indexed) */ 112 | startCol: number 113 | /** Last row of the region (1-indexed) */ 114 | endRow: number 115 | /** Last column of the region (1-indexed) */ 116 | endCol: number 117 | }[] 118 | 119 | /** If the X-keys panel emits timestamps (if not, timestamp will be undefined) */ 120 | emitsTimestamp: boolean 121 | 122 | /** If the product has the Program Switch button, this is a special switch not in the normal switch matrix. If exists, only one per X-keys. */ 123 | hasPS: boolean 124 | /** The number of joysticks available on the device */ 125 | hasJoystick: number 126 | /** The number of trackballs available on the device */ 127 | hasTrackball: number 128 | /** The number of jog wheels available on the device */ 129 | hasJog: number 130 | /** The number of shuttles available on the device */ 131 | hasShuttle: number 132 | /** The number of T-bars available on the device */ 133 | hasTbar: number 134 | /** The number of rotary knobs available on the device */ 135 | hasRotary: number 136 | /** The number of extra buttons available on the device */ 137 | hasExtraButtons: number 138 | 139 | /** If the device has an LCD display */ 140 | hasLCD: boolean 141 | /** If the device has GPIO support */ 142 | hasGPIO: boolean 143 | /** If the device has serial-data support */ 144 | hasSerialData: boolean 145 | /** If the device has DMX support */ 146 | hasDMX: boolean 147 | } 148 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/recordings/1127_XK-4 Stick.json: -------------------------------------------------------------------------------- 1 | { 2 | "device": { 3 | "name": "XK-16 HID", 4 | "productId": 1127 5 | }, 6 | "info": { 7 | "name": "XK-4 Stick", 8 | "productId": 1127, 9 | "interface": 0, 10 | "unitId": 0, 11 | "firmwareVersion": 14, 12 | "colCount": 4, 13 | "rowCount": 1, 14 | "layout": [], 15 | "hasPS": true, 16 | "hasJoystick": 0, 17 | "hasJog": 0, 18 | "hasShuttle": 0, 19 | "hasTbar": 0, 20 | "hasLCD": false, 21 | "hasGPIO": false, 22 | "hasSerialData": false, 23 | "hasDMX": false 24 | }, 25 | "errors": [], 26 | "actions": [ 27 | { 28 | "sentData": [ 29 | "00b306010000000000000000000000000000000000000000000000000000000000000000" 30 | ], 31 | "method": "setIndicatorLED", 32 | "arguments": [ 33 | 1, 34 | true 35 | ], 36 | "anomaly": "" 37 | }, 38 | { 39 | "sentData": [ 40 | "00b306000000000000000000000000000000000000000000000000000000000000000000" 41 | ], 42 | "method": "setIndicatorLED", 43 | "arguments": [ 44 | 1, 45 | false 46 | ], 47 | "anomaly": "" 48 | }, 49 | { 50 | "sentData": [ 51 | "00b307010000000000000000000000000000000000000000000000000000000000000000" 52 | ], 53 | "method": "setIndicatorLED", 54 | "arguments": [ 55 | 2, 56 | true 57 | ], 58 | "anomaly": "" 59 | }, 60 | { 61 | "sentData": [ 62 | "00b307000000000000000000000000000000000000000000000000000000000000000000" 63 | ], 64 | "method": "setIndicatorLED", 65 | "arguments": [ 66 | 2, 67 | false 68 | ], 69 | "anomaly": "" 70 | }, 71 | { 72 | "sentData": [ 73 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 74 | "00b601ff0000000000000000000000000000000000000000000000000000000000000000" 75 | ], 76 | "method": "setAllBacklights", 77 | "arguments": [ 78 | "ffffff" 79 | ], 80 | "anomaly": "" 81 | }, 82 | { 83 | "sentData": [ 84 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 85 | "00b601000000000000000000000000000000000000000000000000000000000000000000" 86 | ], 87 | "method": "setAllBacklights", 88 | "arguments": [ 89 | "blue" 90 | ], 91 | "anomaly": "" 92 | }, 93 | { 94 | "sentData": [ 95 | "00b600000000000000000000000000000000000000000000000000000000000000000000", 96 | "00b601000000000000000000000000000000000000000000000000000000000000000000" 97 | ], 98 | "method": "setAllBacklights", 99 | "arguments": [ 100 | false 101 | ], 102 | "anomaly": "" 103 | }, 104 | { 105 | "sentData": [ 106 | "00b500010100000000000000000000000000000000000000000000000000000000000000" 107 | ], 108 | "method": "setBacklight", 109 | "arguments": [ 110 | 1, 111 | "00f" 112 | ], 113 | "anomaly": "" 114 | }, 115 | { 116 | "sentData": [ 117 | "00b500020100000000000000000000000000000000000000000000000000000000000000" 118 | ], 119 | "method": "setBacklight", 120 | "arguments": [ 121 | 1, 122 | "00f", 123 | true 124 | ], 125 | "anomaly": "" 126 | }, 127 | { 128 | "sentData": [ 129 | "00b500000100000000000000000000000000000000000000000000000000000000000000" 130 | ], 131 | "method": "setBacklight", 132 | "arguments": [ 133 | 1, 134 | "000" 135 | ], 136 | "anomaly": "" 137 | } 138 | ], 139 | "events": [ 140 | { 141 | "data": [ 142 | "0001000000000000cd1c00000000000000000000000000000000000000000000" 143 | ], 144 | "description": "Button 0 pressed. Metadata: row: 0, col: 0, timestamp: 52508" 145 | }, 146 | { 147 | "data": [ 148 | "0001010000000000cd1c00000000000000000000000000000000000000000000" 149 | ], 150 | "description": "Button 1 pressed. Metadata: row: 1, col: 1, timestamp: 52508" 151 | }, 152 | { 153 | "data": [ 154 | "0001000000000000ceb500000000000000000000000000000000000000000000" 155 | ], 156 | "description": "Button 1 released. Metadata: row: 1, col: 1, timestamp: 52917" 157 | }, 158 | { 159 | "data": [ 160 | "0001000100000000d3b200000000000000000000000000000000000000000000" 161 | ], 162 | "description": "Button 2 pressed. Metadata: row: 1, col: 2, timestamp: 54194" 163 | }, 164 | { 165 | "data": [ 166 | "0001000000000000d4c800000000000000000000000000000000000000000000" 167 | ], 168 | "description": "Button 2 released. Metadata: row: 1, col: 2, timestamp: 54472" 169 | }, 170 | { 171 | "data": [ 172 | "0001000001000000d87b00000000000000000000000000000000000000000000" 173 | ], 174 | "description": "Button 3 pressed. Metadata: row: 1, col: 3, timestamp: 55419" 175 | }, 176 | { 177 | "data": [ 178 | "0001000000000000d99a00000000000000000000000000000000000000000000" 179 | ], 180 | "description": "Button 3 released. Metadata: row: 1, col: 3, timestamp: 55706" 181 | }, 182 | { 183 | "data": [ 184 | "0001000000010000de6c00000000000000000000000000000000000000000000" 185 | ], 186 | "description": "Button 4 pressed. Metadata: row: 1, col: 4, timestamp: 56940" 187 | }, 188 | { 189 | "data": [ 190 | "0001000000000000df8b00000000000000000000000000000000000000000000" 191 | ], 192 | "description": "Button 4 released. Metadata: row: 1, col: 4, timestamp: 57227" 193 | } 194 | ] 195 | } 196 | -------------------------------------------------------------------------------- /packages/node/src/methods.ts: -------------------------------------------------------------------------------- 1 | import { XKeys } from '@xkeys-lib/core' 2 | import * as HID from 'node-hid' 3 | import { NodeHIDDevice } from './node-hid-wrapper' 4 | 5 | import { isHID_Device } from './lib' 6 | 7 | import { HID_Device } from './api' 8 | 9 | /** 10 | * Sets up a connection to a HID device (the X-keys panel) 11 | * 12 | * If called without arguments, it will select any connected X-keys panel. 13 | */ 14 | export function setupXkeysPanel(): Promise 15 | export function setupXkeysPanel(HIDDevice: HID.Device): Promise 16 | export function setupXkeysPanel(HIDAsync: HID.HIDAsync): Promise 17 | export function setupXkeysPanel(devicePath: string): Promise 18 | export async function setupXkeysPanel( 19 | devicePathOrHIDDevice?: HID.Device | HID.HID | HID.HIDAsync | string 20 | ): Promise { 21 | let devicePath: string 22 | let device: HID.HIDAsync | undefined 23 | let deviceInfo: 24 | | { 25 | product: string | undefined 26 | vendorId: number 27 | productId: number 28 | interface: number 29 | } 30 | | undefined 31 | try { 32 | if (!devicePathOrHIDDevice) { 33 | // Device not provided, will then select any connected device: 34 | const connectedXkeys = listAllConnectedPanels() 35 | if (!connectedXkeys.length) { 36 | throw new Error('Could not find any connected X-keys panels.') 37 | } 38 | // Just select the first one: 39 | devicePath = connectedXkeys[0].path 40 | device = await HID.HIDAsync.open(devicePath) 41 | 42 | deviceInfo = { 43 | product: connectedXkeys[0].product, 44 | vendorId: connectedXkeys[0].vendorId, 45 | productId: connectedXkeys[0].productId, 46 | interface: connectedXkeys[0].interface, 47 | } 48 | } else if (isHID_Device(devicePathOrHIDDevice)) { 49 | // is HID.Device 50 | 51 | if (!devicePathOrHIDDevice.path) throw new Error('HID.Device path not set!') 52 | 53 | devicePath = devicePathOrHIDDevice.path 54 | device = await HID.HIDAsync.open(devicePath) 55 | 56 | deviceInfo = { 57 | product: devicePathOrHIDDevice.product, 58 | vendorId: devicePathOrHIDDevice.vendorId, 59 | productId: devicePathOrHIDDevice.productId, 60 | interface: devicePathOrHIDDevice.interface, 61 | } 62 | } else if (typeof devicePathOrHIDDevice === 'string') { 63 | // is string (path) 64 | 65 | devicePath = devicePathOrHIDDevice 66 | device = await HID.HIDAsync.open(devicePath) 67 | // deviceInfo is set later 68 | } else if (devicePathOrHIDDevice instanceof HID.HID) { 69 | // Can't use this, since devicePath is missing 70 | throw new Error( 71 | 'HID.HID not supported as argument to setupXkeysPanel, use HID.devices() to find the device and provide that instead.' 72 | ) 73 | } else if (devicePathOrHIDDevice instanceof HID.HIDAsync) { 74 | // @ts-expect-error getDeviceInfo missing in typings 75 | const dInfo = await devicePathOrHIDDevice.getDeviceInfo() 76 | 77 | if (!dInfo.path) 78 | throw new Error( 79 | // Can't use this, we need a path to the device 80 | '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.' 81 | ) 82 | 83 | devicePath = dInfo.path 84 | device = devicePathOrHIDDevice 85 | 86 | deviceInfo = { 87 | product: dInfo.product, 88 | vendorId: dInfo.vendorId, 89 | productId: dInfo.productId, 90 | interface: dInfo.interface, 91 | } 92 | } else { 93 | throw new Error('setupXkeysPanel: invalid arguments') 94 | } 95 | 96 | if (!deviceInfo) { 97 | // @ts-expect-error getDeviceInfo missing in typings 98 | const nodeHidInfo: HID.Device = await device.getDeviceInfo() 99 | // Look through HID.devices(), because HID.Device contains the productId 100 | deviceInfo = { 101 | product: nodeHidInfo.product, 102 | vendorId: nodeHidInfo.vendorId, 103 | productId: nodeHidInfo.productId, 104 | interface: nodeHidInfo.interface, 105 | } 106 | } 107 | 108 | if (!device) throw new Error('Error setting up X-keys: device not found') 109 | if (!devicePath) throw new Error('Error setting up X-keys: devicePath not found') 110 | if (!deviceInfo) throw new Error('Error setting up X-keys: deviceInfo not found') 111 | 112 | const deviceWrap = new NodeHIDDevice(device) 113 | 114 | const xkeys = new XKeys(deviceWrap, deviceInfo, devicePath) 115 | 116 | let alreadyRejected = false 117 | await new Promise((resolve, reject) => { 118 | const markRejected = (e: unknown) => { 119 | reject(e) 120 | alreadyRejected = true 121 | } 122 | const xkeysStopgapErrorHandler = (e: unknown) => { 123 | if (alreadyRejected) { 124 | console.error(`Xkeys: Error emitted after setup already rejected:`, e) 125 | return 126 | } 127 | 128 | markRejected(e) 129 | } 130 | 131 | // Handle all error events until the instance is returned 132 | xkeys.on('error', xkeysStopgapErrorHandler) 133 | 134 | // Wait for the device to initialize: 135 | xkeys 136 | .init() 137 | .then(() => { 138 | resolve() 139 | xkeys.removeListener('error', xkeysStopgapErrorHandler) 140 | }) 141 | .catch(markRejected) 142 | }) 143 | 144 | return xkeys 145 | } catch (e) { 146 | if (device) await device.close().catch(() => null) // Suppress error 147 | 148 | throw e 149 | } 150 | } 151 | /** Returns a list of all connected X-keys-HID-devices */ 152 | export function listAllConnectedPanels(): HID_Device[] { 153 | const connectedXkeys = HID.devices().filter((device) => { 154 | // Filter to only return the supported devices: 155 | 156 | if (!device.path) return false 157 | 158 | const found = XKeys.filterDevice({ 159 | product: device.product, 160 | interface: device.interface, 161 | vendorId: device.vendorId, 162 | productId: device.productId, 163 | }) 164 | if (!found) return false 165 | return true 166 | }) 167 | return connectedXkeys as HID_Device[] 168 | } 169 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/recordings.spec.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as HID from 'node-hid' 3 | import { Product, PRODUCTS, describeEvent } from '@xkeys-lib/core' 4 | import * as HIDMock from '../__mocks__/node-hid' 5 | import { setupXkeysPanel, XKeys, XKeysEvents } from '../' 6 | import { getSentData, handleXkeysMessages, resetSentData } from './lib' 7 | 8 | describe('Recorded tests', () => { 9 | async function setupTestPanel(params: { productId: number }): Promise { 10 | const hidDevice = { 11 | vendorId: XKeys.vendorId, 12 | productId: params.productId, 13 | interface: 0, 14 | path: 'mockPath', 15 | } as HID.Device 16 | 17 | HIDMock.setMockWriteHandler(handleXkeysMessages) 18 | 19 | const myXkeysPanel = await setupXkeysPanel(hidDevice) 20 | 21 | return myXkeysPanel 22 | } 23 | beforeAll(() => { 24 | expect(HIDMock.setMockWriteHandler).toBeTruthy() 25 | // @ts-expect-error mock 26 | expect(HID.setMockWriteHandler).toBeTruthy() 27 | }) 28 | beforeEach(() => {}) 29 | afterEach(() => { 30 | HIDMock.resetMockWriteHandler() 31 | }) 32 | 33 | const dirPath = './src/__tests__/recordings/' 34 | 35 | const recordings: { filePath: string; recording: any }[] = [] 36 | fs.readdirSync(dirPath).forEach((file) => { 37 | if (!file.match(/json$/)) return // only use json files 38 | const recording: any = JSON.parse(fs.readFileSync(dirPath + file, 'utf-8')) 39 | recordings.push({ 40 | filePath: file, 41 | recording: recording, 42 | }) 43 | }) 44 | 45 | recordings.forEach(({ filePath, recording }) => { 46 | test(`Recording "${filePath}"`, async () => { 47 | const xkeysDevice = await setupTestPanel({ 48 | productId: recording.device.productId, 49 | }) 50 | let lastDescription: string[] = [] 51 | let lastData: { event: string; args: any[] }[] = [] 52 | 53 | const handleEvent = (event: keyof XKeysEvents) => { 54 | xkeysDevice.on(event, (...args: any[]) => { 55 | lastDescription.push(describeEvent(event, args)) 56 | lastData.push({ event, args }) 57 | }) 58 | } 59 | handleEvent('down') 60 | handleEvent('up') 61 | handleEvent('jog') 62 | handleEvent('shuttle') 63 | handleEvent('joystick') 64 | handleEvent('tbar') 65 | handleEvent('disconnected') 66 | 67 | // Go through all recorded events: 68 | // (ie mock that data comes from the device, and check that the right events are emitted from the class) 69 | expect(recording.events.length).toBeGreaterThanOrEqual(1) 70 | for (const event of recording.events) { 71 | try { 72 | expect(event.data).toHaveLength(1) 73 | 74 | for (const data of event.data) { 75 | // Mock the device sending data: 76 | // @ts-expect-error hack 77 | xkeysDevice.device.emit('data', Buffer.from(data, 'hex')) 78 | } 79 | if (event.description) { 80 | expect(lastDescription).toEqual([event.description]) 81 | expect(lastData).toHaveLength(1) 82 | const eventType = lastData[0].event 83 | if (['down', 'up'].includes(eventType)) { 84 | const index = lastData[0].args[0] 85 | expect(index).toBeWithinRange(0, 999) 86 | 87 | const metadata = lastData[0].args[1] 88 | expect(metadata).toBeObject() 89 | expect(metadata.row).toBeWithinRange(0, 99) 90 | expect(metadata.col).toBeWithinRange(0, 99) 91 | if (xkeysDevice.info.emitsTimestamp) { 92 | expect(metadata.timestamp).toBeWithinRange(1, Number.POSITIVE_INFINITY) 93 | } else { 94 | expect(metadata.timestamp).toBe(undefined) 95 | } 96 | } else if (['jog', 'shuttle', 'joystick', 'tbar'].includes(eventType)) { 97 | const index = lastData[0].args[0] 98 | expect(index).toBeWithinRange(0, 999) 99 | 100 | // const value = lastData[0].args[1] 101 | 102 | const metadata = lastData[0].args[2] 103 | expect(metadata).toBeObject() 104 | 105 | if (xkeysDevice.info.emitsTimestamp) { 106 | expect(metadata.timestamp).toBeWithinRange(1, Number.POSITIVE_INFINITY) 107 | } else { 108 | expect(metadata.timestamp).toBe(undefined) 109 | } 110 | } else { 111 | throw new Error(`Unsupported event: "${eventType}" (update tests)`) 112 | } 113 | } else { 114 | expect(lastDescription).toEqual([]) 115 | expect(lastData).toHaveLength(0) 116 | } 117 | 118 | lastDescription = [] 119 | lastData = [] 120 | } catch (err) { 121 | console.log(event.description) 122 | throw err 123 | } 124 | } 125 | 126 | // Go through all recorded actions: 127 | // (ie trigger a method on the class, verify that the data sent to the device is correct) 128 | expect(recording.actions.length).toBeGreaterThanOrEqual(1) 129 | resetSentData() 130 | for (const action of recording.actions) { 131 | try { 132 | // @ts-expect-error hack 133 | expect(xkeysDevice[action.method]).toBeTruthy() 134 | expect(action.anomaly).toBeFalsy() 135 | 136 | // @ts-expect-error hack 137 | xkeysDevice[action.method](...action.arguments) 138 | 139 | await xkeysDevice.flush() 140 | 141 | expect(getSentData()).toEqual(action.sentData) 142 | resetSentData() 143 | } catch (err) { 144 | console.log('action', action) 145 | throw err 146 | } 147 | } 148 | }) 149 | }) 150 | 151 | test('Product coverage', () => { 152 | const products: { [name: string]: Product } = {} 153 | for (const [key, product] of Object.entries(PRODUCTS)) { 154 | products[key] = product 155 | } 156 | 157 | recordings.forEach(({ recording }) => { 158 | // Find and remove matched product: 159 | for (const [key, product] of Object.entries(products)) { 160 | let found = false 161 | for (const hidDevice of product.hidDevices) { 162 | if (hidDevice[0] === recording.info.productId && hidDevice[1] === recording.info.interface) { 163 | found = true 164 | } 165 | } 166 | if (found) { 167 | delete products[key] 168 | break 169 | } 170 | } 171 | }) 172 | 173 | console.log( 174 | `Note: Products not yet covered by tests: \n${Object.values(products) 175 | .map((p) => `* ${p.name}`) 176 | .join('\n')}` 177 | ) 178 | 179 | // This number should be decreased as more recordings are added 180 | expect(Object.values(products).length).toBeLessThanOrEqual(21) 181 | }) 182 | }) 183 | -------------------------------------------------------------------------------- /packages/node/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [3.3.0](https://github.com/SuperFlyTV/xkeys/compare/v3.2.0...v3.3.0) (2024-12-09) 7 | 8 | 9 | ### Features 10 | 11 | * add flush() method, resolves [#106](https://github.com/SuperFlyTV/xkeys/issues/106) ([f0ade46](https://github.com/SuperFlyTV/xkeys/commit/f0ade467a900500fdeaf55603ae729f136316746)) 12 | 13 | 14 | 15 | 16 | 17 | # [3.2.0](https://github.com/SuperFlyTV/xkeys/compare/v3.1.2...v3.2.0) (2024-08-26) 18 | 19 | 20 | ### Features 21 | 22 | * Add XkeysWatcher to WebHID version, rework XkeysWatcher to share code between node & webHID versions ([34bbd3c](https://github.com/SuperFlyTV/xkeys/commit/34bbd3cbd765d97f3d4f52690f78d4cfef5817a2)) 23 | 24 | 25 | 26 | 27 | 28 | ## [3.1.2](https://github.com/SuperFlyTV/xkeys/compare/v3.1.1...v3.1.2) (2024-08-12) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * event listeners in node-hid-wapper to follow style in web-hid-wrapper. ([ee1d6c6](https://github.com/SuperFlyTV/xkeys/commit/ee1d6c6c110ddb70fbdeafd389c9c4504ee17f8c)) 34 | 35 | 36 | 37 | 38 | 39 | ## [3.1.1](https://github.com/SuperFlyTV/xkeys/compare/v3.1.0...v3.1.1) (2024-03-04) 40 | 41 | **Note:** Version bump only for package xkeys 42 | 43 | 44 | 45 | 46 | 47 | # [3.1.0](https://github.com/SuperFlyTV/xkeys/compare/v3.0.1...v3.1.0) (2024-01-11) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * expose Xkeys.filterDevice() static method, used to filter for compatible X-keys devices when manually handling HID devices ([ab542a8](https://github.com/SuperFlyTV/xkeys/commit/ab542a8630c749f79cd21c4589eb263c6017ea99)) 53 | * remove hack (possible HID.HID that exposed a devicePath) ([fca382d](https://github.com/SuperFlyTV/xkeys/commit/fca382dd5109a8447ed7ba51d485de255487bd6d)) 54 | * remove support for HID.HID and HID.Async devices in setupXKeysPanel. ([1bc87ba](https://github.com/SuperFlyTV/xkeys/commit/1bc87ba26227831eb7f312e59eb15f9ed47497e1)) 55 | * support providing HID.HIDAsync into setupXkeysPanel() ([190d4a1](https://github.com/SuperFlyTV/xkeys/commit/190d4a1c2dfa1232b250318c30131624cf67fb23)) 56 | * typo ([095c064](https://github.com/SuperFlyTV/xkeys/commit/095c0640a52b920774965192cfb868badb82f012)) 57 | 58 | 59 | ### Features 60 | 61 | * use async node-hid ([429c5ea](https://github.com/SuperFlyTV/xkeys/commit/429c5ea6e83f5a8a025180d3c6a15943bddaf5d6)) 62 | 63 | 64 | 65 | 66 | 67 | ## [3.0.1](https://github.com/SuperFlyTV/xkeys/compare/v3.0.0...v3.0.1) (2023-11-02) 68 | 69 | **Note:** Version bump only for package xkeys 70 | 71 | 72 | 73 | 74 | 75 | # [3.0.0](https://github.com/SuperFlyTV/xkeys/compare/v2.4.0...v3.0.0) (2023-05-03) 76 | 77 | - BREAKING CHANGE: Dropped support for EOL versions of Node.js (<14). 78 | 79 | # [2.4.0](https://github.com/SuperFlyTV/xkeys/compare/v2.3.4...v2.4.0) (2022-10-26) 80 | 81 | ### Bug Fixes 82 | 83 | - update usb dep ([e1bc906](https://github.com/SuperFlyTV/xkeys/commit/e1bc9060e4ef82dce690a2bb76fb01601ed28f7a)) 84 | 85 | ### Features 86 | 87 | - replace usb-detection with usb ([d6349ef](https://github.com/SuperFlyTV/xkeys/commit/d6349ef0b0477045dd8a540887918cc2af8370aa)) 88 | 89 | ## [2.3.4](https://github.com/SuperFlyTV/xkeys/compare/v2.3.3...v2.3.4) (2022-06-06) 90 | 91 | ### Bug Fixes 92 | 93 | - Watcher: async handling of adding/removing devices ([61f0b28](https://github.com/SuperFlyTV/xkeys/commit/61f0b28571a3df72b49f4bd84b6d842408e86acd)) 94 | 95 | ## [2.3.2](https://github.com/SuperFlyTV/xkeys/compare/v2.3.0...v2.3.2) (2021-12-12) 96 | 97 | **Note:** Version bump only for package xkeys 98 | 99 | # [2.3.0](https://github.com/SuperFlyTV/xkeys/compare/v2.2.1...v2.3.0) (2021-11-28) 100 | 101 | ### Features 102 | 103 | - add usePolling option to the XKeysWatcher to fall back to polling, since "usb-detection" might not work on all OS:es ([ab31223](https://github.com/SuperFlyTV/xkeys/commit/ab312236b14cb8f961d0b0bf878c611487a5983f)) 104 | 105 | ## [2.2.1](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0...v2.2.1) (2021-09-22) 106 | 107 | **Note:** Version bump only for package xkeys 108 | 109 | # [2.2.0](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0-alpha.1...v2.2.0) (2021-09-08) 110 | 111 | **Note:** Version bump only for package xkeys 112 | 113 | # [2.2.0-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0-alpha.0...v2.2.0-alpha.1) (2021-09-06) 114 | 115 | ### Bug Fixes 116 | 117 | - re-add devicePath ([349f6a9](https://github.com/SuperFlyTV/xkeys/commit/349f6a93ace9480e18d5ed695186920165fea6e7)) 118 | 119 | # [2.2.0-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1...v2.2.0-alpha.0) (2021-09-06) 120 | 121 | ### Features 122 | 123 | - add feature: "Automatic UnitId mode" ([f7c3a86](https://github.com/SuperFlyTV/xkeys/commit/f7c3a869e8820f856831aad576ce7978dfb9d75c)) 124 | - add XKeys.uniqueId property, to be used with automaticUnitIdMode ([a2e6d7a](https://github.com/SuperFlyTV/xkeys/commit/a2e6d7a6ec917d82bc2a71c1922c22c061232908)) 125 | 126 | ## [2.1.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1-alpha.1...v2.1.1) (2021-05-24) 127 | 128 | **Note:** Version bump only for package xkeys 129 | 130 | ## [2.1.1-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1-alpha.0...v2.1.1-alpha.1) (2021-05-23) 131 | 132 | ### Bug Fixes 133 | 134 | - hack to fix issue in Electron ([501f06d](https://github.com/SuperFlyTV/xkeys/commit/501f06de9a2413832dab4b6a0ef4ef7d2b668967)) 135 | - make XKeysWatcher.stop() close all the devices it has called setupXkeysPanel() for. ([f69b599](https://github.com/SuperFlyTV/xkeys/commit/f69b59912a62b8dcc5ff00a2083c793851bba15c)) 136 | 137 | ## [2.1.1-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0...v2.1.1-alpha.0) (2021-05-23) 138 | 139 | ### Bug Fixes 140 | 141 | - remove listeners on watcher.stop() ([c8d36a3](https://github.com/SuperFlyTV/xkeys/commit/c8d36a3602b8c460233b82a48f6c28a04f52c9de)) 142 | 143 | # [2.1.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0-alpha.0...v2.1.0) (2021-05-15) 144 | 145 | **Note:** Version bump only for package xkeys 146 | 147 | # [2.1.0-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0-alpha.0...v2.1.0-alpha.1) (2021-05-10) 148 | 149 | **Note:** Version bump only for package xkeys 150 | 151 | # [2.1.0-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.0.0...v2.1.0-alpha.0) (2021-05-10) 152 | 153 | ### Bug Fixes 154 | 155 | - refactor repo into lerna mono-repo ([d5bffc1](https://github.com/SuperFlyTV/xkeys/commit/d5bffc1798e7c8e89ae9fcc4355afd438ea82d3a)) 156 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/__snapshots__/xkeys.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Unit tests XKeys methods 1`] = ` 4 | Object { 5 | "colCount": 4, 6 | "emitsTimestamp": true, 7 | "firmwareVersion": 1, 8 | "hasDMX": false, 9 | "hasExtraButtons": 0, 10 | "hasGPIO": false, 11 | "hasJog": 0, 12 | "hasJoystick": 0, 13 | "hasLCD": false, 14 | "hasPS": true, 15 | "hasRotary": 0, 16 | "hasSerialData": false, 17 | "hasShuttle": 0, 18 | "hasTbar": 0, 19 | "hasTrackball": 0, 20 | "interface": 0, 21 | "layout": Array [ 22 | Object { 23 | "endCol": 4, 24 | "endRow": 6, 25 | "index": 0, 26 | "name": "Keys", 27 | "startCol": 1, 28 | "startRow": 1, 29 | }, 30 | ], 31 | "name": "XK-24", 32 | "productId": 1029, 33 | "rowCount": 6, 34 | "unitId": 0, 35 | "vendorId": 1523, 36 | } 37 | `; 38 | 39 | exports[`Unit tests XKeys methods 2`] = `Array []`; 40 | 41 | exports[`Unit tests XKeys methods 3`] = ` 42 | Array [ 43 | "00b305010000000000000000000000000000000000000000000000000000000000000000", 44 | ] 45 | `; 46 | 47 | exports[`Unit tests XKeys methods 4`] = ` 48 | Array [ 49 | "00b305000000000000000000000000000000000000000000000000000000000000000000", 50 | ] 51 | `; 52 | 53 | exports[`Unit tests XKeys methods 5`] = ` 54 | Array [ 55 | "00b305020000000000000000000000000000000000000000000000000000000000000000", 56 | ] 57 | `; 58 | 59 | exports[`Unit tests XKeys methods 6`] = ` 60 | Array [ 61 | "00b504010100000000000000000000000000000000000000000000000000000000000000", 62 | "00b524010100000000000000000000000000000000000000000000000000000000000000", 63 | ] 64 | `; 65 | 66 | exports[`Unit tests XKeys methods 7`] = ` 67 | Array [ 68 | "00b504010100000000000000000000000000000000000000000000000000000000000000", 69 | "00b524010100000000000000000000000000000000000000000000000000000000000000", 70 | ] 71 | `; 72 | 73 | exports[`Unit tests XKeys methods 8`] = ` 74 | Array [ 75 | "00b504010100000000000000000000000000000000000000000000000000000000000000", 76 | "00b524010100000000000000000000000000000000000000000000000000000000000000", 77 | ] 78 | `; 79 | 80 | exports[`Unit tests XKeys methods 9`] = ` 81 | Array [ 82 | "00b504010100000000000000000000000000000000000000000000000000000000000000", 83 | "00b524010100000000000000000000000000000000000000000000000000000000000000", 84 | ] 85 | `; 86 | 87 | exports[`Unit tests XKeys methods 10`] = ` 88 | Array [ 89 | "00b504010100000000000000000000000000000000000000000000000000000000000000", 90 | "00b524000100000000000000000000000000000000000000000000000000000000000000", 91 | ] 92 | `; 93 | 94 | exports[`Unit tests XKeys methods 11`] = ` 95 | Array [ 96 | "00b504000100000000000000000000000000000000000000000000000000000000000000", 97 | "00b524000100000000000000000000000000000000000000000000000000000000000000", 98 | ] 99 | `; 100 | 101 | exports[`Unit tests XKeys methods 12`] = ` 102 | Array [ 103 | "00b504000100000000000000000000000000000000000000000000000000000000000000", 104 | "00b524000100000000000000000000000000000000000000000000000000000000000000", 105 | ] 106 | `; 107 | 108 | exports[`Unit tests XKeys methods 13`] = ` 109 | Array [ 110 | "00b504000100000000000000000000000000000000000000000000000000000000000000", 111 | "00b524000100000000000000000000000000000000000000000000000000000000000000", 112 | ] 113 | `; 114 | 115 | exports[`Unit tests XKeys methods 14`] = ` 116 | Array [ 117 | "00b504020100000000000000000000000000000000000000000000000000000000000000", 118 | "00b524020100000000000000000000000000000000000000000000000000000000000000", 119 | ] 120 | `; 121 | 122 | exports[`Unit tests XKeys methods 15`] = ` 123 | Array [ 124 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 125 | "00b601550000000000000000000000000000000000000000000000000000000000000000", 126 | ] 127 | `; 128 | 129 | exports[`Unit tests XKeys methods 16`] = ` 130 | Array [ 131 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 132 | "00b601550000000000000000000000000000000000000000000000000000000000000000", 133 | ] 134 | `; 135 | 136 | exports[`Unit tests XKeys methods 17`] = ` 137 | Array [ 138 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 139 | "00b601550000000000000000000000000000000000000000000000000000000000000000", 140 | ] 141 | `; 142 | 143 | exports[`Unit tests XKeys methods 18`] = ` 144 | Array [ 145 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 146 | "00b6012d0000000000000000000000000000000000000000000000000000000000000000", 147 | ] 148 | `; 149 | 150 | exports[`Unit tests XKeys methods 19`] = ` 151 | Array [ 152 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 153 | "00b601000000000000000000000000000000000000000000000000000000000000000000", 154 | ] 155 | `; 156 | 157 | exports[`Unit tests XKeys methods 20`] = ` 158 | Array [ 159 | "00b600000000000000000000000000000000000000000000000000000000000000000000", 160 | "00b601000000000000000000000000000000000000000000000000000000000000000000", 161 | ] 162 | `; 163 | 164 | exports[`Unit tests XKeys methods 21`] = ` 165 | Array [ 166 | "00b600000000000000000000000000000000000000000000000000000000000000000000", 167 | "00b601000000000000000000000000000000000000000000000000000000000000000000", 168 | ] 169 | `; 170 | 171 | exports[`Unit tests XKeys methods 22`] = ` 172 | Array [ 173 | "00b600000000000000000000000000000000000000000000000000000000000000000000", 174 | "00b601000000000000000000000000000000000000000000000000000000000000000000", 175 | ] 176 | `; 177 | 178 | exports[`Unit tests XKeys methods 23`] = ` 179 | Array [ 180 | "00b800000000000000000000000000000000000000000000000000000000000000000000", 181 | ] 182 | `; 183 | 184 | exports[`Unit tests XKeys methods 24`] = ` 185 | Array [ 186 | "00bb64640000000000000000000000000000000000000000000000000000000000000000", 187 | ] 188 | `; 189 | 190 | exports[`Unit tests XKeys methods 25`] = ` 191 | Array [ 192 | "00bb00ff0000000000000000000000000000000000000000000000000000000000000000", 193 | ] 194 | `; 195 | 196 | exports[`Unit tests XKeys methods 26`] = ` 197 | Array [ 198 | "00c701000000000000000000000000000000000000000000000000000000000000000000", 199 | ] 200 | `; 201 | 202 | exports[`Unit tests XKeys methods 27`] = ` 203 | Array [ 204 | "00b47f000000000000000000000000000000000000000000000000000000000000000000", 205 | ] 206 | `; 207 | 208 | exports[`Unit tests XKeys methods 28`] = ` 209 | Array [ 210 | "00bd2a000000000000000000000000000000000000000000000000000000000000000000", 211 | ] 212 | `; 213 | 214 | exports[`Unit tests XKeys methods 29`] = ` 215 | Array [ 216 | "00ee00000000000000000000000000000000000000000000000000000000000000000000", 217 | ] 218 | `; 219 | 220 | exports[`Unit tests XKeys methods 30`] = ` 221 | Array [ 222 | "000102030400000000000000000000000000000000000000000000000000000000000000", 223 | ] 224 | `; 225 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/recordings/1130_XK-8 Stick.json: -------------------------------------------------------------------------------- 1 | { 2 | "device": { 3 | "name": "XK-16 HID", 4 | "productId": 1130 5 | }, 6 | "info": { 7 | "name": "XK-8 Stick", 8 | "productId": 1130, 9 | "interface": 0, 10 | "unitId": 2, 11 | "firmwareVersion": 14, 12 | "colCount": 8, 13 | "rowCount": 1, 14 | "layout": [], 15 | "hasPS": true, 16 | "hasJoystick": 0, 17 | "hasJog": 0, 18 | "hasShuttle": 0, 19 | "hasTbar": 0, 20 | "hasLCD": false, 21 | "hasGPIO": false, 22 | "hasSerialData": false, 23 | "hasDMX": false 24 | }, 25 | "errors": [], 26 | "actions": [ 27 | { 28 | "sentData": [ 29 | "00b306010000000000000000000000000000000000000000000000000000000000000000" 30 | ], 31 | "method": "setIndicatorLED", 32 | "arguments": [ 33 | 1, 34 | true 35 | ], 36 | "anomaly": "" 37 | }, 38 | { 39 | "sentData": [ 40 | "00b306000000000000000000000000000000000000000000000000000000000000000000" 41 | ], 42 | "method": "setIndicatorLED", 43 | "arguments": [ 44 | 1, 45 | false 46 | ], 47 | "anomaly": "" 48 | }, 49 | { 50 | "sentData": [ 51 | "00b307010000000000000000000000000000000000000000000000000000000000000000" 52 | ], 53 | "method": "setIndicatorLED", 54 | "arguments": [ 55 | 2, 56 | true 57 | ], 58 | "anomaly": "" 59 | }, 60 | { 61 | "sentData": [ 62 | "00b307000000000000000000000000000000000000000000000000000000000000000000" 63 | ], 64 | "method": "setIndicatorLED", 65 | "arguments": [ 66 | 2, 67 | false 68 | ], 69 | "anomaly": "" 70 | }, 71 | { 72 | "sentData": [ 73 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 74 | "00b601ff0000000000000000000000000000000000000000000000000000000000000000" 75 | ], 76 | "method": "setAllBacklights", 77 | "arguments": [ 78 | "ffffff" 79 | ], 80 | "anomaly": "" 81 | }, 82 | { 83 | "sentData": [ 84 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 85 | "00b601000000000000000000000000000000000000000000000000000000000000000000" 86 | ], 87 | "method": "setAllBacklights", 88 | "arguments": [ 89 | "blue" 90 | ], 91 | "anomaly": "" 92 | }, 93 | { 94 | "sentData": [ 95 | "00b600000000000000000000000000000000000000000000000000000000000000000000", 96 | "00b601000000000000000000000000000000000000000000000000000000000000000000" 97 | ], 98 | "method": "setAllBacklights", 99 | "arguments": [ 100 | false 101 | ], 102 | "anomaly": "" 103 | }, 104 | { 105 | "sentData": [ 106 | "00b500010100000000000000000000000000000000000000000000000000000000000000" 107 | ], 108 | "method": "setBacklight", 109 | "arguments": [ 110 | 1, 111 | "00f" 112 | ], 113 | "anomaly": "" 114 | }, 115 | { 116 | "sentData": [ 117 | "00b500020100000000000000000000000000000000000000000000000000000000000000" 118 | ], 119 | "method": "setBacklight", 120 | "arguments": [ 121 | 1, 122 | "00f", 123 | true 124 | ], 125 | "anomaly": "" 126 | }, 127 | { 128 | "sentData": [ 129 | "00b500000100000000000000000000000000000000000000000000000000000000000000" 130 | ], 131 | "method": "setBacklight", 132 | "arguments": [ 133 | 1, 134 | "000" 135 | ], 136 | "anomaly": "" 137 | } 138 | ], 139 | "events": [ 140 | { 141 | "data": [ 142 | "020100000000000058b500000000000000000000000000000000000000000000" 143 | ], 144 | "description": "Button 0 pressed. Metadata: row: 0, col: 0, timestamp: 22709" 145 | }, 146 | { 147 | "data": [ 148 | "020101000000000064ba00000000000000000000000000000000000000000000" 149 | ], 150 | "description": "Button 1 pressed. Metadata: row: 1, col: 1, timestamp: 25786" 151 | }, 152 | { 153 | "data": [ 154 | "0201000000000000651b00000000000000000000000000000000000000000000" 155 | ], 156 | "description": "Button 1 released. Metadata: row: 1, col: 1, timestamp: 25883" 157 | }, 158 | { 159 | "data": [ 160 | "020100010000000066be00000000000000000000000000000000000000000000" 161 | ], 162 | "description": "Button 3 pressed. Metadata: row: 1, col: 2, timestamp: 26302" 163 | }, 164 | { 165 | "data": [ 166 | "0201000000000000672f00000000000000000000000000000000000000000000" 167 | ], 168 | "description": "Button 3 released. Metadata: row: 1, col: 2, timestamp: 26415" 169 | }, 170 | { 171 | "data": [ 172 | "0201000001000000689d00000000000000000000000000000000000000000000" 173 | ], 174 | "description": "Button 5 pressed. Metadata: row: 1, col: 3, timestamp: 26781" 175 | }, 176 | { 177 | "data": [ 178 | "0201000000000000690e00000000000000000000000000000000000000000000" 179 | ], 180 | "description": "Button 5 released. Metadata: row: 1, col: 3, timestamp: 26894" 181 | }, 182 | { 183 | "data": [ 184 | "02010000000100006a7500000000000000000000000000000000000000000000" 185 | ], 186 | "description": "Button 7 pressed. Metadata: row: 1, col: 4, timestamp: 27253" 187 | }, 188 | { 189 | "data": [ 190 | "02010000000000006ae300000000000000000000000000000000000000000000" 191 | ], 192 | "description": "Button 7 released. Metadata: row: 1, col: 4, timestamp: 27363" 193 | }, 194 | { 195 | "data": [ 196 | "02010200000000006c4600000000000000000000000000000000000000000000" 197 | ], 198 | "description": "Button 2 pressed. Metadata: row: 1, col: 5, timestamp: 27718" 199 | }, 200 | { 201 | "data": [ 202 | "02010000000000006cb700000000000000000000000000000000000000000000" 203 | ], 204 | "description": "Button 2 released. Metadata: row: 1, col: 5, timestamp: 27831" 205 | }, 206 | { 207 | "data": [ 208 | "02010002000000006e1b00000000000000000000000000000000000000000000" 209 | ], 210 | "description": "Button 4 pressed. Metadata: row: 1, col: 6, timestamp: 28187" 211 | }, 212 | { 213 | "data": [ 214 | "02010000000000006e8c00000000000000000000000000000000000000000000" 215 | ], 216 | "description": "Button 4 released. Metadata: row: 1, col: 6, timestamp: 28300" 217 | }, 218 | { 219 | "data": [ 220 | "02010000020000006fc100000000000000000000000000000000000000000000" 221 | ], 222 | "description": "Button 6 pressed. Metadata: row: 1, col: 7, timestamp: 28609" 223 | }, 224 | { 225 | "data": [ 226 | "0201000000000000704b00000000000000000000000000000000000000000000" 227 | ], 228 | "description": "Button 6 released. Metadata: row: 1, col: 7, timestamp: 28747" 229 | }, 230 | { 231 | "data": [ 232 | "0201000000020000719d00000000000000000000000000000000000000000000" 233 | ], 234 | "description": "Button 8 pressed. Metadata: row: 1, col: 8, timestamp: 29085" 235 | }, 236 | { 237 | "data": [ 238 | "0201000000000000723000000000000000000000000000000000000000000000" 239 | ], 240 | "description": "Button 8 released. Metadata: row: 1, col: 8, timestamp: 29232" 241 | } 242 | ] 243 | } -------------------------------------------------------------------------------- /packages/node/src/__tests__/xkeys.spec.ts: -------------------------------------------------------------------------------- 1 | import * as HID from 'node-hid' 2 | import * as HIDMock from '../__mocks__/node-hid' 3 | import { setupXkeysPanel, XKeys } from '../' 4 | import { getSentData, handleXkeysMessages, resetSentData, sleep } from './lib' 5 | 6 | describe('Unit tests', () => { 7 | afterEach(() => { 8 | HIDMock.resetMockWriteHandler() 9 | }) 10 | test('calculateDelta', () => { 11 | expect(XKeys.calculateDelta(100, 100)).toBe(0) 12 | expect(XKeys.calculateDelta(110, 100)).toBe(10) 13 | expect(XKeys.calculateDelta(90, 100)).toBe(-10) 14 | expect(XKeys.calculateDelta(0, 255)).toBe(1) 15 | expect(XKeys.calculateDelta(5, 250)).toBe(11) 16 | expect(XKeys.calculateDelta(255, 0)).toBe(-1) 17 | expect(XKeys.calculateDelta(250, 5)).toBe(-11) 18 | }) 19 | test('XKeys methods', async () => { 20 | // const panel = new XKeys() 21 | 22 | const hidDevice = { 23 | vendorId: XKeys.vendorId, 24 | productId: 1029, 25 | interface: 0, 26 | path: 'mockPath', 27 | } as HID.Device 28 | 29 | HIDMock.setMockWriteHandler(handleXkeysMessages) 30 | 31 | const myXkeysPanel = await setupXkeysPanel(hidDevice) 32 | 33 | const onError = jest.fn(console.log) 34 | 35 | myXkeysPanel.on('error', onError) 36 | 37 | resetSentData() 38 | 39 | expect(myXkeysPanel.firmwareVersion).toBe(1) 40 | resetSentData() 41 | expect(myXkeysPanel.unitId).toBe(0) 42 | resetSentData() 43 | expect(myXkeysPanel.info).toMatchSnapshot() 44 | resetSentData() 45 | myXkeysPanel.getButtons() 46 | await myXkeysPanel.flush() 47 | expect(getSentData()).toMatchSnapshot() 48 | resetSentData() 49 | myXkeysPanel.setIndicatorLED(5, true) 50 | await myXkeysPanel.flush() 51 | expect(getSentData()).toMatchSnapshot() 52 | resetSentData() 53 | myXkeysPanel.setIndicatorLED(5, false) 54 | await myXkeysPanel.flush() 55 | expect(getSentData()).toMatchSnapshot() 56 | resetSentData() 57 | 58 | myXkeysPanel.setIndicatorLED(5, true, true) 59 | await myXkeysPanel.flush() 60 | expect(getSentData()).toMatchSnapshot() 61 | resetSentData() 62 | 63 | myXkeysPanel.setBacklight(5, '59f') 64 | await myXkeysPanel.flush() 65 | expect(getSentData()).toMatchSnapshot() 66 | resetSentData() 67 | myXkeysPanel.setBacklight(5, '5599ff') 68 | await myXkeysPanel.flush() 69 | expect(getSentData()).toMatchSnapshot() 70 | resetSentData() 71 | myXkeysPanel.setBacklight(5, '#5599ff') 72 | await myXkeysPanel.flush() 73 | expect(getSentData()).toMatchSnapshot() 74 | resetSentData() 75 | myXkeysPanel.setBacklight(5, { r: 45, g: 210, b: 255 }) 76 | await myXkeysPanel.flush() 77 | expect(getSentData()).toMatchSnapshot() 78 | resetSentData() 79 | myXkeysPanel.setBacklight(5, true) 80 | await myXkeysPanel.flush() 81 | expect(getSentData()).toMatchSnapshot() 82 | resetSentData() 83 | myXkeysPanel.setBacklight(5, false) 84 | await myXkeysPanel.flush() 85 | expect(getSentData()).toMatchSnapshot() 86 | resetSentData() 87 | myXkeysPanel.setBacklight(5, null) 88 | await myXkeysPanel.flush() 89 | expect(getSentData()).toMatchSnapshot() 90 | resetSentData() 91 | myXkeysPanel.setBacklight(5, null) 92 | await myXkeysPanel.flush() 93 | expect(getSentData()).toMatchSnapshot() 94 | resetSentData() 95 | myXkeysPanel.setBacklight(5, '59f', true) 96 | await myXkeysPanel.flush() 97 | expect(getSentData()).toMatchSnapshot() 98 | resetSentData() 99 | 100 | myXkeysPanel.setAllBacklights('59f') 101 | await myXkeysPanel.flush() 102 | expect(getSentData()).toMatchSnapshot() 103 | resetSentData() 104 | myXkeysPanel.setAllBacklights('5599ff') 105 | await myXkeysPanel.flush() 106 | expect(getSentData()).toMatchSnapshot() 107 | resetSentData() 108 | myXkeysPanel.setAllBacklights('#5599ff') 109 | await myXkeysPanel.flush() 110 | expect(getSentData()).toMatchSnapshot() 111 | resetSentData() 112 | myXkeysPanel.setAllBacklights({ r: 45, g: 210, b: 255 }) 113 | await myXkeysPanel.flush() 114 | expect(getSentData()).toMatchSnapshot() 115 | resetSentData() 116 | myXkeysPanel.setAllBacklights(true) 117 | await myXkeysPanel.flush() 118 | expect(getSentData()).toMatchSnapshot() 119 | resetSentData() 120 | myXkeysPanel.setAllBacklights(false) 121 | await myXkeysPanel.flush() 122 | expect(getSentData()).toMatchSnapshot() 123 | resetSentData() 124 | myXkeysPanel.setAllBacklights(null) 125 | await myXkeysPanel.flush() 126 | expect(getSentData()).toMatchSnapshot() 127 | resetSentData() 128 | myXkeysPanel.setAllBacklights(null) 129 | await myXkeysPanel.flush() 130 | expect(getSentData()).toMatchSnapshot() 131 | resetSentData() 132 | 133 | myXkeysPanel.toggleAllBacklights() 134 | await myXkeysPanel.flush() 135 | expect(getSentData()).toMatchSnapshot() 136 | resetSentData() 137 | myXkeysPanel.setBacklightIntensity(100) 138 | await myXkeysPanel.flush() 139 | expect(getSentData()).toMatchSnapshot() 140 | resetSentData() 141 | myXkeysPanel.setBacklightIntensity(0, 255) 142 | await myXkeysPanel.flush() 143 | expect(getSentData()).toMatchSnapshot() 144 | resetSentData() 145 | myXkeysPanel.saveBackLights() 146 | await myXkeysPanel.flush() 147 | expect(getSentData()).toMatchSnapshot() 148 | resetSentData() 149 | 150 | myXkeysPanel.setFrequency(127) 151 | await myXkeysPanel.flush() 152 | expect(getSentData()).toMatchSnapshot() 153 | resetSentData() 154 | myXkeysPanel.setUnitId(42) 155 | await myXkeysPanel.flush() 156 | expect(getSentData()).toMatchSnapshot() 157 | resetSentData() 158 | myXkeysPanel.rebootDevice() 159 | await myXkeysPanel.flush() 160 | expect(getSentData()).toMatchSnapshot() 161 | resetSentData() 162 | // expect(myXkeysPanel.writeLcdDisplay(line: number, displayChar: string, backlight: boolean) 163 | await myXkeysPanel.flush() 164 | // expect(getSentData()).toMatchSnapshot() 165 | // resetSentData() 166 | 167 | myXkeysPanel.writeData([0, 1, 2, 3, 4]) 168 | await myXkeysPanel.flush() 169 | expect(getSentData()).toMatchSnapshot() 170 | resetSentData() 171 | 172 | expect(onError).toHaveBeenCalledTimes(0) 173 | }) 174 | test('flush()', async () => { 175 | const hidDevice = { 176 | vendorId: XKeys.vendorId, 177 | productId: 1029, 178 | interface: 0, 179 | path: 'mockPath', 180 | } as HID.Device 181 | 182 | const mockWriteStart = jest.fn() 183 | const mockWriteEnd = jest.fn() 184 | HIDMock.setMockWriteHandler(async (hid, message) => { 185 | mockWriteStart() 186 | await sleep(10) 187 | mockWriteEnd() 188 | handleXkeysMessages(hid, message) 189 | }) 190 | 191 | const myXkeysPanel = await setupXkeysPanel(hidDevice) 192 | 193 | const errorListener = jest.fn(console.error) 194 | myXkeysPanel.on('error', errorListener) 195 | 196 | mockWriteStart.mockClear() 197 | mockWriteEnd.mockClear() 198 | 199 | myXkeysPanel.toggleAllBacklights() 200 | 201 | expect(mockWriteStart).toBeCalledTimes(1) 202 | expect(mockWriteEnd).toBeCalledTimes(0) // Should not have been called yet 203 | 204 | // cleanup: 205 | await myXkeysPanel.flush() // waits for all writes to finish 206 | 207 | expect(mockWriteEnd).toBeCalledTimes(1) 208 | 209 | await myXkeysPanel.close() // close the device. 210 | myXkeysPanel.off('error', errorListener) 211 | 212 | expect(errorListener).toHaveBeenCalledTimes(0) 213 | }) 214 | test('flush() with error', async () => { 215 | const hidDevice = { 216 | vendorId: XKeys.vendorId, 217 | productId: 1029, 218 | interface: 0, 219 | path: 'mockPath', 220 | } as HID.Device 221 | 222 | const mockWriteStart = jest.fn() 223 | const mockWriteEnd = jest.fn() 224 | HIDMock.setMockWriteHandler(async (hid, message) => { 225 | mockWriteStart() 226 | await sleep(10) 227 | mockWriteEnd() 228 | // console.log('message', message) 229 | 230 | if (message[0] === 0 && message[1] === 184) { 231 | // toggleAllBacklights 232 | throw new Error('Mock error') 233 | } 234 | 235 | handleXkeysMessages(hid, message) 236 | }) 237 | 238 | const myXkeysPanel = await setupXkeysPanel(hidDevice) 239 | 240 | const errorListener = jest.fn((e) => { 241 | if (`${e}`.includes('Mock error')) return // ignore 242 | console.error(e) 243 | }) 244 | myXkeysPanel.on('error', errorListener) 245 | 246 | mockWriteStart.mockClear() 247 | mockWriteEnd.mockClear() 248 | 249 | myXkeysPanel.toggleAllBacklights() 250 | 251 | expect(mockWriteStart).toBeCalledTimes(1) 252 | expect(errorListener).toBeCalledTimes(0) // Should not have been called yet 253 | 254 | // cleanup: 255 | await myXkeysPanel.flush() // waits for all writes to finish 256 | 257 | expect(errorListener).toBeCalledTimes(1) 258 | errorListener.mockClear() 259 | 260 | await myXkeysPanel.close() // close the device. 261 | myXkeysPanel.off('error', errorListener) 262 | 263 | expect(errorListener).toHaveBeenCalledTimes(0) 264 | }) 265 | }) 266 | -------------------------------------------------------------------------------- /packages/core/src/watcher.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { XKeys } from './xkeys' 3 | 4 | export interface XKeysWatcherOptions { 5 | /** 6 | * This activates the "Automatic UnitId mode", which enables several features: 7 | * First, any x-keys panel with unitId===0 will be issued a (pseudo unique) unitId upon connection, in order for it to be uniquely identified. 8 | * This allows for the connection-events to work a bit differently, mainly enabling the "reconnected"-event for when a panel has been disconnected, then reconnected again. 9 | */ 10 | automaticUnitIdMode?: boolean 11 | 12 | /** If set, will use polling for devices instead of watching for them directly. Might be a bit slower, but is more compatible. */ 13 | usePolling?: boolean 14 | /** If usePolling is set, the interval to use for checking for new devices. */ 15 | pollingInterval?: number 16 | } 17 | 18 | export interface XKeysWatcherEvents { 19 | // Note: This interface defines strong typings for any events that are emitted by the XKeysWatcher class. 20 | 21 | connected: (xkeysPanel: XKeys) => void 22 | error: (err: any) => void 23 | } 24 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 25 | export declare interface GenericXKeysWatcher { 26 | on(event: U, listener: XKeysWatcherEvents[U]): this 27 | emit(event: U, ...args: Parameters): boolean 28 | } 29 | /** 30 | * Set up a watcher for newly connected X-keys panels. 31 | * Note: It is highly recommended to set up a listener for the disconnected event on the X-keys panel, to clean up after a disconnected device. 32 | */ 33 | export abstract class GenericXKeysWatcher extends EventEmitter { 34 | private updateConnectedDevicesTimeout: NodeJS.Timeout | null = null 35 | private updateConnectedDevicesIsRunning = false 36 | private updateConnectedDevicesRunAgain = false 37 | 38 | private seenDevices = new Set() 39 | private setupXkeys = new Map() 40 | 41 | /** A value that is incremented whenever we expect to find a new or removed device in updateConnectedDevices(). */ 42 | private shouldFindChangedReTries = 0 43 | 44 | protected isActive = true 45 | 46 | public debug = false 47 | /** A list of the devices we've called setupNewDevice() for */ 48 | // private setupXkeysPanels: XKeys[] = [] 49 | private prevConnectedIdentifiers: { [key: string]: XKeys } = {} 50 | /** Unique unitIds grouped into productId groups. */ 51 | private uniqueIds = new Map() 52 | 53 | constructor(private _options?: XKeysWatcherOptions) { 54 | super() 55 | 56 | // Do a sweep for all currently connected X-keys panels: 57 | this.triggerUpdateConnectedDevices(false) 58 | } 59 | protected get options(): Required { 60 | return { 61 | automaticUnitIdMode: this._options?.automaticUnitIdMode ?? false, 62 | usePolling: this._options?.usePolling ?? false, 63 | pollingInterval: this._options?.pollingInterval ?? 1000, 64 | } 65 | } 66 | /** 67 | * Stop the watcher 68 | * @param closeAllDevices Set to false in order to NOT close all devices. Use this if you only want to stop the watching. Defaults to true 69 | */ 70 | public async stop(closeAllDevices = true): Promise { 71 | // To be implemented by the subclass and call super.stop() at the end 72 | 73 | this.isActive = false 74 | 75 | if (closeAllDevices) { 76 | // In order for an application to close gracefully, 77 | // we need to close all devices that we've called setupXkeysPanel() on: 78 | 79 | await Promise.all( 80 | Array.from(this.seenDevices.keys()).map(async (device) => this.handleRemovedDevice(device)) 81 | ) 82 | } 83 | } 84 | 85 | protected triggerUpdateConnectedDevices(somethingWasAddedOrRemoved: boolean): void { 86 | if (somethingWasAddedOrRemoved) { 87 | this.shouldFindChangedReTries++ 88 | } 89 | 90 | if (this.updateConnectedDevicesIsRunning) { 91 | // It is already running, so we'll run it again later, when it's done: 92 | this.updateConnectedDevicesRunAgain = true 93 | return 94 | } else if (this.updateConnectedDevicesTimeout) { 95 | // It is already scheduled to run. 96 | 97 | if (somethingWasAddedOrRemoved) { 98 | // Set it to run now: 99 | clearTimeout(this.updateConnectedDevicesTimeout) 100 | this.updateConnectedDevicesTimeout = null 101 | } else { 102 | return 103 | } 104 | } 105 | 106 | if (!this.updateConnectedDevicesTimeout) { 107 | this.updateConnectedDevicesRunAgain = false 108 | this.updateConnectedDevicesTimeout = setTimeout( 109 | () => { 110 | this.updateConnectedDevicesTimeout = null 111 | this.updateConnectedDevicesIsRunning = true 112 | 113 | this.updateConnectedDevices() 114 | .catch(console.error) 115 | .finally(() => { 116 | this.updateConnectedDevicesIsRunning = false 117 | if (this.updateConnectedDevicesRunAgain) this.triggerUpdateConnectedDevices(false) 118 | }) 119 | }, 120 | somethingWasAddedOrRemoved ? 10 : Math.min(this.options.pollingInterval * 0.5, 300) 121 | ) 122 | } 123 | } 124 | protected abstract getConnectedDevices(): Promise> 125 | protected abstract setupXkeysPanel(device: HID_Identifier): Promise 126 | 127 | private async updateConnectedDevices(): Promise { 128 | this.debugLog('updateConnectedDevices') 129 | 130 | const connectedDevices = await this.getConnectedDevices() 131 | 132 | let removed = 0 133 | let added = 0 134 | // Removed devices: 135 | for (const device of this.seenDevices.keys()) { 136 | if (!connectedDevices.has(device)) { 137 | // A device has been removed 138 | this.debugLog('removed') 139 | removed++ 140 | 141 | await this.handleRemovedDevice(device) 142 | } 143 | } 144 | // Added devices: 145 | for (const connectedDevice of connectedDevices.keys()) { 146 | if (!this.seenDevices.has(connectedDevice)) { 147 | // A device has been added 148 | this.debugLog('added') 149 | added++ 150 | this.seenDevices.add(connectedDevice) 151 | this.handleNewDevice(connectedDevice) 152 | } 153 | } 154 | if (this.shouldFindChangedReTries > 0 && (added === 0 || removed === 0)) { 155 | // We expected to find something changed, but didn't. 156 | // Try again later: 157 | this.shouldFindChangedReTries-- 158 | this.triggerUpdateConnectedDevices(false) 159 | } else { 160 | this.shouldFindChangedReTries = 0 161 | } 162 | } 163 | 164 | private handleNewDevice(device: HID_Identifier): void { 165 | // This is called when a new device has been added / connected 166 | 167 | this.setupXkeysPanel(device) 168 | .then(async (xKeysPanel: XKeys) => { 169 | // Since this is async, check if the panel is still connected: 170 | if (this.seenDevices.has(device)) { 171 | await this.setupNewDevice(device, xKeysPanel) 172 | } else { 173 | await this.handleRemovedDevice(device) 174 | } 175 | }) 176 | .catch((err) => { 177 | this.emit('error', err) 178 | }) 179 | } 180 | private async handleRemovedDevice(device: HID_Identifier) { 181 | // This is called when a device has been removed / disconnected 182 | this.seenDevices.delete(device) 183 | 184 | const xkeys = this.setupXkeys.get(device) 185 | this.debugLog('aa') 186 | if (xkeys) { 187 | this.debugLog('bb') 188 | await xkeys._handleDeviceDisconnected() 189 | this.setupXkeys.delete(device) 190 | } 191 | } 192 | 193 | private async setupNewDevice(device: HID_Identifier, xKeysPanel: XKeys): Promise { 194 | // Store for future reference: 195 | this.setupXkeys.set(device, xKeysPanel) 196 | 197 | xKeysPanel.once('disconnected', () => { 198 | this.handleRemovedDevice(device).catch((e) => this.emit('error', e)) 199 | }) 200 | 201 | // this.setupXkeysPanels.push(xkeysPanel) 202 | 203 | if (this.options.automaticUnitIdMode) { 204 | if (xKeysPanel.unitId === 0) { 205 | // if it is 0, we assume that it's new from the factory and can be safely changed 206 | xKeysPanel.setUnitId(this._getNextUniqueId(xKeysPanel)) // the lookup-cache is stored either in memory, or preferably on disk 207 | } 208 | // the PID+UID pair is enough to uniquely identify a panel. 209 | const uniqueIdentifier: string = xKeysPanel.uniqueId 210 | const previousXKeysPanel = this.prevConnectedIdentifiers[uniqueIdentifier] 211 | if (previousXKeysPanel) { 212 | // This panel has been connected before. 213 | 214 | // We want the XKeys-instance to emit a 'reconnected' event. 215 | // This means that we kill off the newly created xkeysPanel, and 216 | 217 | await previousXKeysPanel._handleDeviceReconnected( 218 | xKeysPanel._getHIDDevice(), 219 | xKeysPanel._getDeviceInfo() 220 | ) 221 | } else { 222 | // It seems that this panel hasn't been connected before 223 | this.emit('connected', xKeysPanel) 224 | this.prevConnectedIdentifiers[uniqueIdentifier] = xKeysPanel 225 | } 226 | } else { 227 | // Default behavior: 228 | this.emit('connected', xKeysPanel) 229 | } 230 | } 231 | private _getNextUniqueId(xkeysPanel: XKeys): number { 232 | let nextId = this.uniqueIds.get(xkeysPanel.info.productId) 233 | if (!nextId) { 234 | nextId = 32 // Starting at 32 235 | } else { 236 | nextId++ 237 | } 238 | if (nextId > 255) throw new Error('No more unique ids available!') 239 | 240 | this.uniqueIds.set(xkeysPanel.info.productId, nextId) 241 | 242 | return nextId 243 | } 244 | 245 | protected debugLog(...args: any[]): void { 246 | if (this.debug) console.log(...args) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/recordings/1049_XK-16 Stick.json: -------------------------------------------------------------------------------- 1 | { 2 | "device": { 3 | "name": "XK-16 HID", 4 | "productId": 1049 5 | }, 6 | "info": { 7 | "name": "XK-16 Stick", 8 | "productId": 1049, 9 | "interface": 0, 10 | "unitId": 2, 11 | "firmwareVersion": 14, 12 | "colCount": 16, 13 | "rowCount": 1, 14 | "layout": [], 15 | "hasPS": true, 16 | "hasJoystick": 0, 17 | "hasJog": 0, 18 | "hasShuttle": 0, 19 | "hasTbar": 0, 20 | "hasLCD": false, 21 | "hasGPIO": false, 22 | "hasSerialData": false, 23 | "hasDMX": false 24 | }, 25 | "errors": [], 26 | "actions": [ 27 | { 28 | "sentData": [ 29 | "00b306010000000000000000000000000000000000000000000000000000000000000000" 30 | ], 31 | "method": "setIndicatorLED", 32 | "arguments": [ 33 | 1, 34 | true 35 | ], 36 | "anomaly": "" 37 | }, 38 | { 39 | "sentData": [ 40 | "00b306000000000000000000000000000000000000000000000000000000000000000000" 41 | ], 42 | "method": "setIndicatorLED", 43 | "arguments": [ 44 | 1, 45 | false 46 | ], 47 | "anomaly": "" 48 | }, 49 | { 50 | "sentData": [ 51 | "00b307010000000000000000000000000000000000000000000000000000000000000000" 52 | ], 53 | "method": "setIndicatorLED", 54 | "arguments": [ 55 | 2, 56 | true 57 | ], 58 | "anomaly": "" 59 | }, 60 | { 61 | "sentData": [ 62 | "00b307000000000000000000000000000000000000000000000000000000000000000000" 63 | ], 64 | "method": "setIndicatorLED", 65 | "arguments": [ 66 | 2, 67 | false 68 | ], 69 | "anomaly": "" 70 | }, 71 | { 72 | "sentData": [ 73 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 74 | "00b601ff0000000000000000000000000000000000000000000000000000000000000000" 75 | ], 76 | "method": "setAllBacklights", 77 | "arguments": [ 78 | "ffffff" 79 | ], 80 | "anomaly": "" 81 | }, 82 | { 83 | "sentData": [ 84 | "00b600ff0000000000000000000000000000000000000000000000000000000000000000", 85 | "00b601000000000000000000000000000000000000000000000000000000000000000000" 86 | ], 87 | "method": "setAllBacklights", 88 | "arguments": [ 89 | "blue" 90 | ], 91 | "anomaly": "" 92 | }, 93 | { 94 | "sentData": [ 95 | "00b600000000000000000000000000000000000000000000000000000000000000000000", 96 | "00b601000000000000000000000000000000000000000000000000000000000000000000" 97 | ], 98 | "method": "setAllBacklights", 99 | "arguments": [ 100 | false 101 | ], 102 | "anomaly": "" 103 | }, 104 | { 105 | "sentData": [ 106 | "00b500010100000000000000000000000000000000000000000000000000000000000000" 107 | ], 108 | "method": "setBacklight", 109 | "arguments": [ 110 | 1, 111 | "00f" 112 | ], 113 | "anomaly": "" 114 | }, 115 | { 116 | "sentData": [ 117 | "00b500020100000000000000000000000000000000000000000000000000000000000000" 118 | ], 119 | "method": "setBacklight", 120 | "arguments": [ 121 | 1, 122 | "00f", 123 | true 124 | ], 125 | "anomaly": "" 126 | }, 127 | { 128 | "sentData": [ 129 | "00b500000100000000000000000000000000000000000000000000000000000000000000" 130 | ], 131 | "method": "setBacklight", 132 | "arguments": [ 133 | 1, 134 | "000" 135 | ], 136 | "anomaly": "" 137 | } 138 | ], 139 | "events": [ 140 | { 141 | "data": [ 142 | "0200010000000000457b00000000000000000000000000000000000000000000" 143 | ], 144 | "description": "Button 1 pressed. Metadata: row: 1, col: 1, timestamp: 17787" 145 | }, 146 | { 147 | "data": [ 148 | "0200000000000000461a00000000000000000000000000000000000000000000" 149 | ], 150 | "description": "Button 1 released. Metadata: row: 1, col: 1, timestamp: 17946" 151 | }, 152 | { 153 | "data": [ 154 | "02000001000000004cd000000000000000000000000000000000000000000000" 155 | ], 156 | "description": "Button 5 pressed. Metadata: row: 1, col: 2, timestamp: 19664" 157 | }, 158 | { 159 | "data": [ 160 | "02000000000000004d3f00000000000000000000000000000000000000000000" 161 | ], 162 | "description": "Button 5 released. Metadata: row: 1, col: 2, timestamp: 19775" 163 | }, 164 | { 165 | "data": [ 166 | "02000000010000004e9100000000000000000000000000000000000000000000" 167 | ], 168 | "description": "Button 9 pressed. Metadata: row: 1, col: 3, timestamp: 20113" 169 | }, 170 | { 171 | "data": [ 172 | "02000000000000004efe00000000000000000000000000000000000000000000" 173 | ], 174 | "description": "Button 9 released. Metadata: row: 1, col: 3, timestamp: 20222" 175 | }, 176 | { 177 | "data": [ 178 | "0200000000010000504a00000000000000000000000000000000000000000000" 179 | ], 180 | "description": "Button 13 pressed. Metadata: row: 1, col: 4, timestamp: 20554" 181 | }, 182 | { 183 | "data": [ 184 | "020000000000000050d300000000000000000000000000000000000000000000" 185 | ], 186 | "description": "Button 13 released. Metadata: row: 1, col: 4, timestamp: 20691" 187 | }, 188 | { 189 | "data": [ 190 | "020002000000000051ce00000000000000000000000000000000000000000000" 191 | ], 192 | "description": "Button 2 pressed. Metadata: row: 1, col: 5, timestamp: 20942" 193 | }, 194 | { 195 | "data": [ 196 | "0200000000000000523e00000000000000000000000000000000000000000000" 197 | ], 198 | "description": "Button 2 released. Metadata: row: 1, col: 5, timestamp: 21054" 199 | }, 200 | { 201 | "data": [ 202 | "0200000200000000534a00000000000000000000000000000000000000000000" 203 | ], 204 | "description": "Button 6 pressed. Metadata: row: 1, col: 6, timestamp: 21322" 205 | }, 206 | { 207 | "data": [ 208 | "020000000000000053b600000000000000000000000000000000000000000000" 209 | ], 210 | "description": "Button 6 released. Metadata: row: 1, col: 6, timestamp: 21430" 211 | }, 212 | { 213 | "data": [ 214 | "020000000200000054de00000000000000000000000000000000000000000000" 215 | ], 216 | "description": "Button 10 pressed. Metadata: row: 1, col: 7, timestamp: 21726" 217 | }, 218 | { 219 | "data": [ 220 | "0200000000000000554a00000000000000000000000000000000000000000000" 221 | ], 222 | "description": "Button 10 released. Metadata: row: 1, col: 7, timestamp: 21834" 223 | }, 224 | { 225 | "data": [ 226 | "0200000000020000565900000000000000000000000000000000000000000000" 227 | ], 228 | "description": "Button 14 pressed. Metadata: row: 1, col: 8, timestamp: 22105" 229 | }, 230 | { 231 | "data": [ 232 | "020000000000000056be00000000000000000000000000000000000000000000" 233 | ], 234 | "description": "Button 14 released. Metadata: row: 1, col: 8, timestamp: 22206" 235 | }, 236 | { 237 | "data": [ 238 | "020004000000000057f300000000000000000000000000000000000000000000" 239 | ], 240 | "description": "Button 3 pressed. Metadata: row: 1, col: 9, timestamp: 22515" 241 | }, 242 | { 243 | "data": [ 244 | "0200000000000000586b00000000000000000000000000000000000000000000" 245 | ], 246 | "description": "Button 3 released. Metadata: row: 1, col: 9, timestamp: 22635" 247 | }, 248 | { 249 | "data": [ 250 | "0200000400000000599500000000000000000000000000000000000000000000" 251 | ], 252 | "description": "Button 7 pressed. Metadata: row: 1, col: 10, timestamp: 22933" 253 | }, 254 | { 255 | "data": [ 256 | "02000000000000005a1d00000000000000000000000000000000000000000000" 257 | ], 258 | "description": "Button 7 released. Metadata: row: 1, col: 10, timestamp: 23069" 259 | }, 260 | { 261 | "data": [ 262 | "02000000040000005b1d00000000000000000000000000000000000000000000" 263 | ], 264 | "description": "Button 11 pressed. Metadata: row: 1, col: 11, timestamp: 23325" 265 | }, 266 | { 267 | "data": [ 268 | "02000000000000005ba400000000000000000000000000000000000000000000" 269 | ], 270 | "description": "Button 11 released. Metadata: row: 1, col: 11, timestamp: 23460" 271 | }, 272 | { 273 | "data": [ 274 | "02000000000400005caf00000000000000000000000000000000000000000000" 275 | ], 276 | "description": "Button 15 pressed. Metadata: row: 1, col: 12, timestamp: 23727" 277 | }, 278 | { 279 | "data": [ 280 | "02000000000000005d2400000000000000000000000000000000000000000000" 281 | ], 282 | "description": "Button 15 released. Metadata: row: 1, col: 12, timestamp: 23844" 283 | }, 284 | { 285 | "data": [ 286 | "02000800000000005e3c00000000000000000000000000000000000000000000" 287 | ], 288 | "description": "Button 4 pressed. Metadata: row: 1, col: 13, timestamp: 24124" 289 | }, 290 | { 291 | "data": [ 292 | "02000000000000005eaf00000000000000000000000000000000000000000000" 293 | ], 294 | "description": "Button 4 released. Metadata: row: 1, col: 13, timestamp: 24239" 295 | }, 296 | { 297 | "data": [ 298 | "02000008000000005fbe00000000000000000000000000000000000000000000" 299 | ], 300 | "description": "Button 8 pressed. Metadata: row: 1, col: 14, timestamp: 24510" 301 | }, 302 | { 303 | "data": [ 304 | "0200000000000000602700000000000000000000000000000000000000000000" 305 | ], 306 | "description": "Button 8 released. Metadata: row: 1, col: 14, timestamp: 24615" 307 | }, 308 | { 309 | "data": [ 310 | "0200000008000000614500000000000000000000000000000000000000000000" 311 | ], 312 | "description": "Button 12 pressed. Metadata: row: 1, col: 15, timestamp: 24901" 313 | }, 314 | { 315 | "data": [ 316 | "020000000000000061b400000000000000000000000000000000000000000000" 317 | ], 318 | "description": "Button 12 released. Metadata: row: 1, col: 15, timestamp: 25012" 319 | }, 320 | { 321 | "data": [ 322 | "020000000008000062c600000000000000000000000000000000000000000000" 323 | ], 324 | "description": "Button 16 pressed. Metadata: row: 1, col: 16, timestamp: 25286" 325 | }, 326 | { 327 | "data": [ 328 | "0200000000000000634300000000000000000000000000000000000000000000" 329 | ], 330 | "description": "Button 16 released. Metadata: row: 1, col: 16, timestamp: 25411" 331 | }, 332 | { 333 | "data": [ 334 | "020100000000000069aa00000000000000000000000000000000000000000000" 335 | ], 336 | "description": "Button 0 pressed. Metadata: row: 0, col: 0, timestamp: 27050" 337 | }, 338 | { 339 | "data": [ 340 | "020000000000000069e500000000000000000000000000000000000000000000" 341 | ], 342 | "description": "Button 0 released. Metadata: row: 0, col: 0, timestamp: 27109" 343 | } 344 | ] 345 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [3.3.0](https://github.com/SuperFlyTV/xkeys/compare/v3.2.0...v3.3.0) (2024-12-09) 7 | 8 | 9 | ### Features 10 | 11 | * add flush() method, resolves [#106](https://github.com/SuperFlyTV/xkeys/issues/106) ([f0ade46](https://github.com/SuperFlyTV/xkeys/commit/f0ade467a900500fdeaf55603ae729f136316746)) 12 | 13 | 14 | 15 | 16 | 17 | # [3.2.0](https://github.com/SuperFlyTV/xkeys/compare/v3.1.2...v3.2.0) (2024-08-26) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * add 'disconnect' event for the webHID device ([44179d3](https://github.com/SuperFlyTV/xkeys/commit/44179d374bccf730bd0caf9fee6605359f48cf03)) 23 | 24 | 25 | ### Features 26 | 27 | * Add XkeysWatcher to WebHID version, rework XkeysWatcher to share code between node & webHID versions ([34bbd3c](https://github.com/SuperFlyTV/xkeys/commit/34bbd3cbd765d97f3d4f52690f78d4cfef5817a2)) 28 | 29 | 30 | 31 | 32 | 33 | ## [3.1.2](https://github.com/SuperFlyTV/xkeys/compare/v3.1.1...v3.1.2) (2024-08-12) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * event listeners in node-hid-wapper to follow style in web-hid-wrapper. ([ee1d6c6](https://github.com/SuperFlyTV/xkeys/commit/ee1d6c6c110ddb70fbdeafd389c9c4504ee17f8c)) 39 | 40 | 41 | 42 | 43 | 44 | ## [3.1.1](https://github.com/SuperFlyTV/xkeys/compare/v3.1.0...v3.1.1) (2024-03-04) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * clarify which github page. ([07d7b4f](https://github.com/SuperFlyTV/xkeys/commit/07d7b4f2402ffcaeeba375f5e7f74b4df9eb8de3)) 50 | 51 | 52 | 53 | 54 | 55 | # [3.1.0](https://github.com/SuperFlyTV/xkeys/compare/v3.0.1...v3.1.0) (2024-01-11) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | 61 | * remove hack (possible HID.HID that exposed a devicePath) ([fca382d](https://github.com/SuperFlyTV/xkeys/commit/fca382dd5109a8447ed7ba51d485de255487bd6d)) 62 | * remove support for HID.HID and HID.Async devices in setupXKeysPanel. ([1bc87ba](https://github.com/SuperFlyTV/xkeys/commit/1bc87ba26227831eb7f312e59eb15f9ed47497e1)) 63 | * support providing HID.HIDAsync into setupXkeysPanel() ([190d4a1](https://github.com/SuperFlyTV/xkeys/commit/190d4a1c2dfa1232b250318c30131624cf67fb23)) 64 | * typo ([095c064](https://github.com/SuperFlyTV/xkeys/commit/095c0640a52b920774965192cfb868badb82f012)) 65 | 66 | 67 | ### Features 68 | 69 | * expose Xkeys.filterDevice() static method, used to filter for compatible X-keys devices when manually handling HID devices ([ab542a8](https://github.com/SuperFlyTV/xkeys/commit/ab542a8630c749f79cd21c4589eb263c6017ea99)) 70 | * use async node-hid ([429c5ea](https://github.com/SuperFlyTV/xkeys/commit/429c5ea6e83f5a8a025180d3c6a15943bddaf5d6)) 71 | 72 | 73 | 74 | 75 | 76 | ## [3.0.1](https://github.com/SuperFlyTV/xkeys/compare/v3.0.0...v3.0.1) (2023-11-02) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * filter for correct usage/usagePage when finding xkeys devices ([8b9fdd1](https://github.com/SuperFlyTV/xkeys/commit/8b9fdd1eb69abf03cfbc67f5b503bd01a8623bc5)) 82 | * filter for correct usage/usagePage when finding xkeys devices ([68f3e86](https://github.com/SuperFlyTV/xkeys/commit/68f3e869139b2a846e2be4209f5201f7e4893494)) 83 | 84 | 85 | 86 | 87 | 88 | # [3.0.0](https://github.com/SuperFlyTV/xkeys/compare/v2.4.0...v3.0.0) (2023-05-03) 89 | 90 | ### Bug Fixes 91 | 92 | - issue with trackball ([5e2021a](https://github.com/SuperFlyTV/xkeys/commit/5e2021af49d12a7367d39f638c375210db343714)) 93 | 94 | ### Features 95 | 96 | - BREAKING CHANGE: Dropped support for EOL versions of Node.js (<14). 97 | - Add support for the [RailDriver](https://raildriver.com/). 98 | - Add support for panels with multiple background-light banks (added an argument for bankIndex to `.setBacklight()` & `.setAllBacklights()`) 99 | - Add support for a few upcoming X-keys panels, including features like trackball, rotary 100 | 101 | # [2.4.0](https://github.com/SuperFlyTV/xkeys/compare/v2.3.4...v2.4.0) (2022-10-26) 102 | 103 | ### Bug Fixes 104 | 105 | - update usb dep ([e1bc906](https://github.com/SuperFlyTV/xkeys/commit/e1bc9060e4ef82dce690a2bb76fb01601ed28f7a)) 106 | 107 | ### Features 108 | 109 | - replace usb-detection with usb ([d6349ef](https://github.com/SuperFlyTV/xkeys/commit/d6349ef0b0477045dd8a540887918cc2af8370aa)) 110 | 111 | ## [2.3.4](https://github.com/SuperFlyTV/xkeys/compare/v2.3.3...v2.3.4) (2022-06-06) 112 | 113 | ### Bug Fixes 114 | 115 | - ignore engines for node10 in ci ([8c88b43](https://github.com/SuperFlyTV/xkeys/commit/8c88b43f9d694ac82d094b82042c82bde25e5bd1)) 116 | - pre-commit hook ([887fbb2](https://github.com/SuperFlyTV/xkeys/commit/887fbb2f9b89369dfaa0ccf851646252af9686af)) 117 | - Watcher: async handling of adding/removing devices ([61f0b28](https://github.com/SuperFlyTV/xkeys/commit/61f0b28571a3df72b49f4bd84b6d842408e86acd)) 118 | 119 | ## [2.3.2](https://github.com/SuperFlyTV/xkeys/compare/v2.3.0...v2.3.2) (2021-12-12) 120 | 121 | ### Bug Fixes 122 | 123 | - add XKeys.writeData() method, used for testing and development ([fba879c](https://github.com/SuperFlyTV/xkeys/commit/fba879c0f93ee64fbcdbd7faf5863998300c2016)) 124 | 125 | # [2.3.0](https://github.com/SuperFlyTV/xkeys/compare/v2.2.1...v2.3.0) (2021-11-28) 126 | 127 | ## [2.3.4](https://github.com/SuperFlyTV/xkeys/compare/v2.3.3...v2.3.4) (2022-06-06) 128 | 129 | ### Bug Fixes 130 | 131 | - ignore engines for node10 in ci ([8c88b43](https://github.com/SuperFlyTV/xkeys/commit/8c88b43f9d694ac82d094b82042c82bde25e5bd1)) 132 | - pre-commit hook ([887fbb2](https://github.com/SuperFlyTV/xkeys/commit/887fbb2f9b89369dfaa0ccf851646252af9686af)) 133 | - Watcher: async handling of adding/removing devices ([61f0b28](https://github.com/SuperFlyTV/xkeys/commit/61f0b28571a3df72b49f4bd84b6d842408e86acd)) 134 | 135 | ## [2.3.2](https://github.com/SuperFlyTV/xkeys/compare/v2.3.0...v2.3.2) (2021-12-12) 136 | 137 | ### Bug Fixes 138 | 139 | - add XKeys.writeData() method, used for testing and development ([fba879c](https://github.com/SuperFlyTV/xkeys/commit/fba879c0f93ee64fbcdbd7faf5863998300c2016)) 140 | 141 | # [2.3.0](https://github.com/SuperFlyTV/xkeys/compare/v2.2.1...v2.3.0) (2021-11-28) 142 | 143 | ### Features 144 | 145 | - add usePolling option to the XKeysWatcher to fall back to polling, since "usb-detection" might not work on all OS:es ([ab31223](https://github.com/SuperFlyTV/xkeys/commit/ab312236b14cb8f961d0b0bf878c611487a5983f)) 146 | 147 | ## [2.2.1](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0...v2.2.1) (2021-09-22) 148 | 149 | **Note:** Version bump only for package xkeys-monorepo 150 | 151 | # [2.2.0](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0-alpha.1...v2.2.0) (2021-09-08) 152 | 153 | **Note:** Version bump only for package xkeys-monorepo 154 | 155 | # [2.2.0-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.2.0-alpha.0...v2.2.0-alpha.1) (2021-09-06) 156 | 157 | ### Bug Fixes 158 | 159 | - re-add devicePath ([349f6a9](https://github.com/SuperFlyTV/xkeys/commit/349f6a93ace9480e18d5ed695186920165fea6e7)) 160 | 161 | # [2.2.0-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1...v2.2.0-alpha.0) (2021-09-06) 162 | 163 | ### Features 164 | 165 | - add feature: "Automatic UnitId mode" ([f7c3a86](https://github.com/SuperFlyTV/xkeys/commit/f7c3a869e8820f856831aad576ce7978dfb9d75c)) 166 | - add XKeys.uniqueId property, to be used with automaticUnitIdMode ([a2e6d7a](https://github.com/SuperFlyTV/xkeys/commit/a2e6d7a6ec917d82bc2a71c1922c22c061232908)) 167 | 168 | ## [2.1.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1-alpha.1...v2.1.1) (2021-05-24) 169 | 170 | **Note:** Version bump only for package xkeys-monorepo 171 | 172 | ## [2.1.1-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.1-alpha.0...v2.1.1-alpha.1) (2021-05-23) 173 | 174 | ### Bug Fixes 175 | 176 | - hack to fix issue in Electron ([501f06d](https://github.com/SuperFlyTV/xkeys/commit/501f06de9a2413832dab4b6a0ef4ef7d2b668967)) 177 | - make XKeysWatcher.stop() close all the devices it has called setupXkeysPanel() for. ([f69b599](https://github.com/SuperFlyTV/xkeys/commit/f69b59912a62b8dcc5ff00a2083c793851bba15c)) 178 | 179 | ## [2.1.1-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0...v2.1.1-alpha.0) (2021-05-23) 180 | 181 | ### Bug Fixes 182 | 183 | - remove listeners on watcher.stop() ([c8d36a3](https://github.com/SuperFlyTV/xkeys/commit/c8d36a3602b8c460233b82a48f6c28a04f52c9de)) 184 | 185 | # [2.1.0](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0-alpha.0...v2.1.0) (2021-05-15) 186 | 187 | **Note:** Version bump only for package xkeys-monorepo 188 | 189 | # [2.1.0-alpha.1](https://github.com/SuperFlyTV/xkeys/compare/v2.1.0-alpha.0...v2.1.0-alpha.1) (2021-05-10) 190 | 191 | **Note:** Version bump only for package xkeys-monorepo 192 | 193 | # [2.1.0-alpha.0](https://github.com/SuperFlyTV/xkeys/compare/v2.0.0...v2.1.0-alpha.0) (2021-05-10) 194 | 195 | ### Bug Fixes 196 | 197 | - publication-script for the node-record-test executable (wip) ([e4a8071](https://github.com/SuperFlyTV/xkeys/commit/e4a80719686048b010976d464adb6a40bf86b3c0)) 198 | - refactor repo into lerna mono-repo ([d5bffc1](https://github.com/SuperFlyTV/xkeys/commit/d5bffc1798e7c8e89ae9fcc4355afd438ea82d3a)) 199 | 200 | ### Features 201 | 202 | - add package with web-HID support ([1f27199](https://github.com/SuperFlyTV/xkeys/commit/1f2719969faf93ba45a2bc767f64543fb9ffe6ea)) 203 | 204 | # Changelog 205 | 206 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 207 | 208 | ## [2.0.0](https://github.com/SuperFlyTV/xkeys/compare/v1.1.1...v2.0.0) (2021-04-16) 209 | 210 | ### Features 211 | 212 | - emit precalculated deltaZ for joystick ([2dad07b](https://github.com/SuperFlyTV/xkeys/commit/2dad07b895ba1c284a708a017eb6e7008e2e15a9)) 213 | - Refactor & improve ([0ce375e](https://github.com/SuperFlyTV/xkeys/commit/0ce375ef4f16ccdfa05623f4382084fecbe4162d)) 214 | 215 | ### Bug Fixes 216 | 217 | - bug for joystick deltaZ ([41cd561](https://github.com/SuperFlyTV/xkeys/commit/41cd5618e48f8b07c9bcbe9a5760c02e3cadb529)) 218 | - compare new values with old, not the other way around ([71e1801](https://github.com/SuperFlyTV/xkeys/commit/71e1801e2fbf5a8c3e9e1f71cc839ada72eb796c)) 219 | - joystick bug ([84d2150](https://github.com/SuperFlyTV/xkeys/commit/84d21503ff670f1e4b6d1f021d1b98b1c661fc55)) 220 | - the emitted timestamp is undefined for some products ([a45ee1f](https://github.com/SuperFlyTV/xkeys/commit/a45ee1fd7982d40ef9b3f97f0ffa6c2a7d928d71)) 221 | - use type imports ([373e6b4](https://github.com/SuperFlyTV/xkeys/commit/373e6b40144e9d51ec064cb50deb315a50f24868)) 222 | - use XKeys.listAllConnectedPanels to DRY it up ([d49827d](https://github.com/SuperFlyTV/xkeys/commit/d49827d093474eeebd9d294c4f7c391c54c5daec)) 223 | 224 | ### [1.1.1](https://github.com/SuperFlyTV/xkeys/compare/v1.1.0...v1.1.1) (2021-01-15) 225 | 226 | ### Bug Fixes 227 | 228 | - remove spammy console.log [release] ([e3a0feb](https://github.com/SuperFlyTV/xkeys/commit/e3a0feb0b48686adc1eb2b431a140c25c721c906)) 229 | 230 | ## [1.1.0](https://github.com/SuperFlyTV/xkeys/compare/v1.0.0...v1.1.0) (2021-01-06) 231 | 232 | ### [1.1.0-0](https://github.com/SuperFlyTV/xkeys/compare/v1.0.0...v1.0.1-0) (2021-01-06) 233 | 234 | - Add support for XKE-124 T-bar ([PR](https://github.com/SuperFlyTV/xkeys/pull/23)) 235 | 236 | ## [1.0.0](https://github.com/SuperFlyTV/xkeys/compare/v0.1.1...v1.0.0) (2020-10-27) 237 | 238 | ### Bug Fixes 239 | 240 | - add (guessed) banks for XK16, XK8 & XK4 ([a47834b](https://github.com/SuperFlyTV/xkeys/commit/a47834be031d29033dd04f5978dd7156c473a282)) 241 | - add best-guesses for banks property, for untested products ([8ecffec](https://github.com/SuperFlyTV/xkeys/commit/8ecffeca442b1b5b06fe683b30a4d05e55fb010f)) 242 | - setBacklightIntensity improvements (thanks to [@jonwyett](https://github.com/jonwyett)) ([a75d330](https://github.com/SuperFlyTV/xkeys/commit/a75d330f2161cf8b9d191feec2985ff14a36689d)) 243 | - typings fixes ([a8a7193](https://github.com/SuperFlyTV/xkeys/commit/a8a7193ba44bc691676161dcb3955d7184c1dbae)) 244 | - updated node-hid dependencies ([0ec22e1](https://github.com/SuperFlyTV/xkeys/commit/0ec22e10e9f471ed6a9555847a7f37a645e75228)) 245 | - upgrade dependencies ([98bb387](https://github.com/SuperFlyTV/xkeys/commit/98bb3878ece0f4e5032d31200ba641b881e40006)) 246 | - use device.interface instead of device.usage ([2883c46](https://github.com/SuperFlyTV/xkeys/commit/2883c466f2ea26585a14b6e9765fa4146ba17554)) 247 | -------------------------------------------------------------------------------- /packages/node-record-test/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as readline from 'readline' 2 | import { HID_Device, listAllConnectedPanels, setupXkeysPanel, XKeys, XKeysEvents, describeEvent } from 'xkeys' 3 | import { exists, fsWriteFile } from './lib' 4 | import { HIDDevice } from '@xkeys-lib/core' 5 | 6 | /* 7 | * This script is intended to be used by developers in order to verify that the functionality works and generate test scripts. 8 | * To run this script, run `ts-node scripts/record-test.ts` in a terminal. 9 | * The output recording is saved to /src/__tests__/recordings/ and should be committed to the repository. 10 | * To verify that the recording works in the unit tests, run `npm run unit`. 11 | */ 12 | 13 | console.log('=============================================') 14 | console.log(' Test recorder for X-keys') 15 | console.log('=============================================') 16 | 17 | // Check if there is one (1) Xkeys panel connected: 18 | 19 | const panels = listAllConnectedPanels() 20 | 21 | if (panels.length !== 1) { 22 | console.log('Make one (and only one) X-keys panel is plugged in, then restart this script!') 23 | console.log(`${panels.length} connected panels found:`) 24 | 25 | panels.forEach((device) => { 26 | console.log(`ProductId: ${device.productId}, Product: ${device.product}`) 27 | }) 28 | askQuestion(`(Click Enter to quit)`) 29 | .then(() => { 30 | // eslint-disable-next-line no-process-exit 31 | process.exit(0) 32 | }) 33 | .catch(console.error) 34 | } else { 35 | console.log(``) 36 | console.log(`Note: To quit this program, hit CTRL+C`) 37 | // console.log(``) 38 | 39 | // console.log(`Follow the instructions below:`) 40 | // console.log(`If anything looks wrong on the screen, abort the recording and report the issue.`) 41 | // console.log(``) 42 | 43 | startRecording(panels[0]).catch((err) => { 44 | console.log('err') 45 | console.log(err) 46 | 47 | askQuestion(`(Click Enter to quit)`) 48 | .then(() => { 49 | // eslint-disable-next-line no-process-exit 50 | process.exit(1) 51 | }) 52 | .catch(console.error) 53 | }) 54 | } 55 | 56 | async function startRecording(panel: HID_Device) { 57 | const xkeys = await setupXkeysPanel(panel) 58 | 59 | xkeys.setAllBacklights(false) 60 | xkeys.setIndicatorLED(0, false) 61 | xkeys.setIndicatorLED(1, false) 62 | 63 | console.log(``) 64 | console.log(`Step 1: Verify that the info below matches the panel you've connected:`) 65 | console.log(``) 66 | 67 | console.log(`Name of panel: "${xkeys.info.name}"`) 68 | console.log(`Product id: "${xkeys.info.productId}"`) 69 | console.log(`Unit id (UID): "${xkeys.info.unitId}"`) 70 | 71 | console.log(`Number of rows: ${xkeys.info.rowCount}`) 72 | console.log(`Number of columnts: ${xkeys.info.colCount}`) 73 | console.log(`Layout(s):`) 74 | xkeys.info.layout.forEach((layout) => { 75 | console.log(` Name: ${layout.name}`) 76 | console.log(` Index: ${layout.index}`) 77 | console.log(` StartRow: ${layout.startRow}`) 78 | console.log(` StartCol: ${layout.startCol}`) 79 | console.log(` EndRow: ${layout.endRow}`) 80 | console.log(` EndCol: ${layout.endCol}`) 81 | console.log(``) 82 | }) 83 | 84 | console.log(`Has PS: ${xkeys.info.hasPS}`) 85 | console.log(`Has Joystick: ${xkeys.info.hasJoystick}`) 86 | console.log(`Has Jog: ${xkeys.info.hasJog}`) 87 | console.log(`Has Shuttle: ${xkeys.info.hasShuttle}`) 88 | console.log(`Has Tbar: ${xkeys.info.hasTbar}`) 89 | console.log(`Has LCD: ${xkeys.info.hasLCD}`) 90 | console.log(`Has GPIO: ${xkeys.info.hasGPIO}`) 91 | console.log(`Has SerialData: ${xkeys.info.hasSerialData}`) 92 | console.log(`Has DMX: ${xkeys.info.hasDMX}`) 93 | 94 | console.log(``) 95 | await askQuestion(`Does this look good? (click Enter to continue)`) 96 | console.log(``) 97 | 98 | const fileName = `${xkeys.info.productId}_${xkeys.info.name}.json` 99 | const path = `${fileName}` 100 | 101 | if (await exists(path)) { 102 | console.log(`Warning: Recording file "${path}" already exists!`) 103 | const answer = await askQuestion('Do you want to overwrite the file (Y/n)?') 104 | if (answer === 'n') { 105 | console.log(`Exiting application`) 106 | // eslint-disable-next-line no-process-exit 107 | process.exit(0) 108 | } 109 | } 110 | 111 | // console.log(``) 112 | // console.log(`------ Starting recording ------`) 113 | // console.log(``) 114 | 115 | const recording: any = { 116 | device: { 117 | name: panel.product, 118 | productId: panel.productId, 119 | }, 120 | info: xkeys.info, 121 | errors: [], 122 | actions: [], 123 | events: [], 124 | } 125 | 126 | const save = async () => { 127 | await fsWriteFile(path, JSON.stringify(recording, undefined, 2)) 128 | } 129 | let triggerSaveRunning = false 130 | let triggerSaveRunAgain = false 131 | let triggerSaveTimeout: NodeJS.Timeout | null = null 132 | const triggerSave = () => { 133 | triggerSaveRunAgain = false 134 | if (triggerSaveRunning) triggerSaveRunAgain = true 135 | if (!triggerSaveTimeout) { 136 | triggerSaveTimeout = setTimeout(() => { 137 | triggerSaveTimeout = null 138 | triggerSaveRunning = true 139 | save() 140 | .then(() => { 141 | triggerSaveRunning = false 142 | if (triggerSaveRunAgain) triggerSave() 143 | }) 144 | .catch((err) => { 145 | triggerSaveRunning = false 146 | if (triggerSaveRunAgain) triggerSave() 147 | console.log(err) 148 | }) 149 | }, 100) 150 | } 151 | } 152 | triggerSave() 153 | 154 | // catch ctrl+c: 155 | process.on('SIGINT', () => { 156 | console.log(`Saved file at "${path}"`) 157 | // eslint-disable-next-line no-process-exit 158 | process.exit(0) 159 | }) 160 | 161 | // Intercept all data sent to the device: 162 | 163 | let bufferedWrites: Buffer[] = [] 164 | 165 | // @ts-expect-error hack 166 | const xkeysDevice = xkeys.device as HIDDevice 167 | 168 | // eslint-disable-next-line @typescript-eslint/unbound-method 169 | const orgWrite = xkeysDevice.write 170 | xkeysDevice.write = (data: number[]) => { 171 | bufferedWrites.push(Buffer.from(data)) 172 | 173 | return orgWrite.call(xkeysDevice, data) 174 | } 175 | const checkAction = async (xkeys: XKeys, question: string, method: string, args: any[]) => { 176 | // @ts-expect-error hack 177 | xkeys[method](...args) 178 | 179 | const anomaly = await askQuestion(question) 180 | 181 | recording.actions.push({ 182 | sentData: bufferedWrites.map((buf) => buf.toString('hex')), 183 | method, 184 | arguments: args, 185 | anomaly, 186 | }) 187 | bufferedWrites = [] 188 | triggerSave() 189 | } 190 | 191 | console.log(``) 192 | console.log( 193 | `Step 2: Don't touch anything on the panel just yet, first we're going to verify a few functionalities.` 194 | ) 195 | 196 | console.log(`On the following questions, click Enter if OK, and write a message if something was off.`) 197 | 198 | console.log(``) 199 | await askQuestion(`Are you ready to start? (click Enter to continue)`) 200 | 201 | await checkAction(xkeys, `Did the first of the LED indicators turn on?`, 'setIndicatorLED', [1, true]) 202 | await checkAction(xkeys, `Did the LED indicator turn off?`, 'setIndicatorLED', [1, false]) 203 | 204 | await checkAction(xkeys, `Did the other LED indicator turn on?`, 'setIndicatorLED', [2, true]) 205 | await checkAction(xkeys, `Did the LED indicator turn off?`, 'setIndicatorLED', [2, false]) 206 | 207 | await checkAction(xkeys, `Did all button backlights turn on (all colors)?`, 'setAllBacklights', ['ffffff']) 208 | 209 | await checkAction(xkeys, `Did all button backlights turn blue?`, 'setAllBacklights', ['blue']) 210 | 211 | await checkAction(xkeys, `Did all button backlights turn off?`, 'setAllBacklights', [false]) 212 | 213 | await checkAction(xkeys, `Did the first button light up blue?`, 'setBacklight', [1, '00f']) 214 | 215 | await checkAction(xkeys, `Did the first button flash blue?`, 'setBacklight', [1, '00f', true]) 216 | 217 | await checkAction(xkeys, `Did the first button light turn off?`, 'setBacklight', [1, '000']) 218 | 219 | // xkeys.toggleAllBacklights() 220 | // await askQuestion(`Did the light turn off?`) 221 | // logAction('toggleAllBacklights', []) 222 | 223 | // setBacklight 224 | // setAllBacklights 225 | // toggleAllBacklights 226 | // saveBackLights 227 | // setFrequency 228 | // setUnitId 229 | // writeLcdDisplay 230 | 231 | console.log(``) 232 | console.log(`-------------------------------------------`) 233 | console.log(`Done with initial checks!`) 234 | console.log(`-----`) 235 | console.log(``) 236 | console.log(`Step 3: Follow the instructions below:`) 237 | console.log(``) 238 | 239 | console.log( 240 | `Press (and release) the buttons and fiddle with all of the analogue inputs on the X-keys panel, one at a time.` 241 | ) 242 | 243 | console.log(`For every action you do, check the following on the screen:`) 244 | console.log(`* The action description on the screen should match what you just did.`) 245 | console.log( 246 | `* (Buttons only) The button should light up in the following pattern: (Blue, Red, Green, White, Black/Off). On non-RGB panels, the lights will fall back to something equivalent.` 247 | ) 248 | 249 | console.log(``) 250 | console.log(`If anything looks wrong on the screen, abort the recording and report the issue.`) 251 | 252 | console.log(``) 253 | console.log(`The resulting files are created in the same folder as this executable`) 254 | console.log(``) 255 | console.log(`After you're done, hit CTRL+C to exit the recording.`) 256 | 257 | let bufferedData: Buffer[] = [] 258 | 259 | // @ts-expect-error hack, private property 260 | xkeys.device.on('data', (data) => { 261 | bufferedData.push(data) 262 | }) 263 | 264 | xkeys.on('error', (err) => { 265 | recording.errors.push(err) 266 | triggerSave() 267 | }) 268 | let colorLoop: any = undefined 269 | const handleEvent = (event: keyof XKeysEvents) => { 270 | xkeys.on(event, (...args: any[]) => { 271 | setImmediate(() => { 272 | const description = describeEvent(event, args) 273 | 274 | recording.events.push({ 275 | data: bufferedData.map((buf) => buf.toString('hex')), 276 | description: description, 277 | }) 278 | bufferedData = [] 279 | console.log(description) 280 | 281 | if (event === 'down') { 282 | const keyIndex = args[0] 283 | colorLoop = doColorLoop(xkeys, keyIndex) 284 | } else if (event === 'up') { 285 | if (colorLoop) { 286 | colorLoop.stop() 287 | colorLoop = undefined 288 | } 289 | } 290 | triggerSave() 291 | }) 292 | }) 293 | } 294 | handleEvent('down') 295 | handleEvent('up') 296 | handleEvent('jog') 297 | handleEvent('shuttle') 298 | handleEvent('joystick') 299 | handleEvent('tbar') 300 | handleEvent('disconnected') 301 | } 302 | 303 | async function askQuestion(query: string): Promise { 304 | const rl = readline.createInterface({ 305 | input: process.stdin, 306 | output: process.stdout, 307 | }) 308 | 309 | return new Promise((resolve) => 310 | rl.question(query, (ans: string | number) => { 311 | rl.close() 312 | resolve(ans) 313 | }) 314 | ) 315 | } 316 | function doColorLoop(xkeys: XKeys, keyIndex: number) { 317 | let active = true 318 | 319 | const doLoop = async () => { 320 | while (active) { 321 | xkeys.setBacklight(keyIndex, '0000ff') 322 | await waitTime(300) 323 | xkeys.setBacklight(keyIndex, '000000') 324 | await waitTime(200) 325 | if (!active) break 326 | 327 | xkeys.setBacklight(keyIndex, 'ff0000') 328 | await waitTime(300) 329 | xkeys.setBacklight(keyIndex, '000000') 330 | await waitTime(200) 331 | if (!active) break 332 | 333 | xkeys.setBacklight(keyIndex, '00ff00') 334 | await waitTime(300) 335 | xkeys.setBacklight(keyIndex, '000000') 336 | await waitTime(200) 337 | if (!active) break 338 | 339 | xkeys.setBacklight(keyIndex, 'ffffff') 340 | await waitTime(300) 341 | xkeys.setBacklight(keyIndex, '000000') 342 | await waitTime(200) 343 | if (!active) break 344 | 345 | xkeys.setBacklight(keyIndex, '000000') 346 | await waitTime(300) 347 | xkeys.setBacklight(keyIndex, '000000') 348 | await waitTime(200) 349 | 350 | await waitTime(1000) 351 | } 352 | xkeys.setBacklight(keyIndex, '000000') 353 | } 354 | 355 | doLoop().catch(console.log) 356 | 357 | return { 358 | stop: () => { 359 | active = false 360 | }, 361 | } 362 | } 363 | async function waitTime(time: number) { 364 | return new Promise((resolve) => setTimeout(resolve, time)) 365 | } 366 | --------------------------------------------------------------------------------