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:
Róbert Kiss
2019-01-18 17:37:31 +01:00
committed by László Monda
parent bfc08edfce
commit 3964698cf7
53 changed files with 1784 additions and 249 deletions

View File

@@ -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 {

View File

@@ -1,5 +1,4 @@
export * from './constants';
export * from './uhk-blhost';
export * from './uhk-hid-device';
export * from './uhk-operations';
export * from './util';

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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}`);
};