feat(device): flash device firmware from Agent (#499)

* add dataModelVersion, usbProtocolVersion, slaveProtocolVersion

* read the package.json at appstart

* flash firmware

* update firmware

* fix extra resource path

* fix import modules

* update lock files

* fix imports

* terminal window

* exclude tmp folder from git repo

* ok button

* auto scroll in xterm

* fix maxTry count calculation

* optimize logging

* optimize timeout

* readSync

* Add extra delay

* fix async call

* fix error message in log

* fix ok button disable state

* retry

* list devices

* close device after reenumeration

* retry snooze

* kboot maxtry 10

* retry 100

* remove deprecated toPayload ngrx helper

* flash firmware with custom file

* fix tslint
This commit is contained in:
Róbert Kiss
2017-11-27 22:12:43 +01:00
committed by László Monda
parent f608791a09
commit 297fd3be79
52 changed files with 3914 additions and 894 deletions

View File

@@ -9,13 +9,16 @@ export namespace Constants {
*/
export enum UsbCommand {
GetProperty = 0,
Reenumerate = 1,
UploadUserConfig = 8,
ApplyConfig = 9,
LaunchEepromTransfer = 12,
ReadHardwareConfig = 13,
WriteHardwareConfig = 14,
ReadUserConfig = 15,
GetKeyboardState = 16
GetKeyboardState = 16,
JumpToModuleBootloader = 18,
SendKbootCommandToModule = 19
}
export enum EepromTransfer {
@@ -33,3 +36,42 @@ export enum SystemPropertyIds {
HardwareConfigSize = 4,
MaxUserConfigSize = 5
}
export enum EnumerationModes {
Bootloader = 0,
Buspal = 1,
NormalKeyboard = 2,
CompatibleKeyboard = 3
}
export const enumerationModeIdToProductId = {
'0': 0x6120,
'1': 0x6121,
'2': 0x6122,
'3': 0x6123
};
export enum EnumerationNameToProductId {
bootloader = 0x6120,
buspal = 0x6121,
normalKeyboard = 0x6122,
compatibleKeyboard = 0x6123
}
export enum ModuleSlotToI2cAddress {
leftHalf = '0x10',
leftAddon = '0x20',
rightAddon = '0x30'
}
export enum ModuleSlotToId {
leftHalf = 1,
leftAddon = 2,
rightAddon = 3
}
export enum KbootCommands {
idle = 0,
ping = 1,
reset = 2
}

View File

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

View File

@@ -0,0 +1,89 @@
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 !== null && 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/amd64/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

@@ -1,91 +1,24 @@
import { Device, devices, HID } from 'node-hid';
import { LogService } from 'uhk-common';
import { Constants, EepromTransfer, UsbCommand } from './constants';
import {
Constants,
EepromTransfer,
enumerationModeIdToProductId,
EnumerationModes,
KbootCommands,
ModuleSlotToI2cAddress,
ModuleSlotToId,
UsbCommand
} from './constants';
import { bufferToString, getTransferData, retry, snooze } from './util';
const snooze = ms => new Promise(resolve => setTimeout(resolve, ms));
export const BOOTLOADER_TIMEOUT_MS = 5000;
/**
* HID API wrapper to support unified logging and async write
*/
export class UhkHidDevice {
/**
* Convert the Buffer to number[]
* @param {Buffer} buffer
* @returns {number[]}
* @private
* @static
*/
public static convertBufferToIntArray(buffer: Buffer): number[] {
return Array.prototype.slice.call(buffer, 0);
}
/**
* Split the communication package into 64 byte fragments
* @param {UsbCommand} usbCommand
* @param {Buffer} configBuffer
* @returns {Buffer[]}
* @private
*/
public static getTransferBuffers(usbCommand: UsbCommand, configBuffer: Buffer): Buffer[] {
const fragments: Buffer[] = [];
const MAX_SENDING_PAYLOAD_SIZE = Constants.MAX_PAYLOAD_SIZE - 4;
for (let offset = 0; offset < configBuffer.length; offset += MAX_SENDING_PAYLOAD_SIZE) {
const length = offset + MAX_SENDING_PAYLOAD_SIZE < configBuffer.length
? MAX_SENDING_PAYLOAD_SIZE
: configBuffer.length - offset;
const header = new Buffer([usbCommand, length, offset & 0xFF, offset >> 8]);
fragments.push(Buffer.concat([header, configBuffer.slice(offset, offset + length)]));
}
return fragments;
}
/**
* Create the communication package that will send over USB and
* - add usb report code as 1st byte
* - https://github.com/node-hid/node-hid/issues/187 issue
* @param {Buffer} buffer
* @returns {number[]}
* @private
* @static
*/
private static getTransferData(buffer: Buffer): number[] {
const data = UhkHidDevice.convertBufferToIntArray(buffer);
// if data start with 0 need to add additional leading zero because HID API remove it.
// https://github.com/node-hid/node-hid/issues/187
// if (data.length > 0 && data[0] === 0 && process.platform === 'win32') {
// data.unshift(0);
// }
// From HID API documentation:
// http://www.signal11.us/oss/hidapi/hidapi/doxygen/html/group__API.html#gad14ea48e440cf5066df87cc6488493af
// The first byte of data[] must contain the Report ID.
// For devices which only support a single report, this must be set to 0x0.
data.unshift(0);
return data;
}
/**
* Convert buffer to space separated hexadecimal string
* @param {Buffer} buffer
* @returns {string}
* @private
* @static
*/
private static bufferToString(buffer: Array<number>): string {
let str = '';
for (let i = 0; i < buffer.length; i++) {
let hex = buffer[i].toString(16) + ' ';
if (hex.length <= 2) {
hex = '0' + hex;
}
str += hex;
}
return str;
}
/**
* Internal variable that represent the USB UHK device
* @private
@@ -132,7 +65,7 @@ export class UhkHidDevice {
this.logService.error('[UhkHidDevice] Transfer error: ', err);
return reject(err);
}
const logString = UhkHidDevice.bufferToString(receivedData);
const logString = bufferToString(receivedData);
this.logService.debug('[UhkHidDevice] USB[R]:', logString);
if (receivedData[0] !== 0) {
@@ -142,8 +75,8 @@ export class UhkHidDevice {
return resolve(Buffer.from(receivedData));
});
const sendData = UhkHidDevice.getTransferData(buffer);
this.logService.debug('[UhkHidDevice] USB[W]:', UhkHidDevice.bufferToString(sendData).substr(3));
const sendData = getTransferData(buffer);
this.logService.debug('[UhkHidDevice] USB[W]:', bufferToString(sendData).substr(3));
device.write(sendData);
});
}
@@ -157,13 +90,13 @@ export class UhkHidDevice {
* Close the communication chanel with UHK Device
*/
public close(): void {
this.logService.info('[UhkHidDevice] Device communication closing.');
this.logService.debug('[UhkHidDevice] Device communication closing.');
if (!this._device) {
return;
}
this._device.close();
this._device = null;
this.logService.info('[UhkHidDevice] Device communication closed.');
this.logService.debug('[UhkHidDevice] Device communication closed.');
}
public async waitUntilKeyboardBusy(): Promise<void> {
@@ -177,6 +110,77 @@ export class UhkHidDevice {
}
}
async reenumerate(enumerationMode: EnumerationModes): Promise<void> {
const reenumMode = EnumerationModes[enumerationMode].toString();
this.logService.debug(`[UhkHidDevice] Start reenumeration, mode: ${reenumMode}`);
const message = new Buffer([
UsbCommand.Reenumerate,
enumerationMode,
BOOTLOADER_TIMEOUT_MS & 0xff,
(BOOTLOADER_TIMEOUT_MS & 0xff << 8) >> 8,
(BOOTLOADER_TIMEOUT_MS & 0xff << 16) >> 16,
(BOOTLOADER_TIMEOUT_MS & 0xff << 24) >> 24
]);
const enumeratedProductId = enumerationModeIdToProductId[enumerationMode.toString()];
const startTime = new Date();
let jumped = false;
while (new Date().getTime() - startTime.getTime() < 20000) {
const devs = devices();
this.logService.silly('[UhkHidDevice] reenumeration devices', devs);
const inBootloaderMode = devs.some((x: Device) =>
x.vendorId === Constants.VENDOR_ID &&
x.productId === enumeratedProductId);
if (inBootloaderMode) {
this.logService.debug(`[UhkHidDevice] reenumeration devices up`);
return;
}
this.logService.silly(`[UhkHidDevice] Could not find reenumerated device: ${reenumMode}. Waiting...`);
await snooze(100);
if (!jumped) {
const device = this.getDevice();
if (device) {
const data = getTransferData(message);
this.logService.debug(`[UhkHidDevice] USB[T]: Enumerate device. Mode: ${reenumMode}`);
this.logService.debug('[UhkHidDevice] USB[W]:', bufferToString(data).substr(3));
device.write(data);
device.close();
jumped = true;
} else {
this.logService.silly(`[UhkHidDevice] USB[T]: Enumerate device is not ready yet}`);
}
}
}
this.logService.error(`[UhkHidDevice] Could not find reenumerated device: ${reenumMode}. Timeout`);
throw new Error(`Could not reenumerate as ${reenumMode}`);
}
async sendKbootCommandToModule(module: ModuleSlotToI2cAddress, command: KbootCommands, maxTry = 1): Promise<any> {
let transfer;
const moduleName = kbootKommandName(module);
this.logService.debug(`[UhkHidDevice] USB[T]: Send KbootCommand ${moduleName} ${KbootCommands[command].toString()}`);
if (command === KbootCommands.idle) {
transfer = new Buffer([UsbCommand.SendKbootCommandToModule, command]);
} else {
transfer = new Buffer([UsbCommand.SendKbootCommandToModule, command, Number.parseInt(module)]);
}
await retry(async () => await this.write(transfer), maxTry, this.logService);
}
async jumpToBootloaderModule(module: ModuleSlotToId): Promise<any> {
this.logService.debug(`[UhkHidDevice] USB[T]: Jump to bootloader. Module: ${ModuleSlotToId[module].toString()}`);
const transfer = new Buffer([UsbCommand.JumpToModuleBootloader, module]);
await this.write(transfer);
}
/**
* Return the stored version of HID device. If not exist try to initialize.
* @returns {HID}
@@ -205,11 +209,11 @@ export class UhkHidDevice {
((x.usagePage === 128 && x.usage === 129) || x.interface === 0));
if (!dev) {
this.logService.info('[UhkHidDevice] UHK Device not found:');
this.logService.debug('[UhkHidDevice] UHK Device not found:');
return null;
}
const device = new HID(dev.path);
this.logService.info('[UhkHidDevice] Used device:', dev);
this.logService.debug('[UhkHidDevice] Used device:', dev);
return device;
}
catch (err) {
@@ -218,5 +222,20 @@ export class UhkHidDevice {
return null;
}
}
function kbootKommandName(module: ModuleSlotToI2cAddress): string {
switch (module) {
case ModuleSlotToI2cAddress.leftHalf:
return 'leftHalf';
case ModuleSlotToI2cAddress.leftAddon:
return 'leftAddon';
case ModuleSlotToI2cAddress.rightAddon:
return 'rightAddon';
default :
return 'Unknown';
}
}

View File

@@ -0,0 +1,80 @@
import { LogService } from 'uhk-common';
import { EnumerationModes, EnumerationNameToProductId, KbootCommands, ModuleSlotToI2cAddress, ModuleSlotToId } from './constants';
import * as path from 'path';
import * as fs from 'fs';
import { UhkBlhost } from './uhk-blhost';
import { UhkHidDevice } from './uhk-hid-device';
import { snooze } from './util';
export class UhkOperations {
constructor(private logService: LogService,
private blhost: UhkBlhost,
private device: UhkHidDevice,
private rootDir: string) {
}
public async updateRightFirmware(firmwarePath = this.getFirmwarePath()) {
this.logService.debug('[UhkOperations] Start flashing right firmware');
const prefix = [`--usb 0x1d50,0x${EnumerationNameToProductId.bootloader.toString(16)}`];
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']);
this.logService.debug('[UhkOperations] End flashing right firmware');
}
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},100k`];
await this.device.reenumerate(EnumerationModes.NormalKeyboard);
this.device.close();
await snooze(1000);
await this.device.sendKbootCommandToModule(ModuleSlotToI2cAddress.leftHalf, KbootCommands.ping, 100);
await snooze(1000);
await this.device.jumpToBootloaderModule(ModuleSlotToId.leftHalf);
this.device.close();
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']);
await snooze(1000);
await this.device.reenumerate(EnumerationModes.NormalKeyboard);
this.device.close();
await snooze(1000);
await this.device.sendKbootCommandToModule(ModuleSlotToI2cAddress.leftHalf, KbootCommands.reset, 100);
this.device.close();
await snooze(1000);
await this.device.sendKbootCommandToModule(ModuleSlotToI2cAddress.leftHalf, KbootCommands.idle);
this.device.close();
this.logService.debug('[UhkOperations] End flashing left module firmware');
}
private getFirmwarePath(): string {
const firmware = path.join(this.rootDir, 'packages/firmware/devices/uhk60-right/firmware.hex');
if (fs.existsSync(firmware)) {
return firmware;
}
throw new Error(`Could not found firmware ${firmware}`);
}
private getLeftModuleFirmwarePath(): string {
const firmware = path.join(this.rootDir, 'packages/firmware/modules/uhk60-left.bin');
if (fs.existsSync(firmware)) {
return firmware;
}
throw new Error(`Could not found firmware ${firmware}`);
}
}

View File

@@ -0,0 +1,97 @@
import { Constants, UsbCommand } from './constants';
import { LogService } from '../../uhk-common';
export const snooze = ms => new Promise(resolve => setTimeout(resolve, ms));
/**
* Convert the Buffer to number[]
* @param {Buffer} buffer
* @returns {number[]}
* @private
* @static
*/
export function convertBufferToIntArray(buffer: Buffer): number[] {
return Array.prototype.slice.call(buffer, 0);
}
/**
* Split the communication package into 64 byte fragments
* @param {UsbCommand} usbCommand
* @param {Buffer} configBuffer
* @returns {Buffer[]}
* @private
*/
export function getTransferBuffers(usbCommand: UsbCommand, configBuffer: Buffer): Buffer[] {
const fragments: Buffer[] = [];
const MAX_SENDING_PAYLOAD_SIZE = Constants.MAX_PAYLOAD_SIZE - 4;
for (let offset = 0; offset < configBuffer.length; offset += MAX_SENDING_PAYLOAD_SIZE) {
const length = offset + MAX_SENDING_PAYLOAD_SIZE < configBuffer.length
? MAX_SENDING_PAYLOAD_SIZE
: configBuffer.length - offset;
const header = new Buffer([usbCommand, length, offset & 0xFF, offset >> 8]);
fragments.push(Buffer.concat([header, configBuffer.slice(offset, offset + length)]));
}
return fragments;
}
/**
* Create the communication package that will send over USB and
* @param {Buffer} buffer
* @returns {number[]}
* @private
* @static
*/
export function getTransferData(buffer: Buffer): number[] {
const data = convertBufferToIntArray(buffer);
data.unshift(0);
return data;
}
/**
* Convert buffer to space separated hexadecimal string
* @param {Buffer} buffer
* @returns {string}
* @private
* @static
*/
export function bufferToString(buffer: Array<number>): string {
let str = '';
for (let i = 0; i < buffer.length; i++) {
let hex = buffer[i].toString(16) + ' ';
if (hex.length <= 2) {
hex = '0' + hex;
}
str += hex;
}
return str;
}
export async function retry(command: Function, maxTry = 3, logService?: LogService): Promise<any> {
let retryCount = 0;
while (true) {
try {
// logService.debug(`[retry] try to run FUNCTION:\n ${command}, \n retry: ${retryCount}`);
await command();
await snooze(100);
// logService.debug(`[retry] success FUNCTION:\n ${command}, \n retry: ${retryCount}`);
return;
} catch (err) {
retryCount++;
if (retryCount >= maxTry) {
if (logService) {
// logService.error(`[retry] failed and no try rerun FUNCTION:\n ${command}, \n retry: ${retryCount}`);
}
throw err;
} else {
if (logService) {
logService.error(`[retry] failed, but try run FUNCTION:\n ${command}, \n retry: ${retryCount}`);
}
}
}
}
}