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 9d30d35df7f..a028d507e6d 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 a0930c95649..d21b2a4b236 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, FirmwareUpgradeIpcResponse, getHardwareConfigFromDeviceResponse, @@ -26,6 +27,8 @@ import { isDeviceProtocolSupportFirmwareChecksum, isDeviceProtocolSupportGitInfo, isDeviceProtocolSupportStatusError, + isVersionGte, + isVersionLt, isSameFirmware, KeyboardLayout, LEFT_HALF_MODULE, @@ -97,6 +100,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: @@ -681,6 +696,17 @@ export class DeviceService { await snooze(1000); const firmwarePath = getDeviceFirmwarePath(UHK_80_DEVICE_LEFT, packageJson); await this.operations.updateFirmwareWithMcuManager(firmwarePath, UHK_80_DEVICE_LEFT); + await waitForUhkDeviceConnected(UHK_80_DEVICE_LEFT); + + await this.updateModuleFirmware({ + eventSender: event.sender, + forceUpgrade: data.forceUpgrade, + hardwareModules, + moduleSlotId: ModuleSlotToId.leftModule, + packageJson, + uhkDeviceProduct: UHK_80_DEVICE_LEFT, + uhkDeviceFirmwareVersion: packageJson.firmwareVersion, + }); if (!(await isUhkDeviceConnected(uhkDeviceProduct))) { this.logService.misc('[DeviceService] To finish the firmware upgrade, now connect the right half via USB. (You can disconnect the left half or use a second USB cable.)'); @@ -689,12 +715,35 @@ export class DeviceService { await waitForUhkDeviceConnected(uhkDeviceProduct); } else { - await this.operations - .updateModuleWithKboot( - getModuleFirmwarePath(leftModuleInfo.module, packageJson), + if (isVersionLt(hardwareModules.rightModuleInfo.firmwareVersion, FIRMWARE_VERSION_SUPPORT_KBOOT_NATIVE)) { + await this.operations + .updateModuleWithKboot( + getModuleFirmwarePath(leftModuleInfo.module, packageJson), + uhkDeviceProduct, + leftModuleInfo.module + ); + } + else { + await this.updateModuleFirmware({ + eventSender: event.sender, + forceUpgrade: data.forceUpgrade, + hardwareModules, + moduleSlotId: ModuleSlotToId.leftHalf, + packageJson, uhkDeviceProduct, - leftModuleInfo.module - ); + 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 { const moduleConfig = packageJson.modules.find(firmwareDevice => firmwareDevice.moduleId === leftModuleInfo.module.id); @@ -705,79 +754,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( - 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); - - 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; @@ -1516,4 +1514,94 @@ export class DeviceService { this.win.webContents.send(IpcEvents.device.zephyrLog, logEntry) } } + + 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`); + } + } } diff --git a/packages/uhk-usb/package-lock.json b/packages/uhk-usb/package-lock.json index 26aa7eb9e93..e7e4724be23 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" }, @@ -347,6 +353,10 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "peer": 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 33e9c35b473..db4004ca450 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 116b1ab0bf0..cb477d666cf 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 259e42bc837..55727747f97 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 570c0b858b4..e66a906ec6c 100644 --- a/packages/uhk-usb/src/uhk-hid-device.ts +++ b/packages/uhk-usb/src/uhk-hid-device.ts @@ -479,7 +479,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(); @@ -619,8 +619,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, activeLayerNumber, diff --git a/packages/uhk-usb/src/uhk-operations.ts b/packages/uhk-usb/src/uhk-operations.ts index 3ae27fd0a80..9ec2d09dac2 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, @@ -45,6 +47,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, @@ -197,7 +204,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); this.logService.misc('[UhkOperations] Reset mcu bootloader'); @@ -275,7 +282,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); @@ -304,6 +311,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 = 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} @@ -463,9 +516,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); @@ -501,7 +560,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(); } @@ -529,6 +588,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): Promise { while (true) { const buffer = await this.device.write(Buffer.from([UsbCommand.GetProperty, DevicePropertyIds.CurrentKbootCommand])); @@ -544,6 +622,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); @@ -697,17 +784,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)); @@ -958,19 +1052,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, + command: UsbCommand, onProgress?: (percent: number) => void ): 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]); @@ -1005,4 +1095,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 7228a682d45..458ca695197 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); + } +})();