feat: kboot package (#894)
* feat: kboot package * feat: kboot package * fix: wait 1 sec after device is available * test: fix unit test * refactor: clean unused codes * doc: improve readme.md * doc: improve readme.md * test: fix unit test * chore: fix lint settings * style: fix linting issues
This commit is contained in:
committed by
László Monda
parent
bfc08edfce
commit
3964698cf7
@@ -71,9 +71,9 @@ export enum EnumerationNameToProductId {
|
||||
}
|
||||
|
||||
export enum ModuleSlotToI2cAddress {
|
||||
leftHalf = '0x10',
|
||||
leftAddon = '0x20',
|
||||
rightAddon = '0x30'
|
||||
leftHalf = 0x10,
|
||||
leftAddon = 0x20,
|
||||
rightAddon = 0x30
|
||||
}
|
||||
|
||||
export enum ModuleSlotToId {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './constants';
|
||||
export * from './uhk-blhost';
|
||||
export * from './uhk-hid-device';
|
||||
export * from './uhk-operations';
|
||||
export * from './util';
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import * as path from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import { LogService } from 'uhk-common';
|
||||
import { retry } from './util';
|
||||
|
||||
export class UhkBlhost {
|
||||
private blhostPath: string;
|
||||
|
||||
constructor(private logService: LogService,
|
||||
private rootDir: string) {
|
||||
}
|
||||
|
||||
public async runBlhostCommand(params: Array<string>): Promise<void> {
|
||||
const self = this;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const blhostPath = this.getBlhostPath();
|
||||
self.logService.debug(`[blhost] RUN: ${blhostPath} ${params.join(' ')}`);
|
||||
const childProcess = spawn(`"${blhostPath}"`, params, {shell: true});
|
||||
let finished = false;
|
||||
|
||||
childProcess.stdout.on('data', data => {
|
||||
self.logService.debug(`[blhost] STDOUT: ${data}`);
|
||||
});
|
||||
|
||||
childProcess.stderr.on('data', data => {
|
||||
self.logService.error(`[blhost] STDERR: ${data}`);
|
||||
});
|
||||
|
||||
childProcess.on('close', code => {
|
||||
self.logService.debug(`[blhost] CLOSE_CODE: ${code}`);
|
||||
finish(code);
|
||||
});
|
||||
|
||||
childProcess.on('exit', code => {
|
||||
self.logService.debug(`[blhost] EXIT_CODE: ${code}`);
|
||||
finish(code);
|
||||
});
|
||||
|
||||
childProcess.on('error', err => {
|
||||
self.logService.debug(`[blhost] ERROR: ${err}`);
|
||||
});
|
||||
|
||||
function finish(code) {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
|
||||
finished = true;
|
||||
|
||||
self.logService.debug(`[blhost] FINISHED: ${code}`);
|
||||
|
||||
if (code !== 0) {
|
||||
return reject(new Error(`blhost error code:${code}`));
|
||||
}
|
||||
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async runBlhostCommandRetry(params: Array<string>, maxTry = 100): Promise<void> {
|
||||
return await retry(async () => await this.runBlhostCommand(params), maxTry, this.logService);
|
||||
}
|
||||
|
||||
private getBlhostPath(): string {
|
||||
if (this.blhostPath) {
|
||||
return this.blhostPath;
|
||||
}
|
||||
|
||||
let blhostPath;
|
||||
switch (process.platform) {
|
||||
case 'linux':
|
||||
blhostPath = 'linux/x86_64/blhost';
|
||||
break;
|
||||
case 'darwin':
|
||||
blhostPath = 'mac/blhost';
|
||||
break;
|
||||
case 'win32':
|
||||
blhostPath = 'win/blhost.exe';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Could not find blhost path. Unknown platform:${process.platform}`);
|
||||
}
|
||||
|
||||
this.blhostPath = path.join(this.rootDir, `packages/blhost/${blhostPath}`);
|
||||
|
||||
return this.blhostPath;
|
||||
}
|
||||
}
|
||||
@@ -242,7 +242,7 @@ export class UhkHidDevice {
|
||||
if (command === KbootCommands.idle) {
|
||||
transfer = new Buffer([UsbCommand.SendKbootCommandToModule, command]);
|
||||
} else {
|
||||
transfer = new Buffer([UsbCommand.SendKbootCommandToModule, command, Number.parseInt(module)]);
|
||||
transfer = new Buffer([UsbCommand.SendKbootCommandToModule, command, module]);
|
||||
}
|
||||
await retry(async () => await this.write(transfer), maxTry, this.logService);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { HardwareModuleInfo, LogService, UhkBuffer } from 'uhk-common';
|
||||
import { DataOption, KBoot, Properties, UsbPeripheral } from 'kboot';
|
||||
|
||||
import {
|
||||
Constants,
|
||||
EnumerationModes,
|
||||
EnumerationNameToProductId,
|
||||
KbootCommands,
|
||||
@@ -10,21 +13,13 @@ import {
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import { UhkBlhost } from './uhk-blhost';
|
||||
import { UhkHidDevice } from './uhk-hid-device';
|
||||
import { snooze } from './util';
|
||||
import {
|
||||
convertBufferToIntArray,
|
||||
getTransferBuffers,
|
||||
DevicePropertyIds,
|
||||
UsbCommand,
|
||||
ConfigBufferId
|
||||
} from '../index';
|
||||
import { readBootloaderFirmwareFromHexFileAsync, snooze, waitForDevice } from './util';
|
||||
import { ConfigBufferId, convertBufferToIntArray, DevicePropertyIds, getTransferBuffers, UsbCommand } from '../index';
|
||||
import { LoadConfigurationsResult } from './models/load-configurations-result';
|
||||
|
||||
export class UhkOperations {
|
||||
constructor(private logService: LogService,
|
||||
private blhost: UhkBlhost,
|
||||
private device: UhkHidDevice,
|
||||
private rootDir: string) {
|
||||
}
|
||||
@@ -32,23 +27,40 @@ export class UhkOperations {
|
||||
public async updateRightFirmware(firmwarePath = this.getFirmwarePath()) {
|
||||
this.logService.debug(`[UhkOperations] Operating system: ${os.type()} ${os.release()} ${os.arch()}`);
|
||||
this.logService.debug('[UhkOperations] Start flashing right firmware');
|
||||
const prefix = [`--usb 0x1d50,0x${EnumerationNameToProductId.bootloader.toString(16)}`];
|
||||
|
||||
this.logService.info('[UhkOperations] Reenumerate bootloader');
|
||||
await this.device.reenumerate(EnumerationModes.Bootloader);
|
||||
this.device.close();
|
||||
await this.blhost.runBlhostCommand([...prefix, 'flash-security-disable', '0403020108070605']);
|
||||
await this.blhost.runBlhostCommand([...prefix, 'flash-erase-region', '0xc000', '475136']);
|
||||
await this.blhost.runBlhostCommand([...prefix, 'flash-image', `"${firmwarePath}"`]);
|
||||
await this.blhost.runBlhostCommand([...prefix, 'reset']);
|
||||
const kboot = new KBoot(new UsbPeripheral({ productId: Constants.BOOTLOADER_ID, vendorId: Constants.VENDOR_ID }));
|
||||
this.logService.info('[UhkOperations] Waiting for bootloader');
|
||||
await waitForDevice(Constants.VENDOR_ID, Constants.BOOTLOADER_ID);
|
||||
this.logService.info('[UhkOperations] Flash security disable');
|
||||
await kboot.flashSecurityDisable([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
|
||||
this.logService.info('[UhkOperations] Flash erase region');
|
||||
await kboot.flashEraseRegion(0xc000, 475136);
|
||||
|
||||
this.logService.debug('[UhkOperations] Read RIGHT firmware from file');
|
||||
const bootloaderMemoryMap = await readBootloaderFirmwareFromHexFileAsync(firmwarePath);
|
||||
this.logService.info('[UhkOperations] Write memory');
|
||||
for (const [startAddress, data] of bootloaderMemoryMap.entries()) {
|
||||
const dataOption: DataOption = {
|
||||
startAddress,
|
||||
data
|
||||
};
|
||||
|
||||
await kboot.writeMemory(dataOption);
|
||||
}
|
||||
|
||||
this.logService.info('[UhkOperations] Reset bootloader');
|
||||
await kboot.reset();
|
||||
this.logService.info('[UhkOperations] Close communication channels');
|
||||
kboot.close();
|
||||
this.logService.debug('[UhkOperations] Right firmware successfully flashed');
|
||||
}
|
||||
|
||||
public async updateLeftModule(firmwarePath = this.getLeftModuleFirmwarePath()) {
|
||||
this.logService.debug('[UhkOperations] Start flashing left module firmware');
|
||||
|
||||
const prefix = [`--usb 0x1d50,0x${EnumerationNameToProductId.buspal.toString(16)}`];
|
||||
const buspalPrefix = [...prefix, `--buspal i2c,${ModuleSlotToI2cAddress.leftHalf}`];
|
||||
|
||||
await this.device.reenumerate(EnumerationModes.NormalKeyboard);
|
||||
this.device.close();
|
||||
await snooze(1000);
|
||||
@@ -66,14 +78,49 @@ export class UhkOperations {
|
||||
|
||||
await this.device.reenumerate(EnumerationModes.Buspal);
|
||||
this.device.close();
|
||||
await this.blhost.runBlhostCommandRetry([...buspalPrefix, 'get-property', '1']);
|
||||
await this.blhost.runBlhostCommand([...buspalPrefix, 'flash-erase-all-unsecure']);
|
||||
await this.blhost.runBlhostCommand([...buspalPrefix, 'write-memory', '0x0', `"${firmwarePath}"`]);
|
||||
await this.blhost.runBlhostCommand([...prefix, 'reset']);
|
||||
this.logService.info('[UhkOperations] Waiting for buspal');
|
||||
await waitForDevice(Constants.VENDOR_ID, EnumerationNameToProductId.buspal);
|
||||
let tryCount = 0;
|
||||
const usbPeripheral = new UsbPeripheral({ productId: EnumerationNameToProductId.buspal, vendorId: Constants.VENDOR_ID });
|
||||
const kboot = new KBoot(usbPeripheral);
|
||||
while (true) {
|
||||
try {
|
||||
this.logService.debug('[UhkOperations] Try to connect to the LEFT keyboard');
|
||||
await kboot.configureI2c(ModuleSlotToI2cAddress.leftHalf);
|
||||
await kboot.getProperty(Properties.BootloaderVersion);
|
||||
break;
|
||||
} catch {
|
||||
if (tryCount > 100) {
|
||||
throw new Error('Can not connect to the LEFT keyboard');
|
||||
}
|
||||
} finally {
|
||||
kboot.close();
|
||||
}
|
||||
await snooze(100);
|
||||
tryCount++;
|
||||
}
|
||||
this.logService.debug('[UhkOperations] Flash erase all on LEFT keyboard');
|
||||
await kboot.configureI2c(ModuleSlotToI2cAddress.leftHalf);
|
||||
await kboot.flashEraseAllUnsecure();
|
||||
|
||||
this.logService.debug('[UhkOperations] Read LEFT firmware from file');
|
||||
const configData = fs.readFileSync(firmwarePath);
|
||||
|
||||
this.logService.debug('[UhkOperations] Write memory');
|
||||
await kboot.configureI2c(ModuleSlotToI2cAddress.leftHalf);
|
||||
await kboot.writeMemory({ startAddress: 0, data: configData });
|
||||
|
||||
this.logService.debug('[UhkOperations] Reset LEFT keyboard');
|
||||
await kboot.reset();
|
||||
|
||||
this.logService.info('[UhkOperations] Close communication channels');
|
||||
kboot.close();
|
||||
|
||||
await snooze(1000);
|
||||
await this.device.reenumerate(EnumerationModes.NormalKeyboard);
|
||||
this.device.close();
|
||||
await snooze(1000);
|
||||
this.logService.info('[UhkOperations] Waiting for normalKeyboard');
|
||||
await waitForDevice(Constants.VENDOR_ID, EnumerationNameToProductId.normalKeyboard);
|
||||
await this.device.sendKbootCommandToModule(ModuleSlotToI2cAddress.leftHalf, KbootCommands.reset, 100);
|
||||
this.device.close();
|
||||
await snooze(1000);
|
||||
@@ -173,8 +220,7 @@ export class UhkOperations {
|
||||
await this.sendUserConfigToKeyboard(buffer);
|
||||
this.logService.debug('[DeviceOperation] USB[T]: Write user configuration to EEPROM');
|
||||
await this.device.writeConfigToEeprom(ConfigBufferId.validatedUserConfig);
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
this.logService.error('[DeviceOperation] Transferring error', error);
|
||||
throw error;
|
||||
} finally {
|
||||
@@ -221,8 +267,7 @@ export class UhkOperations {
|
||||
moduleProtocolVersion: `${uhkBuffer.readUInt16()}.${uhkBuffer.readUInt16()}.${uhkBuffer.readUInt16()}`,
|
||||
firmwareVersion: `${uhkBuffer.readUInt16()}.${uhkBuffer.readUInt16()}.${uhkBuffer.readUInt16()}`
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
this.logService.error('[DeviceOperation] Could not read left module version information', error);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Device } from 'node-hid';
|
||||
import { Device, devices } from 'node-hid';
|
||||
import { readFile } from 'fs-extra';
|
||||
import { EOL } from 'os';
|
||||
import MemoryMap from 'nrf-intel-hex';
|
||||
import { LogService } from 'uhk-common';
|
||||
|
||||
import { Constants, UsbCommand } from './constants';
|
||||
@@ -122,3 +123,30 @@ export const getFileContentAsync = async (filePath: string): Promise<Array<strin
|
||||
.map(x => x.trim())
|
||||
.filter(x => !x.startsWith('#') && x.length > 0);
|
||||
};
|
||||
|
||||
export const readBootloaderFirmwareFromHexFileAsync = async (hexFilePath: string): Promise<Map<any, any>> => {
|
||||
const fileContent = await readFile(hexFilePath, { encoding: 'utf8' });
|
||||
const memoryMap = MemoryMap.fromHex(fileContent);
|
||||
|
||||
return memoryMap;
|
||||
};
|
||||
|
||||
export const waitForDevice = async (vendorId: number, productId: number): Promise<void> => {
|
||||
const startTime = new Date().getTime() + 15000;
|
||||
|
||||
while (startTime > new Date().getTime()) {
|
||||
|
||||
const isAvailable = devices()
|
||||
.some(dev => dev.vendorId === vendorId && dev.productId === productId);
|
||||
|
||||
if (isAvailable) {
|
||||
await snooze(1000);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await snooze(250);
|
||||
}
|
||||
|
||||
throw new Error(`Cannot find device with vendorId: ${vendorId}, productId: ${productId}`);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user