From 31d41068646c65632421df8cb4857a0b168689b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Wed, 8 Jul 2026 18:11:34 +0200 Subject: [PATCH 1/2] feat: kboot native firmware upgrade --- packages/mcumgr/src/util/crc16.ts | 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 | 3 +- packages/uhk-usb/src/uhk-operations.ts | 130 ++++++++++++++++-- packages/uhk-usb/src/utils/crc16.ts | 46 +++++++ packages/usb/get-device-state.ts | 1 + packages/usb/kboot-native-firmware-upgrade.ts | 40 ++++++ 10 files changed, 229 insertions(+), 16 deletions(-) create mode 100644 packages/uhk-usb/src/models/module-flash-state.ts create mode 100644 packages/uhk-usb/src/utils/crc16.ts create mode 100755 packages/usb/kboot-native-firmware-upgrade.ts diff --git a/packages/mcumgr/src/util/crc16.ts b/packages/mcumgr/src/util/crc16.ts index 4f5312dc5cc..ca8d55302d8 100644 --- a/packages/mcumgr/src/util/crc16.ts +++ b/packages/mcumgr/src/util/crc16.ts @@ -1,3 +1,4 @@ +// TODO: eliminate the duplication of the uhk-usb-module const CRC16TABLE = new Uint16Array([ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 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..3cb01d8c64f 100644 --- a/packages/uhk-usb/src/uhk-hid-device.ts +++ b/packages/uhk-usb/src/uhk-hid-device.ts @@ -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..41f26f93809 100644 --- a/packages/uhk-usb/src/uhk-operations.ts +++ b/packages/uhk-usb/src/uhk-operations.ts @@ -1,5 +1,6 @@ import { McuManager, SerialPeripheral } from '@uhk/mcumgr'; import * as fs from 'fs'; +import { readFile } from 'node:fs/promises'; import { DataOption, KBoot, Properties, UsbPeripheral } from 'kboot'; import { ALL_UHK_DEVICES, @@ -45,6 +46,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, @@ -60,6 +66,7 @@ import { readBootloaderFirmwareFromHexFileAsync, waitForDevice } from './util.js'; +import crc16 from './utils/crc16.js'; import { generateDeviceSerialNumber } from './utils/generate-device-serial-number.js'; import { convertMsToDuration, convertSlaveI2cErrorBuffer, snooze, waitUntil} from './utils/index.js'; import { normalizeStatusBuffer } from './utils/normalize-status-buffer.js'; @@ -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,51 @@ 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); + + 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 +515,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 +559,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 +587,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 +621,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 +783,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 +1051,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 +1094,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/uhk-usb/src/utils/crc16.ts b/packages/uhk-usb/src/utils/crc16.ts new file mode 100644 index 00000000000..9f94ecf830e --- /dev/null +++ b/packages/uhk-usb/src/utils/crc16.ts @@ -0,0 +1,46 @@ +// TODO: eliminate the duplication of the uhk-usb-module +const CRC16TABLE = new Uint16Array([ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 +]); + +export default function crc16(data: ArrayLike): number { + let crc = 0; + const l = data.length; + for (let i = 0; i < l; i++) { + crc = ((crc << 8) & 0xff00) ^ CRC16TABLE[((crc >> 8) & 0xff) ^ data[i]]; + } + + return crc; +} + 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); + } +})(); From 2c9ab5d358bf1d9a240b0c8e8ab14156998f63ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Wed, 8 Jul 2026 22:09:38 +0200 Subject: [PATCH 2/2] feat: use kboot native firmare upgrade --- packages/crc16-xmodem/lib/index.d.ts | 1 + .../crc16.ts => crc16-xmodem/lib/index.js} | 9 +- 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 | 238 ++++++++++++------ packages/uhk-usb/package-lock.json | 10 + packages/uhk-usb/package.json | 1 + packages/uhk-usb/src/uhk-hid-device.ts | 2 +- packages/uhk-usb/src/uhk-operations.ts | 3 +- packages/uhk-usb/src/utils/crc16.ts | 46 ---- 15 files changed, 279 insertions(+), 143 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 delete mode 100644 packages/uhk-usb/src/utils/crc16.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 ca8d55302d8..12654822636 100644 --- a/packages/mcumgr/src/util/crc16.ts +++ b/packages/crc16-xmodem/lib/index.js @@ -1,4 +1,3 @@ -// TODO: eliminate the duplication of the uhk-usb-module const CRC16TABLE = new Uint16Array([ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, @@ -34,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/uhk-hid-device.ts b/packages/uhk-usb/src/uhk-hid-device.ts index 3cb01d8c64f..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(); diff --git a/packages/uhk-usb/src/uhk-operations.ts b/packages/uhk-usb/src/uhk-operations.ts index 41f26f93809..9ec2d09dac2 100644 --- a/packages/uhk-usb/src/uhk-operations.ts +++ b/packages/uhk-usb/src/uhk-operations.ts @@ -1,4 +1,5 @@ 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'; @@ -66,7 +67,6 @@ import { readBootloaderFirmwareFromHexFileAsync, waitForDevice } from './util.js'; -import crc16 from './utils/crc16.js'; import { generateDeviceSerialNumber } from './utils/generate-device-serial-number.js'; import { convertMsToDuration, convertSlaveI2cErrorBuffer, snooze, waitUntil} from './utils/index.js'; import { normalizeStatusBuffer } from './utils/normalize-status-buffer.js'; @@ -324,6 +324,7 @@ export class UhkOperations { 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); diff --git a/packages/uhk-usb/src/utils/crc16.ts b/packages/uhk-usb/src/utils/crc16.ts deleted file mode 100644 index 9f94ecf830e..00000000000 --- a/packages/uhk-usb/src/utils/crc16.ts +++ /dev/null @@ -1,46 +0,0 @@ -// TODO: eliminate the duplication of the uhk-usb-module -const CRC16TABLE = new Uint16Array([ - 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, - 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, - 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, - 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, - 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, - 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, - 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, - 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, - 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, - 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, - 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, - 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, - 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, - 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, - 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, - 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, - 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, - 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, - 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, - 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, - 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, - 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, - 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, - 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, - 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, - 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, - 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, - 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, - 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, - 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, - 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, - 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 -]); - -export default function crc16(data: ArrayLike): number { - let crc = 0; - const l = data.length; - for (let i = 0; i < l; i++) { - crc = ((crc << 8) & 0xff00) ^ CRC16TABLE[((crc >> 8) & 0xff) ^ data[i]]; - } - - return crc; -} -