├── .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 |
--------------------------------------------------------------------------------