From 75bb6f5eaaa45b28f2688a9e5fd244788cf0ffd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Thu, 6 Aug 2026 12:57:07 +0200 Subject: [PATCH] feat: kboot native firmware upgrade --- packages/crc16-xmodem/lib/index.d.ts | 1 + .../crc16.ts => crc16-xmodem/lib/index.js} | 8 +- packages/crc16-xmodem/package-lock.json | 13 + packages/crc16-xmodem/package.json | 21 ++ packages/crc16-xmodem/test/index.test.js | 26 ++ packages/mcumgr/package-lock.json | 31 +++ packages/mcumgr/package.json | 4 +- packages/mcumgr/src/serial-peripheral.ts | 2 +- packages/mcumgr/test/util/crc16.test.ts | 15 -- .../uhk-agent/src/services/device.service.ts | 239 ++++++++++++------ packages/uhk-usb/package-lock.json | 10 + packages/uhk-usb/package.json | 1 + packages/uhk-usb/src/constants.ts | 7 +- packages/uhk-usb/src/models/device-state.ts | 1 + packages/uhk-usb/src/models/index.ts | 1 + .../uhk-usb/src/models/module-flash-state.ts | 15 ++ packages/uhk-usb/src/uhk-hid-device.ts | 5 +- packages/uhk-usb/src/uhk-operations.ts | 133 ++++++++-- packages/usb/get-device-state.ts | 1 + packages/usb/kboot-native-firmware-upgrade.ts | 40 +++ 20 files changed, 456 insertions(+), 118 deletions(-) create mode 100644 packages/crc16-xmodem/lib/index.d.ts rename packages/{mcumgr/src/util/crc16.ts => crc16-xmodem/lib/index.js} (93%) create mode 100644 packages/crc16-xmodem/package-lock.json create mode 100644 packages/crc16-xmodem/package.json create mode 100644 packages/crc16-xmodem/test/index.test.js delete mode 100644 packages/mcumgr/test/util/crc16.test.ts create mode 100644 packages/uhk-usb/src/models/module-flash-state.ts create mode 100755 packages/usb/kboot-native-firmware-upgrade.ts diff --git a/packages/crc16-xmodem/lib/index.d.ts b/packages/crc16-xmodem/lib/index.d.ts new file mode 100644 index 00000000000..342fef551b0 --- /dev/null +++ b/packages/crc16-xmodem/lib/index.d.ts @@ -0,0 +1 @@ +export default function crc16XModem(data: ArrayLike): number; diff --git a/packages/mcumgr/src/util/crc16.ts b/packages/crc16-xmodem/lib/index.js similarity index 93% rename from packages/mcumgr/src/util/crc16.ts rename to packages/crc16-xmodem/lib/index.js index 4f5312dc5cc..12654822636 100644 --- a/packages/mcumgr/src/util/crc16.ts +++ b/packages/crc16-xmodem/lib/index.js @@ -33,7 +33,13 @@ const CRC16TABLE = new Uint16Array([ 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 ]); -export default function crc16(data: ArrayLike): number { +/** + * CRC-16/XMODEM (poly 0x1021, init 0x0000, no reflection, xorout 0x0000) + * + * @param {ArrayLike<*>} data + * @returns {number} + */ +export default function crc16XModem(data) { let crc = 0; const l = data.length; for (let i = 0; i < l; i++) { diff --git a/packages/crc16-xmodem/package-lock.json b/packages/crc16-xmodem/package-lock.json new file mode 100644 index 00000000000..bad979593bd --- /dev/null +++ b/packages/crc16-xmodem/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "crc16-xmodem", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "crc16-xmodem", + "version": "1.0.0", + "license": "GPL-3.0" + } + } +} diff --git a/packages/crc16-xmodem/package.json b/packages/crc16-xmodem/package.json new file mode 100644 index 00000000000..1f67f20ac94 --- /dev/null +++ b/packages/crc16-xmodem/package.json @@ -0,0 +1,21 @@ +{ + "name": "crc16-xmodem", + "private": true, + "type": "module", + "version": "1.0.0", + "description": "Some filesystem helper function", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "author": "Ultimate Gadget Laboratories", + "repository": { + "type": "git", + "url": "git@github.com:UltimateHackingKeyboard/agent.git" + }, + "scripts": { + "build:user-config": "tsx ./scripts/generate-user-configs.ts", + "clean": "rimraf ./node_modules", + "test": "node --test", + "lint": "eslint" + }, + "license": "GPL-3.0" +} diff --git a/packages/crc16-xmodem/test/index.test.js b/packages/crc16-xmodem/test/index.test.js new file mode 100644 index 00000000000..c90ffa0a1cf --- /dev/null +++ b/packages/crc16-xmodem/test/index.test.js @@ -0,0 +1,26 @@ +import {it, test} from 'node:test'; + +import crc16XModem from "../lib/index.js"; + +test('standard check vector "123456789" → 0x31C3', ({ assert }) => { + assert.strictEqual(crc16XModem(Buffer.from('123456789')), 0x31C3); +}); + +test('empty buffer returns init value', ({ assert }) => { + assert.strictEqual(crc16XModem([]), 0x0000); // XModem init is 0 +}); + +test('single byte', ({ assert }) => { + assert.strictEqual(crc16XModem([0x00]), 0x0000); + assert.strictEqual(crc16XModem([0xFF]), 0x1EF0); +}); + +it('uhk command', ({ assert }) => { + const crc = crc16XModem([ + 0x02, 0x00, 0x00, 0x09, 0x00, 0x00, 0x09, 0x00, + 0xA1, 0x61, 0x64, 0x65, 0x68, 0x65, 0x6C, 0x6C, + 0x6F, + ]); + + assert.strictEqual(crc, 24488); +}); diff --git a/packages/mcumgr/package-lock.json b/packages/mcumgr/package-lock.json index 1e9ddbd7301..62b1fb6889d 100644 --- a/packages/mcumgr/package-lock.json +++ b/packages/mcumgr/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "license": "See in LICENSE", "dependencies": { + "crc16-xmodem": "file:../crc16-xmodem", "debug": "^4.4.1", "tslib": "^2.8.1" }, @@ -16,11 +17,16 @@ "serialport": ">= 13.0.0" } }, + "../crc16-xmodem": { + "version": "1.0.0", + "license": "GPL-3.0" + }, "node_modules/@serialport/binding-mock": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz", "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==", "license": "MIT", + "peer": true, "dependencies": { "@serialport/bindings-interface": "^1.2.1", "debug": "^4.3.3" @@ -35,6 +41,7 @@ "integrity": "sha512-r25o4Bk/vaO1LyUfY/ulR6hCg/aWiN6Wo2ljVlb4Pj5bqWGcSRC4Vse4a9AcapuAu/FeBzHCbKMvRQeCuKjzIQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@serialport/bindings-interface": "1.2.2", "@serialport/parser-readline": "12.0.0", @@ -54,6 +61,7 @@ "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz", "integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==", "license": "MIT", + "peer": true, "engines": { "node": ">=12.0.0" }, @@ -66,6 +74,7 @@ "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz", "integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==", "license": "MIT", + "peer": true, "dependencies": { "@serialport/parser-delimiter": "12.0.0" }, @@ -81,6 +90,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -98,6 +108,7 @@ "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==", "license": "MIT", + "peer": true, "engines": { "node": "^12.22 || ^14.13 || >=16" } @@ -107,6 +118,7 @@ "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-13.0.0.tgz", "integrity": "sha512-32yvqeTAqJzAEtX5zCrN1Mej56GJ5h/cVFsCDPbF9S1ZSC9FWjOqNAgtByseHfFTSTs/4ZBQZZcZBpolt8sUng==", "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -119,6 +131,7 @@ "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-13.0.0.tgz", "integrity": "sha512-RErAe57g9gvnlieVYGIn1xymb1bzNXb2QtUQd14FpmbQQYlcrmuRnJwKa1BgTCujoCkhtaTtgHlbBWOxm8U2uA==", "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -131,6 +144,7 @@ "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-13.0.0.tgz", "integrity": "sha512-Qqyb0FX1avs3XabQqNaZSivyVbl/yl0jywImp7ePvfZKLwx7jBZjvL+Hawt9wIG6tfq6zbFM24vzCCK7REMUig==", "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -143,6 +157,7 @@ "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-13.0.0.tgz", "integrity": "sha512-a0w0WecTW7bD2YHWrpTz1uyiWA2fDNym0kjmPeNSwZ2XCP+JbirZt31l43m2ey6qXItTYVuQBthm75sPVeHnGA==", "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -155,6 +170,7 @@ "resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-13.0.0.tgz", "integrity": "sha512-60ZDDIqYRi0Xs2SPZUo4Jr5LLIjtb+rvzPKMJCohrO6tAqSDponcNpcB1O4W21mKTxYjqInSz+eMrtk0LLfZIg==", "license": "MIT", + "peer": true, "engines": { "node": ">=8.6.0" } @@ -164,6 +180,7 @@ "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-13.0.0.tgz", "integrity": "sha512-dov3zYoyf0dt1Sudd1q42VVYQ4WlliF0MYvAMA3MOyiU1IeG4hl0J6buBA2w4gl3DOCC05tGgLDN/3yIL81gsA==", "license": "MIT", + "peer": true, "dependencies": { "@serialport/parser-delimiter": "13.0.0" }, @@ -179,6 +196,7 @@ "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-13.0.0.tgz", "integrity": "sha512-JNUQA+y2Rfs4bU+cGYNqOPnNMAcayhhW+XJZihSLQXOHcZsFnOa2F9YtMg9VXRWIcnHldHYtisp62Etjlw24bw==", "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -191,6 +209,7 @@ "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-13.0.0.tgz", "integrity": "sha512-m7HpIf56G5XcuDdA3DB34Z0pJiwxNRakThEHjSa4mG05OnWYv0IG8l2oUyYfuGMowQWaVnQ+8r+brlPxGVH+eA==", "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -203,6 +222,7 @@ "resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-13.0.0.tgz", "integrity": "sha512-fUHZEExm6izJ7rg0A1yjXwu4sOzeBkPAjDZPfb+XQoqgtKAk+s+HfICiYn7N2QU9gyaeCO8VKgWwi+b/DowYOg==", "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -215,6 +235,7 @@ "resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-13.0.0.tgz", "integrity": "sha512-DoXJ3mFYmyD8X/8931agJvrBPxqTaYDsPoly9/cwQSeh/q4EjQND9ySXBxpWz5WcpyCU4jOuusqCSAPsbB30Eg==", "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" }, @@ -227,6 +248,7 @@ "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-13.0.0.tgz", "integrity": "sha512-F7xLJKsjGo2WuEWMSEO1SimRcOA+WtWICsY13r0ahx8s2SecPQH06338g28OT7cW7uRXI7oEQAk62qh5gHJW3g==", "license": "MIT", + "peer": true, "dependencies": { "@serialport/bindings-interface": "1.2.2", "debug": "4.4.0" @@ -243,6 +265,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -255,6 +278,10 @@ } } }, + "node_modules/crc16-xmodem": { + "resolved": "../crc16-xmodem", + "link": true + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -283,6 +310,7 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz", "integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==", "license": "MIT", + "peer": true, "engines": { "node": "^18 || ^20 || >= 21" } @@ -292,6 +320,7 @@ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "license": "MIT", + "peer": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -303,6 +332,7 @@ "resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz", "integrity": "sha512-PHpnTd8isMGPfFTZNCzOZp9m4mAJSNWle9Jxu6BPTcWq7YXl5qN7tp8Sgn0h+WIGcD6JFz5QDgixC2s4VW7vzg==", "license": "MIT", + "peer": true, "dependencies": { "@serialport/binding-mock": "10.2.2", "@serialport/bindings-cpp": "13.0.0", @@ -331,6 +361,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/mcumgr/package.json b/packages/mcumgr/package.json index 7666622fae6..6860b82ce97 100644 --- a/packages/mcumgr/package.json +++ b/packages/mcumgr/package.json @@ -7,8 +7,7 @@ "scripts": { "build": "tsc --project ./src/tsconfig.json", "clean": "rimraf ./node_modules ./dist", - "lint": "eslint", - "test": "node --loader=ts-node/esm --test \"**/*.test.ts\"" + "lint": "eslint" }, "keywords": [ "mcu", @@ -19,6 +18,7 @@ "license": "See in LICENSE", "description": "Micro controller manager utility", "dependencies": { + "crc16-xmodem": "file:../crc16-xmodem", "debug": "^4.4.1", "tslib": "^2.8.1" }, diff --git a/packages/mcumgr/src/serial-peripheral.ts b/packages/mcumgr/src/serial-peripheral.ts index d6685942f59..9cd65cb9da0 100644 --- a/packages/mcumgr/src/serial-peripheral.ts +++ b/packages/mcumgr/src/serial-peripheral.ts @@ -1,9 +1,9 @@ import debug from 'debug'; import {setTimeout} from 'node:timers/promises'; +import crc16 from 'crc16-xmodem'; import {SerialPort} from 'serialport'; import { Peripheral } from './peripheral.js'; -import crc16 from './util/crc16.js'; import toUint16 from './util/to-uint16.js'; import convertToHex from './util/convert-to-hex.js'; import fromUint16 from "./util/from-uint16.js"; diff --git a/packages/mcumgr/test/util/crc16.test.ts b/packages/mcumgr/test/util/crc16.test.ts deleted file mode 100644 index baad51c1df4..00000000000 --- a/packages/mcumgr/test/util/crc16.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, it } from 'node:test'; - -import crc16 from '../../src/util/crc16.js'; - -describe('crc16', () => { - it('should work', ({ assert }) => { - const crc = crc16([ - 0x02, 0x00, 0x00, 0x09, 0x00, 0x00, 0x09, 0x00, - 0xA1, 0x61, 0x64, 0x65, 0x68, 0x65, 0x6C, 0x6C, - 0x6F, - ]); - - assert.strictEqual(crc, 24488); - }); -}); diff --git a/packages/uhk-agent/src/services/device.service.ts b/packages/uhk-agent/src/services/device.service.ts index ea145994fbc..63ff9555fe6 100644 --- a/packages/uhk-agent/src/services/device.service.ts +++ b/packages/uhk-agent/src/services/device.service.ts @@ -15,6 +15,7 @@ import { DeviceConnectionState, escapeZephyrControlChars, findUhkModuleById, + FirmwareJson, FIRMWARE_UPGRADE_METHODS, FirmwareUpgradeConnectPrompt, FirmwareUpgradeIpcResponse, @@ -27,6 +28,8 @@ import { isDeviceProtocolSupportFirmwareChecksum, isDeviceProtocolSupportGitInfo, isDeviceProtocolSupportStatusError, + isVersionGte, + isVersionLt, isSameFirmware, KeyboardLayout, LEFT_HALF_MODULE, @@ -100,6 +103,18 @@ import { import { QueueManager } from './queue-manager'; import { ZephyrLogService } from './zephyr-log.service'; +const FIRMWARE_VERSION_SUPPORT_KBOOT_NATIVE = '17.2.0'; + +interface UpdateModuleFirmwareOptions { + eventSender: Electron.WebContents; + forceUpgrade: boolean; + hardwareModules: HardwareModules; + moduleSlotId: ModuleSlotToId; + packageJson: FirmwareJson; + uhkDeviceProduct: UhkDeviceProduct; + uhkDeviceFirmwareVersion: string; +} + /** * IpcMain pair of the UHK Communication * Functionality: @@ -641,7 +656,8 @@ export class DeviceService { this._checkStatusBuffer = true; response.userConfigSaved = true; } - } else { + } + else { event.sender.send(IpcEvents.device.moduleFirmwareUpgradeSkip, { moduleName: RIGHT_HALF_FIRMWARE_UPGRADE_MODULE_NAME, newFirmwareChecksum: deviceConfig?.md5, @@ -698,16 +714,40 @@ export class DeviceService { ); } else { - await this.operations - .updateModuleWithKboot({ - firmwarePath: getModuleFirmwarePath(leftModuleInfo.module, packageJson), - device: uhkDeviceProduct, - module: leftModuleInfo.module, - onProgress: this.createFirmwareProgressReporter(event.sender, leftModuleInfo.module.name), - prompt: (message) => this.firmwareUpgradePrompt(event.sender, message), + if (isVersionLt(hardwareModules.rightModuleInfo.firmwareVersion, FIRMWARE_VERSION_SUPPORT_KBOOT_NATIVE)) { + await this.operations + .updateModuleWithKboot({ + firmwarePath: getModuleFirmwarePath(leftModuleInfo.module, packageJson), + device: uhkDeviceProduct, + module: leftModuleInfo.module, + onProgress: this.createFirmwareProgressReporter(event.sender, leftModuleInfo.module.name), + prompt: (message) => this.firmwareUpgradePrompt(event.sender, message), + }); + } + else { + await this.updateModuleFirmware({ + eventSender: event.sender, + forceUpgrade: data.forceUpgrade, + hardwareModules, + moduleSlotId: ModuleSlotToId.leftHalf, + packageJson, + uhkDeviceProduct, + uhkDeviceFirmwareVersion: hardwareModules.rightModuleInfo.firmwareVersion, }); + } + + await this.updateModuleFirmware({ + eventSender: event.sender, + forceUpgrade: data.forceUpgrade, + hardwareModules, + moduleSlotId: ModuleSlotToId.leftModule, + packageJson, + uhkDeviceProduct: UHK_80_DEVICE_LEFT, + uhkDeviceFirmwareVersion: packageJson.firmwareVersion, + }); } - } else { + } + else { const moduleConfig = packageJson.modules.find(firmwareDevice => firmwareDevice.moduleId === leftModuleInfo.module.id); event.sender.send(IpcEvents.device.moduleFirmwareUpgradeSkip, { @@ -716,81 +756,28 @@ export class DeviceService { reason: ModuleFirmwareUpgradeSkipReason.ModuleChecksumMatches, } as ModuleFirmwareUpgradeSkipInfo); this.logService.misc('[DeviceService] Skip left firmware upgrade.'); - } - for (const moduleInfo of hardwareModules.moduleInfos) { - if (moduleInfo.module.slotId === ModuleSlotToId.leftHalf) { - // Left half upgrade mandatory, it is running before the other modules upgrade. - continue; - } - // TODO: implement MCUBOOT version - if (uhkDeviceProduct.firmwareUpgradeMethod === FIRMWARE_UPGRADE_METHODS.MCUBOOT) { - event.sender.send(IpcEvents.device.moduleFirmwareUpgradeSkip, { - moduleName: moduleInfo.module.name, - newFirmwareChecksum: '', - reason: ModuleFirmwareUpgradeSkipReason.Uhk80Limitation, - } as ModuleFirmwareUpgradeSkipInfo); - - continue; - } - - if (moduleInfo.module.firmwareUpgradeSupported) { - this.logService.misc(`[DeviceService] "${moduleInfo.module.name}" firmware version:`, moduleInfo.info.firmwareVersion); - this.logService.misc(`[DeviceService] "${moduleInfo.module.name}" current remote firmware checksum:`, moduleInfo.info.remoteFirmwareChecksum); - - const moduleFirmwareInfo = hardwareModules.rightModuleInfo.modules[moduleInfo.module.id]; - if (moduleFirmwareInfo) { - this.logService.misc(`[DeviceService] "${moduleInfo.module.name}" new built firmware checksum:`, moduleFirmwareInfo.builtFirmwareChecksum); - } - - const isModuleFirmwareSame = isSameFirmware( - { - firmwareChecksum: moduleInfo.info.remoteFirmwareChecksum, - firmwareVersion: moduleInfo.info.firmwareVersion - }, - { - firmwareChecksum: moduleFirmwareInfo?.builtFirmwareChecksum, - firmwareVersion: packageJson.firmwareVersion - } - ); - - if (data.forceUpgrade || !isModuleFirmwareSame) { - const moduleConfig = packageJson.modules.find(firmwareDevice => firmwareDevice.moduleId === moduleInfo.module.id); - - event.sender.send(IpcEvents.device.moduleFirmwareUpgrading, { - forceUpgraded: isModuleFirmwareSame, - moduleName: moduleInfo.module.name, - newFirmwareChecksum: moduleConfig.md5, - } as CurrentlyUpdatingModuleInfo); - await this.operations - .updateModuleWithKboot({ - firmwarePath: getModuleFirmwarePath(moduleInfo.module, packageJson), - device: uhkDeviceProduct, - module: moduleInfo.module, - onProgress: this.createFirmwareProgressReporter(event.sender, moduleInfo.module.name), - prompt: (message) => this.firmwareUpgradePrompt(event.sender, message), - }); - this.logService.misc(`[DeviceService] "${moduleInfo.module.name}" firmware update done.`); - } else { - const moduleConfig = packageJson.modules.find(firmwareDevice => firmwareDevice.moduleId === moduleInfo.module.id); - - event.sender.send(IpcEvents.device.moduleFirmwareUpgradeSkip, { - moduleName: moduleInfo.module.name, - newFirmwareChecksum: moduleConfig?.md5, - reason: ModuleFirmwareUpgradeSkipReason.ModuleChecksumMatches, - } as ModuleFirmwareUpgradeSkipInfo); - this.logService.misc(`[DeviceService] Skip "${moduleInfo.module.name}" firmware upgrade.`); - } - } else { - event.sender.send(IpcEvents.device.moduleFirmwareUpgradeSkip, { - moduleName: moduleInfo.module.name, - newFirmwareChecksum: '', - reason: ModuleFirmwareUpgradeSkipReason.NotSupported, - } as ModuleFirmwareUpgradeSkipInfo); - this.logService.misc(`[DeviceService] Skip "${moduleInfo.module.name}" firmware upgrade. Currently not supported`); - } + await this.updateModuleFirmware({ + eventSender: event.sender, + forceUpgrade: data.forceUpgrade, + hardwareModules, + moduleSlotId: ModuleSlotToId.leftModule, + packageJson, + uhkDeviceProduct: UHK_80_DEVICE_LEFT, + uhkDeviceFirmwareVersion: leftModuleInfo.info.firmwareVersion, + }); } + await this.updateModuleFirmware({ + eventSender: event.sender, + forceUpgrade: data.forceUpgrade, + hardwareModules, + moduleSlotId: ModuleSlotToId.rightModule, + packageJson, + uhkDeviceProduct, + uhkDeviceFirmwareVersion: hardwareModules.rightModuleInfo.firmwareVersion, + }); + await copySmartMacroDocToWebserver(firmwarePathData, this.logService); await makeFolderWriteableToUserOnLinux(getSmartMacroDocRootPath()); response.success = true; @@ -1552,6 +1539,96 @@ export class DeviceService { } } + private async updateModuleFirmware({ + eventSender, + forceUpgrade, + hardwareModules, + moduleSlotId, + packageJson, + uhkDeviceProduct, + uhkDeviceFirmwareVersion, + }: UpdateModuleFirmwareOptions): Promise { + const moduleInfo = hardwareModules.moduleInfos.find(info => info.module.slotId === moduleSlotId); + + if (!moduleInfo) { + return; + } + + let updateMethod = 'updateModuleWithKboot' + + if (uhkDeviceProduct.firmwareUpgradeMethod === FIRMWARE_UPGRADE_METHODS.MCUBOOT) { + if (isVersionLt(uhkDeviceFirmwareVersion, FIRMWARE_VERSION_SUPPORT_KBOOT_NATIVE)) { + eventSender.send(IpcEvents.device.moduleFirmwareUpgradeSkip, { + moduleName: moduleInfo.module.name, + newFirmwareChecksum: '', + reason: ModuleFirmwareUpgradeSkipReason.Uhk80Limitation, + } as ModuleFirmwareUpgradeSkipInfo); + + return; + } + + updateMethod = 'updateModuleWithKbootNative' + } + else if (isVersionGte(uhkDeviceFirmwareVersion, FIRMWARE_VERSION_SUPPORT_KBOOT_NATIVE)) { + updateMethod = 'updateModuleWithKbootNative' + } + + if (moduleInfo.module.firmwareUpgradeSupported) { + this.logService.misc(`[DeviceService] "${moduleInfo.module.name}" firmware version:`, moduleInfo.info.firmwareVersion); + this.logService.misc(`[DeviceService] "${moduleInfo.module.name}" current remote firmware checksum:`, moduleInfo.info.remoteFirmwareChecksum); + + const moduleFirmwareInfo = hardwareModules.rightModuleInfo.modules[moduleInfo.module.id]; + if (moduleFirmwareInfo) { + this.logService.misc(`[DeviceService] "${moduleInfo.module.name}" new built firmware checksum:`, moduleFirmwareInfo.builtFirmwareChecksum); + } + + const isModuleFirmwareSame = isSameFirmware( + { + firmwareChecksum: moduleInfo.info.remoteFirmwareChecksum, + firmwareVersion: moduleInfo.info.firmwareVersion + }, + { + firmwareChecksum: moduleFirmwareInfo?.builtFirmwareChecksum, + firmwareVersion: packageJson.firmwareVersion + } + ); + + if (forceUpgrade || !isModuleFirmwareSame) { + const moduleConfig = packageJson.modules.find(firmwareDevice => firmwareDevice.moduleId === moduleInfo.module.id); + + eventSender.send(IpcEvents.device.moduleFirmwareUpgrading, { + forceUpgraded: isModuleFirmwareSame, + moduleName: moduleInfo.module.name, + newFirmwareChecksum: moduleConfig.md5, + } as CurrentlyUpdatingModuleInfo); + + await this.operations[updateMethod]( + getModuleFirmwarePath(moduleInfo.module, packageJson), + uhkDeviceProduct, + moduleInfo.module + ); + + this.logService.misc(`[DeviceService] "${moduleInfo.module.name}" firmware update done.`); + } else { + const moduleConfig = packageJson.modules.find(firmwareDevice => firmwareDevice.moduleId === moduleInfo.module.id); + + eventSender.send(IpcEvents.device.moduleFirmwareUpgradeSkip, { + moduleName: moduleInfo.module.name, + newFirmwareChecksum: moduleConfig?.md5, + reason: ModuleFirmwareUpgradeSkipReason.ModuleChecksumMatches, + } as ModuleFirmwareUpgradeSkipInfo); + this.logService.misc(`[DeviceService] Skip "${moduleInfo.module.name}" firmware upgrade.`); + } + } else { + eventSender.send(IpcEvents.device.moduleFirmwareUpgradeSkip, { + moduleName: moduleInfo.module.name, + newFirmwareChecksum: '', + reason: ModuleFirmwareUpgradeSkipReason.NotSupported, + } as ModuleFirmwareUpgradeSkipInfo); + this.logService.misc(`[DeviceService] Skip "${moduleInfo.module.name}" firmware upgrade. Currently not supported`); + } + } + private async waitForUhkDeviceWithConnectPrompt( eventSender: Electron.WebContents, device: UhkDeviceProduct, diff --git a/packages/uhk-usb/package-lock.json b/packages/uhk-usb/package-lock.json index eb3c6e14708..8ecdd2391ed 100644 --- a/packages/uhk-usb/package-lock.json +++ b/packages/uhk-usb/package-lock.json @@ -10,6 +10,7 @@ "license": "See in LICENSE", "dependencies": { "@uhk/mcumgr": "file:../mcumgr", + "crc16-xmodem": "file:../crc16-xmodem", "is-root": "3.0.0", "kboot": "file:../kboot", "nrf-intel-hex": "1.4.0", @@ -23,6 +24,10 @@ "serialport": ">= 13.0.0" } }, + "../crc16-xmodem": { + "version": "1.0.0", + "license": "GPL-3.0" + }, "../kboot": { "version": "0.0.0", "license": "See in LICENSE", @@ -37,6 +42,7 @@ "version": "0.0.0", "license": "See in LICENSE", "dependencies": { + "crc16-xmodem": "file:../crc16-xmodem", "debug": "^4.4.1", "tslib": "^2.8.1" }, @@ -291,6 +297,10 @@ "resolved": "../mcumgr", "link": true }, + "node_modules/crc16-xmodem": { + "resolved": "../crc16-xmodem", + "link": true + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", diff --git a/packages/uhk-usb/package.json b/packages/uhk-usb/package.json index 5d58841de87..4a6da0367b7 100644 --- a/packages/uhk-usb/package.json +++ b/packages/uhk-usb/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@uhk/mcumgr": "file:../mcumgr", + "crc16-xmodem": "file:../crc16-xmodem", "kboot": "file:../kboot", "is-root": "3.0.0", "nrf-intel-hex": "1.4.0", diff --git a/packages/uhk-usb/src/constants.ts b/packages/uhk-usb/src/constants.ts index 0a42af6a93d..11e86c6d5db 100644 --- a/packages/uhk-usb/src/constants.ts +++ b/packages/uhk-usb/src/constants.ts @@ -36,6 +36,10 @@ export enum UsbCommand { EraseBleSettings = 0x1d, ExecShellCommand = 0x1e, ReadOled = 0x1f, + WriteModuleFirmware = 0x20, + FlashModule = 0x21, + GetModuleFlashState = 0x22, + ValidateBufferCrc = 0x23, } export enum EepromOperation { @@ -46,7 +50,8 @@ export enum EepromOperation { export enum ConfigBufferId { hardwareConfig = 0, stagingUserConfig = 1, - validatedUserConfig = 2 + validatedUserConfig = 2, + moduleFirmware = 3, } export enum DevicePropertyIds { diff --git a/packages/uhk-usb/src/models/device-state.ts b/packages/uhk-usb/src/models/device-state.ts index 952a8d5e387..1d30ee8724b 100644 --- a/packages/uhk-usb/src/models/device-state.ts +++ b/packages/uhk-usb/src/models/device-state.ts @@ -1,6 +1,7 @@ export interface DeviceState { isEepromBusy: boolean; isMacroStatusDirty: boolean; + isModuleFlashBusy: boolean; isZephyrLogAvailable: boolean; areHalvesMerged: boolean; isLeftHalfConnected: boolean; diff --git a/packages/uhk-usb/src/models/index.ts b/packages/uhk-usb/src/models/index.ts index 75cbf676657..d411f69a7d4 100644 --- a/packages/uhk-usb/src/models/index.ts +++ b/packages/uhk-usb/src/models/index.ts @@ -4,6 +4,7 @@ export * from './duration.js'; export * from './i2c-baud-rate.js'; export * from './i2c-error-buffer.js'; export * from './load-configurations-result.js'; +export * from './module-flash-state.js'; export * from './pairing-info.js'; export * from './reenumerate-option.js'; export * from './reenumerate-result.js'; diff --git a/packages/uhk-usb/src/models/module-flash-state.ts b/packages/uhk-usb/src/models/module-flash-state.ts new file mode 100644 index 00000000000..f021d1b47e9 --- /dev/null +++ b/packages/uhk-usb/src/models/module-flash-state.ts @@ -0,0 +1,15 @@ +export const MODULE_FLASH_STATE = Object.freeze({ + Idle: 0, + Erasing: 1, + Writing: 2, + Done: 3, + Error: 4, +}) + +export type MODULE_FLASH_STATE_KEY_TYPE = keyof typeof MODULE_FLASH_STATE; +export type MODULE_FLASH_STATE_TYPE = typeof MODULE_FLASH_STATE[MODULE_FLASH_STATE_KEY_TYPE]; + +export interface ModuleFlashResponse { + state: MODULE_FLASH_STATE_TYPE; + errorCode?: number; +} diff --git a/packages/uhk-usb/src/uhk-hid-device.ts b/packages/uhk-usb/src/uhk-hid-device.ts index 5ee44b6e104..d533fd62445 100644 --- a/packages/uhk-usb/src/uhk-hid-device.ts +++ b/packages/uhk-usb/src/uhk-hid-device.ts @@ -480,7 +480,7 @@ export class UhkHidDevice { ): Promise { await this.close(); const reenumMode = EnumerationModes[enumerationMode].toString(); - this.logService.misc(`[UhkHidDevice] Start reenumeration, mode: ${reenumMode}, timeout: ${timeout}ms`); + this.logService.misc(`[UhkHidDevice] Start reenumeration, device: ${device.logName}, mode: ${reenumMode}, timeout: ${timeout}ms`); const vidPidPairs = getDeviceEnumerateVidPidPairs(device, enumerationMode); const startTime = new Date(); @@ -620,8 +620,9 @@ export class UhkHidDevice { const activeLayerNumber = buffer[6] & 0x7f; return { - isEepromBusy: buffer[1] !== 0, + isEepromBusy: isBitSet(buffer[1], 0), isMacroStatusDirty: buffer[7] !== 0, + isModuleFlashBusy: isBitSet(buffer[1], 1), areHalvesMerged: isBitSet(buffer[2], 0), isLeftHalfConnected: buffer[3] !== 0, activeKeymapIndex: buffer[8], diff --git a/packages/uhk-usb/src/uhk-operations.ts b/packages/uhk-usb/src/uhk-operations.ts index 76315f2f894..f37274313c0 100644 --- a/packages/uhk-usb/src/uhk-operations.ts +++ b/packages/uhk-usb/src/uhk-operations.ts @@ -1,5 +1,7 @@ import { McuManager, SerialPeripheral } from '@uhk/mcumgr'; +import crc16 from 'crc16-xmodem'; import * as fs from 'fs'; +import { readFile } from 'node:fs/promises'; import { DataOption, KBoot, Properties, UsbPeripheral } from 'kboot'; import { ALL_UHK_DEVICES, @@ -47,6 +49,11 @@ import { UsbCommand, UsbVariables } from './constants.js'; +import { + MODULE_FLASH_STATE, + MODULE_FLASH_STATE_TYPE, + ModuleFlashResponse, +} from './models/module-flash-state.js'; import { DebugInfo, Duration, @@ -213,7 +220,7 @@ export class UhkOperations { const peripheral = new SerialPeripheral(reenumerateResult.serialPath); const mcuManager = new McuManager(peripheral); this.logService.misc(`[UhkOperations] Read ${device.logName} firmware from file`); - const configData = fs.readFileSync(firmwarePath); + const configData = await readFile(firmwarePath); this.logService.misc('[UhkOperations] Write memory with mcumgr'); await mcuManager.imageUpload(configData, onProgress); this.logService.misc('[UhkOperations] Reset mcu bootloader'); @@ -296,7 +303,7 @@ export class UhkOperations { await kboot.flashEraseAllUnsecure(); this.logService.misc(`[UhkOperations] Read "${module.name}" firmware from file`); - const configData = fs.readFileSync(firmwarePath); + const configData = await readFile(firmwarePath); this.logService.misc('[UhkOperations] Write memory'); await kboot.configureI2c(module.i2cAddress); @@ -325,6 +332,52 @@ export class UhkOperations { this.logService.misc(`[UhkOperations] "${module.name}" firmware successfully flashed`); } + public async updateModuleWithKbootNative( + firmwarePath: string, + device: UhkDeviceProduct, + module: UhkModule + ): Promise { + this.logService.misc(`[UhkOperations][kboot-native] Start flashing "${module.name}" module firmware`); + await this.device.reenumerate({ + device, + enumerationMode: EnumerationModes.NormalKeyboard, + }); + await this.device.close(); + await snooze(1000); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const configData: Buffer = await readFile(firmwarePath) as any; + this.logService.misc('[UhkOperations][kboot-native] sending firmware to the keyboard'); + await this.sendConfigToKeyboard(configData, UsbCommand.WriteModuleFirmware); + + this.logService.misc('[UhkOperations][kboot-native] validating module firmware'); + const crc = crc16(configData); + this.logService.misc(`[UhkOperations][kboot-native] module firmware crc: ${crc}`); + const isValid = await this.isBufferValid(ConfigBufferId.moduleFirmware, configData.length, crc); + + if (!isValid) { + this.logService.error('[UhkOperations][kboot-native] module firmware crc validation failed'); + throw new Error('Module firmware crc validation failed'); + } + + this.logService.misc('[UhkOperations][kboot-native] flashing module'); + await this.flashModule(module); + + this.logService.misc('[UhkOperations][kboot-native] wait until flashing finished'); + const response = await this.waitUntilModuleFlashFinished(); + + if (response.state === MODULE_FLASH_STATE.Error) { + this.logService.error(`[UhkOperations][kboot-native] error occurred in the module flashing process. Error code: ${response.errorCode}`); + + throw new Error(`Error occurred in the module flashing process. Error code: ${response.errorCode}`); + } + + this.logService.misc('[UhkOperations][kboot-native] wait until flashing finished'); + await this.waitUntilModuleFlashFinished(); + + this.logService.misc(`[UhkOperations][kboot-native] "${module.name}" firmware successfully flashed`); + } + /** * Return with the actual UserConfiguration from UHK Device * @returns {Promise} @@ -485,9 +538,15 @@ export class UhkOperations { const transferPercentRange = 0.83; reportProgress(preTransferPercent); - await this.sendConfigToKeyboard(configBuffer, true, (percent) => { - reportProgress(preTransferPercent + Math.round(percent * transferPercentRange)); - }); + + await this.sendConfigToKeyboard( + resultBuffer.getBufferContent(), + UsbCommand.WriteStagingUserConfig, + (percent) => { + reportProgress(preTransferPercent + Math.round(percent * transferPercentRange)); + }, + ); + reportProgress(86); await this.applyConfiguration(); reportProgress(90); @@ -523,7 +582,7 @@ export class UhkOperations { hardwareConfig.toBinary(hardwareBuffer); const buffer = hardwareBuffer.getBufferContent(); - await this.sendConfigToKeyboard(buffer, false); + await this.sendConfigToKeyboard(buffer, UsbCommand.WriteHardwareConfig); await this.writeConfigToEeprom(ConfigBufferId.hardwareConfig); await this.waitUntilKeyboardBusy(); } @@ -551,6 +610,25 @@ export class UhkOperations { } } + public async waitUntilModuleFlashFinished(): Promise { + while (true) { + const response = await this.getModuleFlashState(); + if (response.state === MODULE_FLASH_STATE.Erasing) { + this.logService.misc('[DeviceOperation] Module flash erasing, wait...'); + await snooze(200); + continue; + } + + if (response.state === MODULE_FLASH_STATE.Writing) { + this.logService.misc('[DeviceOperation] Module flash writing, wait...'); + await snooze(200); + continue; + } + + return response; + } + } + public async waitForKbootIdle(moduleName: string, prompt?: PromptCallback): Promise { let promptSent = false; while (true) { @@ -574,6 +652,15 @@ export class UhkOperations { } } + public async getModuleFlashState(): Promise { + const buffer = await this.device.write(Buffer.from([UsbCommand.GetModuleFlashState])); + + return { + state: buffer[0] as MODULE_FLASH_STATE_TYPE, + errorCode: buffer[1] + } + } + public async getModuleProperty({ module, property } : GetModulePropertyArguments): Promise { const moduleSlotName = getSlotIdName(module); @@ -727,17 +814,24 @@ export class UhkOperations { public async eraseHardwareConfig(): Promise { this.logService.usbOps('[DeviceOperation] USB[T]: Erase hardware configuration'); const buffer = Buffer.from(Array(64).fill(0xff)); - await this.sendConfigToKeyboard(buffer, false); + await this.sendConfigToKeyboard(buffer, UsbCommand.WriteHardwareConfig); await this.writeConfigToEeprom(ConfigBufferId.hardwareConfig); } public async eraseUserConfig(): Promise { this.logService.usbOps('[DeviceOperation] USB[T]: Erase user configuration'); const buffer = Buffer.from(Array(2 ** 15 - 64).fill(0xff)); - await this.sendConfigToKeyboard(buffer, true); + await this.sendConfigToKeyboard(buffer, UsbCommand.WriteStagingUserConfig); await this.writeConfigToEeprom(ConfigBufferId.stagingUserConfig); } + public async flashModule(module: UhkModule): Promise { + this.logService.usbOps('[DeviceOperation] USB[T]: Flash module'); + const buffer = Buffer.from([UsbCommand.FlashModule, module.slotId]); + + await this.device.write(buffer); + } + public async switchKeymap(keymapAbbreviation: string): Promise { this.logService.usbOps('[DeviceOperation] USB[T]: Switch keymap'); const keymapAbbreviationAscii = keymapAbbreviation.split('').map(char => char.charCodeAt(0)); @@ -988,19 +1082,15 @@ export class UhkOperations { /** * IpcMain handler. Send the UserConfiguration to the UHK Device and send a response with the result. * @param {Buffer} buffer - UserConfiguration buffer - * @param {Boolean} isUserConfiguration - User or Hardware configuration + * @param {UsbCommand} command - Represent which configuration area write to the device * @returns {Promise} * @private */ private async sendConfigToKeyboard( buffer: Buffer, - isUserConfiguration, - onProgress?: ProgressCallback + command: UsbCommand, + onProgress?: ProgressCallback, ): Promise { - const command = isUserConfiguration - ? UsbCommand.WriteStagingUserConfig - : UsbCommand.WriteHardwareConfig; - const fragments = getTransferBuffers(command, buffer); for (let i = 0; i < fragments.length; i++) { await this.device.write(fragments[i]); @@ -1035,4 +1125,17 @@ export class UhkOperations { await this.device.write(buffer); } + + public async isBufferValid(bufferId: ConfigBufferId, expectedSize: number, expectedCrc: number): Promise { + this.logService.usbOps('[DeviceOperation] USB[T]: Validate Buffer CRC'); + const buffer = Buffer.alloc(6); + buffer.writeUInt8(UsbCommand.ValidateBufferCrc, 0); + buffer.writeUInt8(bufferId, 1); + buffer.writeUInt16LE(expectedSize, 2); + buffer.writeUInt16LE(expectedCrc, 4); + + const response = await this.device.write(buffer); + + return response[1] === 0; + } } diff --git a/packages/usb/get-device-state.ts b/packages/usb/get-device-state.ts index d21f220fde5..5dd06bf42f8 100755 --- a/packages/usb/get-device-state.ts +++ b/packages/usb/get-device-state.ts @@ -14,6 +14,7 @@ setInterval(async function () { console.log( `isEepromBusy: ${state.isEepromBusy ? 'yes' : 'no'} | \ isMacroStatusDirty: ${state.isMacroStatusDirty ? 'yes' : 'no'} | \ +isModuleFlashBusy: ${state.isModuleFlashBusy ? 'yes' : 'no'} | \ isZephyrLogAvailable: ${state.isZephyrLogAvailable ? 'yes' : 'no'} | \ areHalvesMerged: ${state.areHalvesMerged ? 'yes' : 'no'} | \ newPairedDevice: ${state.newPairedDevice ? 'yes' : 'no'} | \ diff --git a/packages/usb/kboot-native-firmware-upgrade.ts b/packages/usb/kboot-native-firmware-upgrade.ts new file mode 100755 index 00000000000..020d3795df9 --- /dev/null +++ b/packages/usb/kboot-native-firmware-upgrade.ts @@ -0,0 +1,40 @@ +#!/usr/bin/env -S node --loader ts-node/esm --no-warnings=ExperimentalWarning + +import fs from 'fs'; +import * as path from 'path'; +import { LEFT_HALF_MODULE } from 'uhk-common'; +import { getCurrentUhkDeviceProduct, getDeviceFirmwarePath, getFirmwarePackageJson } from 'uhk-usb'; + +import Uhk, { errorHandler, yargs } from './src/index.js'; + +(async function () { + try { + const argv = yargs + .usage('Usage: $0 ') + .argv; + + const firmwarePath = argv._[0]; + + if (!fs.existsSync(firmwarePath)) { + console.log('Firmware directory does not exists.'); + process.exit(1); + } + + const uhkDeviceProduct = await getCurrentUhkDeviceProduct(argv); + + const { operations } = Uhk(argv); + + const packageJsonPath = path.join(firmwarePath, 'package.json'); + const packageJson = await getFirmwarePackageJson({ + packageJsonPath, + tmpDirectory: firmwarePath + }); + const rightFirmwarePath = getDeviceFirmwarePath(uhkDeviceProduct, packageJson); + + await operations.updateRightFirmwareWithKboot(rightFirmwarePath, uhkDeviceProduct); + const leftFirmwarePath = path.join(firmwarePath, 'modules/uhk60-left.bin'); + await operations.updateModuleWithKbootNative(leftFirmwarePath, uhkDeviceProduct, LEFT_HALF_MODULE); + } catch (error) { + await errorHandler(error); + } +})();