Skip to content

Commit 07a633e

Browse files
committed
feat: kboot native firmware upgrade
1 parent 851ec60 commit 07a633e

10 files changed

Lines changed: 221 additions & 14 deletions

File tree

packages/mcumgr/src/util/crc16.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// TODO: eliminate the duplication of the uhk-usb-module
12
const CRC16TABLE = new Uint16Array([
23
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
34
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,

packages/uhk-usb/src/constants.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ export enum UsbCommand {
3636
EraseBleSettings = 0x1d,
3737
ExecShellCommand = 0x1e,
3838
ReadOled = 0x1f,
39+
WriteModuleFirmware = 0x20,
40+
FlashModule = 0x21,
41+
GetModuleFlashState = 0x22,
42+
ValidateBufferCrc = 0x23,
3943
}
4044

4145
export enum EepromOperation {
@@ -46,7 +50,8 @@ export enum EepromOperation {
4650
export enum ConfigBufferId {
4751
hardwareConfig = 0,
4852
stagingUserConfig = 1,
49-
validatedUserConfig = 2
53+
validatedUserConfig = 2,
54+
moduleFirmware = 3,
5055
}
5156

5257
export enum DevicePropertyIds {

packages/uhk-usb/src/models/device-state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export interface DeviceState {
22
isEepromBusy: boolean;
33
isMacroStatusDirty: boolean;
4+
isModuleFlashBusy: boolean;
45
isZephyrLogAvailable: boolean;
56
areHalvesMerged: boolean;
67
isLeftHalfConnected: boolean;

packages/uhk-usb/src/models/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './duration.js';
44
export * from './i2c-baud-rate.js';
55
export * from './i2c-error-buffer.js';
66
export * from './load-configurations-result.js';
7+
export * from './module-flash-state.js';
78
export * from './pairing-info.js';
89
export * from './reenumerate-option.js';
910
export * from './reenumerate-result.js';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const MODULE_FLASH_STATE = Object.freeze({
2+
Idle: 0,
3+
Erasing: 1,
4+
Writing: 2,
5+
Done: 3,
6+
Error: 4,
7+
})
8+
9+
export type MODULE_FLASH_STATE_KEY_TYPE = keyof typeof MODULE_FLASH_STATE;
10+
export type MODULE_FLASH_STATE_TYPE = typeof MODULE_FLASH_STATE[MODULE_FLASH_STATE_KEY_TYPE];
11+
12+
export interface ModuleFlashResponse {
13+
state: MODULE_FLASH_STATE_TYPE;
14+
errorCode?: number;
15+
}

packages/uhk-usb/src/uhk-hid-device.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,8 +613,9 @@ export class UhkHidDevice {
613613
const activeLayerNumber = buffer[6] & 0x7f;
614614

615615
return {
616-
isEepromBusy: buffer[1] !== 0,
616+
isEepromBusy: isBitSet(buffer[1], 0),
617617
isMacroStatusDirty: buffer[7] !== 0,
618+
isModuleFlashBusy: isBitSet(buffer[1], 1),
618619
areHalvesMerged: isBitSet(buffer[2], 0),
619620
isLeftHalfConnected: buffer[3] !== 0,
620621
activeLayerNumber,

packages/uhk-usb/src/uhk-operations.ts

Lines changed: 108 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { McuManager, SerialPeripheral } from '@uhk/mcumgr';
22
import * as fs from 'fs';
3+
import { readFile } from 'node:fs/promises';
34
import { DataOption, KBoot, Properties, UsbPeripheral } from 'kboot';
45
import {
56
ALL_UHK_DEVICES,
@@ -46,6 +47,11 @@ import {
4647
UsbCommand,
4748
UsbVariables
4849
} from './constants.js';
50+
import {
51+
MODULE_FLASH_STATE,
52+
MODULE_FLASH_STATE_TYPE,
53+
ModuleFlashResponse,
54+
} from './models/module-flash-state.js';
4955
import {
5056
DebugInfo,
5157
Duration,
@@ -61,6 +67,7 @@ import {
6167
readBootloaderFirmwareFromHexFileAsync,
6268
waitForDevice
6369
} from './util.js';
70+
import crc16 from './utils/crc16.js';
6471
import { generateDeviceSerialNumber } from './utils/generate-device-serial-number.js';
6572
import { convertMsToDuration, convertSlaveI2cErrorBuffer, snooze, waitUntil} from './utils/index.js';
6673
import { normalizeStatusBuffer } from './utils/normalize-status-buffer.js';
@@ -198,7 +205,7 @@ export class UhkOperations {
198205
const peripheral = new SerialPeripheral(reenumerateResult.serialPath);
199206
const mcuManager = new McuManager(peripheral);
200207
this.logService.misc(`[UhkOperations] Read ${device.logName} firmware from file`);
201-
const configData = fs.readFileSync(firmwarePath);
208+
const configData = await readFile(firmwarePath);
202209
this.logService.misc('[UhkOperations] Write memory with mcumgr');
203210
await mcuManager.imageUpload(configData);
204211
this.logService.misc('[UhkOperations] Reset mcu bootloader');
@@ -276,7 +283,7 @@ export class UhkOperations {
276283
await kboot.flashEraseAllUnsecure();
277284

278285
this.logService.misc(`[UhkOperations] Read "${module.name}" firmware from file`);
279-
const configData = fs.readFileSync(firmwarePath);
286+
const configData = await readFile(firmwarePath);
280287

281288
this.logService.misc('[UhkOperations] Write memory');
282289
await kboot.configureI2c(module.i2cAddress);
@@ -305,6 +312,51 @@ export class UhkOperations {
305312
this.logService.misc(`[UhkOperations] "${module.name}" firmware successfully flashed`);
306313
}
307314

315+
public async updateModuleWithKbootNative(
316+
firmwarePath: string,
317+
device: UhkDeviceProduct,
318+
module: UhkModule
319+
): Promise<void> {
320+
this.logService.misc(`[UhkOperations][kboot-native] Start flashing "${module.name}" module firmware`);
321+
await this.device.reenumerate({
322+
device,
323+
enumerationMode: EnumerationModes.NormalKeyboard,
324+
});
325+
await this.device.close();
326+
await snooze(1000);
327+
328+
const configData = await readFile(firmwarePath) as any;
329+
this.logService.misc('[UhkOperations][kboot-native] sending firmware to the keyboard');
330+
await this.sendConfigToKeyboard(configData, UsbCommand.WriteModuleFirmware);
331+
332+
this.logService.misc('[UhkOperations][kboot-native] validating module firmware');
333+
const crc = crc16(configData);
334+
this.logService.misc(`[UhkOperations][kboot-native] module firmware crc: ${crc}`);
335+
const isValid = await this.isBufferValid(ConfigBufferId.moduleFirmware, configData.length, crc);
336+
337+
if (!isValid) {
338+
this.logService.error('[UhkOperations][kboot-native] module firmware crc validation failed');
339+
throw new Error('Module firmware crc validation failed');
340+
}
341+
342+
this.logService.misc('[UhkOperations][kboot-native] flashing module');
343+
await this.flashModule(module);
344+
345+
this.logService.misc('[UhkOperations][kboot-native] wait until flashing finished');
346+
const response = await this.waitUntilModuleFlashFinished();
347+
348+
if (response.state === MODULE_FLASH_STATE.Error) {
349+
this.logService.error(`[UhkOperations][kboot-native] error occurred in the module flashing process. Error code: ${response.errorCode}`);
350+
351+
throw new Error(`Error occurred in the module flashing process. Error code: ${response.errorCode}`);
352+
}
353+
354+
this.logService.misc('[UhkOperations][kboot-native] wait until flashing finished');
355+
await this.waitUntilModuleFlashFinished();
356+
357+
this.logService.misc(`[UhkOperations][kboot-native] "${module.name}" firmware successfully flashed`);
358+
}
359+
308360
/**
309361
* Return with the actual UserConfiguration from UHK Device
310362
* @returns {Promise<Buffer>}
@@ -423,7 +475,7 @@ export class UhkOperations {
423475

424476
const resultBuffer = new UhkBuffer(UHK_EEPROM_SIZE)
425477
userConfiguration.toBinary(resultBuffer)
426-
await this.sendConfigToKeyboard(resultBuffer.getBufferContent(), true);
478+
await this.sendConfigToKeyboard(resultBuffer.getBufferContent(), UsbCommand.WriteStagingUserConfig);
427479
await this.applyConfiguration();
428480
this.logService.usbOps('[DeviceOperation] USB[T]: Write user configuration to EEPROM');
429481
await this.writeConfigToEeprom(ConfigBufferId.validatedUserConfig);
@@ -455,7 +507,7 @@ export class UhkOperations {
455507
hardwareConfig.toBinary(hardwareBuffer);
456508
const buffer = hardwareBuffer.getBufferContent();
457509

458-
await this.sendConfigToKeyboard(buffer, false);
510+
await this.sendConfigToKeyboard(buffer, UsbCommand.WriteHardwareConfig);
459511
await this.writeConfigToEeprom(ConfigBufferId.hardwareConfig);
460512
await this.waitUntilKeyboardBusy();
461513
}
@@ -483,6 +535,25 @@ export class UhkOperations {
483535
}
484536
}
485537

538+
public async waitUntilModuleFlashFinished(): Promise<ModuleFlashResponse> {
539+
while (true) {
540+
const response = await this.getModuleFlashState();
541+
if (response.state === MODULE_FLASH_STATE.Erasing) {
542+
this.logService.misc('[DeviceOperation] Module flash erasing, wait...');
543+
await snooze(200);
544+
continue;
545+
}
546+
547+
if (response.state === MODULE_FLASH_STATE.Writing) {
548+
this.logService.misc('[DeviceOperation] Module flash writing, wait...');
549+
await snooze(200);
550+
continue;
551+
}
552+
553+
return response;
554+
}
555+
}
556+
486557
public async waitForKbootIdle(moduleName: string): Promise<boolean> {
487558
while (true) {
488559
const buffer = await this.device.write(Buffer.from([UsbCommand.GetProperty, DevicePropertyIds.CurrentKbootCommand]));
@@ -498,6 +569,15 @@ export class UhkOperations {
498569
}
499570
}
500571

572+
public async getModuleFlashState(): Promise<ModuleFlashResponse> {
573+
const buffer = await this.device.write(Buffer.from([UsbCommand.GetModuleFlashState]));
574+
575+
return {
576+
state: buffer[0] as MODULE_FLASH_STATE_TYPE,
577+
errorCode: buffer[1]
578+
}
579+
}
580+
501581
public async getModuleProperty({ module, property } : GetModulePropertyArguments): Promise<UhkBuffer> {
502582
const moduleSlotName = getSlotIdName(module);
503583

@@ -651,17 +731,24 @@ export class UhkOperations {
651731
public async eraseHardwareConfig(): Promise<void> {
652732
this.logService.usbOps('[DeviceOperation] USB[T]: Erase hardware configuration');
653733
const buffer = Buffer.from(Array(64).fill(0xff));
654-
await this.sendConfigToKeyboard(buffer, false);
734+
await this.sendConfigToKeyboard(buffer, UsbCommand.WriteHardwareConfig);
655735
await this.writeConfigToEeprom(ConfigBufferId.hardwareConfig);
656736
}
657737

658738
public async eraseUserConfig(): Promise<void> {
659739
this.logService.usbOps('[DeviceOperation] USB[T]: Erase user configuration');
660740
const buffer = Buffer.from(Array(2 ** 15 - 64).fill(0xff));
661-
await this.sendConfigToKeyboard(buffer, true);
741+
await this.sendConfigToKeyboard(buffer, UsbCommand.WriteStagingUserConfig);
662742
await this.writeConfigToEeprom(ConfigBufferId.stagingUserConfig);
663743
}
664744

745+
public async flashModule(module: UhkModule): Promise<void> {
746+
this.logService.usbOps('[DeviceOperation] USB[T]: Flash module');
747+
const buffer = Buffer.from([UsbCommand.FlashModule, module.slotId]);
748+
749+
await this.device.write(buffer);
750+
}
751+
665752
public async switchKeymap(keymapAbbreviation: string): Promise<void> {
666753
this.logService.usbOps('[DeviceOperation] USB[T]: Switch keymap');
667754
const keymapAbbreviationAscii = keymapAbbreviation.split('').map(char => char.charCodeAt(0));
@@ -909,15 +996,11 @@ export class UhkOperations {
909996
/**
910997
* IpcMain handler. Send the UserConfiguration to the UHK Device and send a response with the result.
911998
* @param {Buffer} buffer - UserConfiguration buffer
912-
* @param {Boolean} isUserConfiguration - User or Hardware configuration
999+
* @param {UsbCommand} command - Represent which configuration area write to the device
9131000
* @returns {Promise<void>}
9141001
* @private
9151002
*/
916-
private async sendConfigToKeyboard(buffer: Buffer, isUserConfiguration): Promise<void> {
917-
const command = isUserConfiguration
918-
? UsbCommand.WriteStagingUserConfig
919-
: UsbCommand.WriteHardwareConfig;
920-
1003+
private async sendConfigToKeyboard(buffer: Buffer, command: UsbCommand): Promise<void> {
9211004
const fragments = getTransferBuffers(command, buffer);
9221005
for (const fragment of fragments) {
9231006
await this.device.write(fragment);
@@ -937,4 +1020,17 @@ export class UhkOperations {
9371020

9381021
await this.device.write(buffer);
9391022
}
1023+
1024+
public async isBufferValid(bufferId: ConfigBufferId, expectedSize: number, expectedCrc: number): Promise<boolean> {
1025+
this.logService.usbOps('[DeviceOperation] USB[T]: Validate Buffer CRC');
1026+
const buffer = Buffer.alloc(6);
1027+
buffer.writeUInt8(UsbCommand.ValidateBufferCrc, 0);
1028+
buffer.writeUInt8(bufferId, 1);
1029+
buffer.writeUInt16LE(expectedSize, 2);
1030+
buffer.writeUInt16LE(expectedCrc, 4);
1031+
1032+
const response = await this.device.write(buffer);
1033+
1034+
return response[1] === 0;
1035+
}
9401036
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// TODO: eliminate the duplication of the uhk-usb-module
2+
const CRC16TABLE = new Uint16Array([
3+
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
4+
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
5+
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
6+
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
7+
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
8+
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
9+
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
10+
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
11+
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
12+
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
13+
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
14+
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
15+
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
16+
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
17+
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
18+
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
19+
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
20+
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
21+
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
22+
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
23+
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
24+
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
25+
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
26+
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
27+
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
28+
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
29+
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
30+
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
31+
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
32+
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
33+
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
34+
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
35+
]);
36+
37+
export default function crc16(data: ArrayLike<any>): number {
38+
let crc = 0;
39+
const l = data.length;
40+
for (let i = 0; i < l; i++) {
41+
crc = ((crc << 8) & 0xff00) ^ CRC16TABLE[((crc >> 8) & 0xff) ^ data[i]];
42+
}
43+
44+
return crc;
45+
}
46+

packages/usb/get-device-state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ setInterval(async function () {
1414
console.log(
1515
`isEepromBusy: ${state.isEepromBusy ? 'yes' : 'no'} | \
1616
isMacroStatusDirty: ${state.isMacroStatusDirty ? 'yes' : 'no'} | \
17+
isModuleFlashBusy: ${state.isModuleFlashBusy ? 'yes' : 'no'} | \
1718
isZephyrLogAvailable: ${state.isZephyrLogAvailable ? 'yes' : 'no'} | \
1819
areHalvesMerged: ${state.areHalvesMerged ? 'yes' : 'no'} | \
1920
newPairedDevice: ${state.newPairedDevice ? 'yes' : 'no'} | \
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env -S node --loader ts-node/esm --no-warnings=ExperimentalWarning
2+
3+
import fs from 'fs';
4+
import * as path from 'path';
5+
import { LEFT_HALF_MODULE } from 'uhk-common';
6+
import { getCurrentUhkDeviceProduct, getDeviceFirmwarePath, getFirmwarePackageJson } from 'uhk-usb';
7+
8+
import Uhk, { errorHandler, yargs } from './src/index.js';
9+
10+
(async function () {
11+
try {
12+
const argv = yargs
13+
.usage('Usage: $0 <firmware directory>')
14+
.argv;
15+
16+
const firmwarePath = argv._[0];
17+
18+
if (!fs.existsSync(firmwarePath)) {
19+
console.log('Firmware directory does not exists.');
20+
process.exit(1);
21+
}
22+
23+
const uhkDeviceProduct = await getCurrentUhkDeviceProduct(argv);
24+
25+
const { operations } = Uhk(argv);
26+
27+
const packageJsonPath = path.join(firmwarePath, 'package.json');
28+
const packageJson = await getFirmwarePackageJson({
29+
packageJsonPath,
30+
tmpDirectory: firmwarePath
31+
});
32+
const rightFirmwarePath = getDeviceFirmwarePath(uhkDeviceProduct, packageJson);
33+
34+
await operations.updateRightFirmwareWithKboot(rightFirmwarePath, uhkDeviceProduct);
35+
const leftFirmwarePath = path.join(firmwarePath, 'modules/uhk60-left.bin');
36+
await operations.updateModuleWithKbootNative(leftFirmwarePath, uhkDeviceProduct, LEFT_HALF_MODULE);
37+
} catch (error) {
38+
await errorHandler(error);
39+
}
40+
})();

0 commit comments

Comments
 (0)