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

121
package-lock.json generated
View File

@@ -9467,15 +9467,6 @@
"integrity": "sha512-wo+yjrdAtoXt43Vy92a+0IPCYViiyLAHyp0QVS4xL/tfvVz5sXIW1ubLZk3nhVkD92fQpUMKX+fzMjr5F489vw==",
"dev": true
},
"homedir-polyfill": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz",
"integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=",
"dev": true,
"requires": {
"parse-passwd": "^1.0.0"
}
},
"hosted-git-info": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz",
@@ -10390,79 +10381,12 @@
}
},
"jasmine-ts": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/jasmine-ts/-/jasmine-ts-0.2.1.tgz",
"integrity": "sha512-Ljieg2aAfd8JHSmSQgQpGNTCWzD05LdbX21dkmRKuk9xqEz9ip17+033UiWKOUeIy2t+adiOfo0vZzEV61z96A==",
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/jasmine-ts/-/jasmine-ts-0.3.0.tgz",
"integrity": "sha512-K5joodjVOh3bnD06CNXC8P5htDq/r0Rhjv66ECOpdIGFLly8kM7V+X/GXcd9kv+xO+tIq3q9Y8B5OF6yr/iiDw==",
"dev": true,
"requires": {
"jasmine": "^2.6.0",
"ts-node": "^3.2.0",
"typescript": "^2.4.1",
"yargs": "^8.0.2"
},
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"chalk": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
"integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"source-map-support": {
"version": "0.4.18",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
"integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
"dev": true,
"requires": {
"source-map": "^0.5.6"
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
},
"ts-node": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.3.0.tgz",
"integrity": "sha1-wTxqMCTjC+EYDdUwOPwgkonUv2k=",
"dev": true,
"requires": {
"arrify": "^1.0.0",
"chalk": "^2.0.0",
"diff": "^3.1.0",
"make-error": "^1.1.1",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"source-map-support": "^0.4.0",
"tsconfig": "^6.0.0",
"v8flags": "^3.0.0",
"yn": "^2.0.0"
}
}
}
},
"js-base64": {
@@ -12221,6 +12145,12 @@
"set-blocking": "~2.0.0"
}
},
"nrf-intel-hex": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/nrf-intel-hex/-/nrf-intel-hex-1.3.0.tgz",
"integrity": "sha512-oXwBJxX/0Jc4fe2Jxjv3Mw9/qw9JdToDLvJuozfVx+twpkc2oSUm8W/OODX6W4kmWOaYA11ORpGLfQ8BP7mndw==",
"dev": true
},
"nth-check": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz",
@@ -12866,12 +12796,6 @@
"error-ex": "^1.2.0"
}
},
"parse-passwd": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
"dev": true
},
"pascalcase": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
@@ -16645,24 +16569,6 @@
"yn": "^2.0.0"
}
},
"tsconfig": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-6.0.0.tgz",
"integrity": "sha1-aw6DdgA9evGGT434+J3QBZ/80DI=",
"dev": true,
"requires": {
"strip-bom": "^3.0.0",
"strip-json-comments": "^2.0.0"
},
"dependencies": {
"strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
"dev": true
}
}
},
"tslib": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz",
@@ -17285,15 +17191,6 @@
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
"dev": true
},
"v8flags": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.1.tgz",
"integrity": "sha512-iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==",
"dev": true,
"requires": {
"homedir-polyfill": "^1.0.1"
}
},
"validate-npm-package-license": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz",

View File

@@ -56,19 +56,21 @@
"jasmine": "2.8.0",
"jasmine-core": "2.8.0",
"jasmine-node": "2.0.1",
"jasmine-ts": "0.2.1",
"jasmine-ts": "0.3.0",
"jsonfile": "4.0.0",
"lerna": "3.2.0",
"lodash-es": "4.17.4",
"mkdirp": "0.5.1",
"node-hid": "0.7.3",
"npm-run-all": "4.0.2",
"nrf-intel-hex": "1.3.0",
"pre-commit": "1.2.2",
"request": "2.88.0",
"rimraf": "2.6.1",
"standard-version": "4.4.0",
"stylelint": "9.6.0",
"svg-sprite": "1.5.0",
"source-map-support": "0.5.9",
"ts-loader": "2.3.1",
"ts-node": "7.0.1",
"tslint": "5.9.1",
@@ -94,7 +96,7 @@
"pack": "node ./scripts/release.js",
"sprites": "node ./scripts/generate-svg-sprites",
"release": "node ./scripts/release.js",
"clean": "lerna exec rimraf ./node_modules ./dist && rimraf ./node_modules ./dist",
"clean": "lerna exec rimraf ./node_modules ./dist && rimraf ./node_modules ./dist ./tmp",
"predeploy-gh-pages": "lerna run build:web --scope=uhk-web",
"deploy-gh-pages": "gh-pages -d packages/uhk-web/dist"
},

1
packages/kboot/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './src';

View File

@@ -0,0 +1,8 @@
{
"spec_dir": "test",
"spec_files": [
"**/*[sS]pec.ts"
],
"stopSpecOnExpectationFailure": true,
"random": false
}

524
packages/kboot/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
{
"name": "kboot",
"main": "dist/index.js",
"version": "0.0.0",
"description": "Javascript implementation of the Kinetis Bootloader protocol",
"author": "Ultimate Gadget Laboratories",
"repository": {
"type": "git",
"url": "git@github.com:UltimateHackingKeyboard/agent.git"
},
"license": "GPL-3.0",
"engines": {
"node": ">=8.12.0 <9.0.0",
"npm": ">=6.4.1 <7.0.0"
},
"dependencies": {
"debug": "^4.1.1",
"byte-data": "^16.0.3",
"tslib": "^1.9.3",
"node-hid": ">= 0.7.3"
},
"peer-dependencies": {},
"scripts": {
"build": "tsc --project ./src/tsconfig.json",
"lint": "tslint --project tsconfig.json"
}
}

65
packages/kboot/readme.md Normal file
View File

@@ -0,0 +1,65 @@
Javascript implementation of the Kinetis Bootloader protocol
============================================================
Based on the [Kinetis Bootloader v2.0.0 Reference Manual](https://github.com/UltimateHackingKeyboard/bootloader/blob/master/doc/Kinetis%20Bootloader%20v2.0.0%20Reference%20Manual.pdf)
## Supported communication channels/protocols
- [x] USB
- [ ] I2C
- [ ] SPI
- [ ] CAN
- [ ] UART
## Supported Commands
We implemented only the commands that is used in UHK software.
If someone needs other commands, (s)he can easily implement it based on existing.
- [x] GetProperty
- [ ] SetProperty
- [ ] FlashEraseAll
- [x] FlashEraseRegion
- [x] FlashEraseAllUnsecure
- [x] ReadMemory
- [x] WriteMemory
- [ ] FillMemory
- [x] FlashSecurityDisable
- [ ] Execute
- [ ] Call
- [x] Reset
- [ ] FlashProgramOnce
- [ ] FlashReadOnce
- [ ] FlashReadResource
- [ ] ConfigureQuadSpi
- [ ] ReliableUpdate
- [x] ConfigureI2c
- [ ] ConfigureSpi
- [ ] ConfigureCan
## How to use
```Typescript
// Initialize peripheral
const usbPeripheral = new UsbPeripheral({ productId: 1, vendorId: 1 });
// Initialize Kboot
const kboot = new KBoot(usbPeripheral);
// Call the command
const version = await kboot.getBootloaderVersion();
// ... more commands
// Close the communication channel. Release resources
kboot.close();
```
If you have to communicate other I2C device over USB call `kboot.configureI2c(i2cId)` before the command.
```Typescript
const usbPeripheral = new UsbPeripheral({ productId: 1, vendorId: 1 });
const kboot = new KBoot(usbPeripheral);
// Get the bootloader version of I2C device
await kboot.configureI2c(i2cId);
const version = await kboot.getBootloaderVersion();
```
## TODO
- [ ] Improve exception handling

View File

@@ -0,0 +1,2 @@
export const DEFAULT_USB_PID = 0x0073;
export const DEFAULT_USB_VID = 0x15A2;

View File

@@ -0,0 +1,23 @@
export enum Commands {
FlashEraseAll = 0x01,
FlashEraseRegion = 0x02,
ReadMemory = 0x03,
WriteMemory = 0x04,
FillMemory = 0x05,
FlashSecurityDisable = 0x06,
GetProperty = 0x07,
ReceiveSBFile = 0x08,
Execute = 0x09,
Call = 0x0A,
Reset = 0x0B,
SetProperty = 0x0C,
FlashEraseAllUnsecure = 0x0D,
FlashProgramOnce = 0x0E,
FlashReadOnce = 0x0F,
FlashReadResource = 0x10,
ConfigureQuadSpi = 0x11,
ReliableUpdate = 0x12,
ConfigureI2c = 0xc1,
ConfigureSpi = 0xc2,
ConfigureCan = 0xc3
}

View File

@@ -0,0 +1,5 @@
export * from './commands';
export * from './memory-ids';
export * from './properties';
export * from './response-codes';
export * from './response-tags';

View File

@@ -0,0 +1,5 @@
export enum MemoryIds {
Internal = 0,
Spi0 = 1,
ExecuteOnly = 0x10
}

View File

@@ -0,0 +1,26 @@
export enum Properties {
BootloaderVersion = 0x01,
AvailablePeripherals = 0x02,
FlashStartAddress = 0x03,
FlashSize = 0x04,
FlashSectorSize = 0x05,
FlashBlockCount = 0x06,
AvailableCommands = 0x07,
CrcCheckStatus = 0x08,
VerifyWrites = 0x0A,
MaxPacketSize = 0x0B,
ReservedRegions = 0x0C,
ValidateRegions = 0x0D,
RAMStartAddress = 0x0E,
RAMSize = 0x0F,
SystemDeviceIdent = 0x10,
FlashSecurityState = 0x11,
UniqueDeviceIdent = 0x12,
FlashFacSupport = 0x13,
FlashAccessSegmentSize = 0x14,
FlashAccessSegmentCount = 0x15,
FlashReadMargin = 0x16,
QspiInitStatus = 0x17,
TargetVersion = 0x18,
ExternalMemoryAttributes = 0x19
}

View File

@@ -0,0 +1,76 @@
export enum ResponseCodes {
// Generic status codes.
Success = 0,
Fail = 1,
ReadOnly = 2,
OutOfRange = 3,
InvalidArgument = 4,
// Flash driver errors.
FlashSizeError = 100,
FlashAlignmentError = 101,
FlashAddressError = 102,
FlashAccessError = 103,
FlashProtectionViolation = 104,
FlashCommandFailure = 105,
FlashUnknownProperty = 106,
// I2C driver errors.
I2C_SlaveTxUnderrun = 200,
I2C_SlaveRxOverrun = 201,
I2C_AribtrationLost = 202,
// SPI driver errors.
SPI_SlaveTxUnderrun = 300,
SPI_SlaveRxOverrun = 301,
// QuadSPI driver errors
QSPI_FlashSizeError = 400,
QSPI_FlashAlignmentError = 401,
QSPI_FlashAddressError = 402,
QSPI_FlashCommandFailure = 403,
QSPI_FlashUnknownProperty = 404,
QSPI_NotConfigured = 405,
QSPI_CommandNotSupported = 406,
// Bootloader errors.
UnknownCommand = 10000,
SecurityViolation = 10001,
AbortDataPhase = 10002,
PingError = 10003,
NoResponse = 10004,
NoResponseExpected = 10005,
// SB loader errors.
RomLdrSectionOverrun = 10100,
RomLdrSignature = 10101,
RomLdrSectionLength = 10102,
RomLdrUnencryptedOnly = 10103,
RomLdrEOFReached = 10104,
RomLdrChecksum = 10105,
RomLdrCrc32Error = 10106,
RomLdrUnknownCommand = 10107,
RomLdrIdNotFound = 10108,
RomLdrDataUnderrun = 10109,
RomLdrJumpReturned = 10110,
RomLdrCallFailed = 10111,
RomLdrKeyNotFound = 10112,
RomLdrSecureOnly = 10113,
// Memory interface errors.
MemoryRangeInvalid = 10200,
MemoryReadFailed = 10201,
MemoryWriteFailed = 10202,
// Property store errors.
UnknownProperty = 10300,
ReadOnlyProperty = 10301,
InvalidPropertyValue = 10302,
// Property store errors.
AppCrcCheckPassed = 10400,
AppCrcCheckFailed = 10401,
AppCrcCheckInactive = 10402,
AppCrcCheckInvalid = 10403,
AppCrcCheckOutOfRange = 10404
}

View File

@@ -0,0 +1,7 @@
export enum ResponseTags {
Generic = 0xA0,
ReadMemory = 0xA3,
Property = 0xA7,
FlashReadOnce = 0xAF,
FlashReadResource = 0xB0
}

View File

@@ -0,0 +1,6 @@
export * from './kboot';
export * from './enums';
export * from './models';
export * from './peripheral';
export * from './usb-peripheral';
export * from './util';

194
packages/kboot/src/kboot.ts Normal file
View File

@@ -0,0 +1,194 @@
import { debug } from 'debug';
import { pack } from 'byte-data';
import { Peripheral } from './peripheral';
import { Commands, MemoryIds, Properties, ResponseCodes, ResponseTags } from './enums';
import { BootloaderVersion, CommandOption, CommandResponse, DataOption } from './models';
const logger = debug('kboot');
export class KBoot {
constructor(private peripheral: Peripheral) {
}
open(): void {
this.peripheral.open();
}
close(): void {
this.peripheral.close();
}
// ================= Read properties ==================
async getProperty(property: Properties, memoryId = MemoryIds.Internal): Promise<CommandResponse> {
const command: CommandOption = {
command: Commands.GetProperty,
params: [
...pack(property, { bits: 32 }),
...pack(memoryId, { bits: 32 })
]
};
const response = await this.peripheral.sendCommand(command);
if (response.tag !== ResponseTags.Property) {
throw new Error('Response tag is not property response');
}
if (response.code === ResponseCodes.UnknownProperty) {
throw new Error('Unknown property!');
}
if (response.code !== ResponseCodes.Success) {
throw new Error(`Unknown error. Error code:${response.code}`);
}
return response;
}
async getBootloaderVersion(): Promise<BootloaderVersion> {
const response = await this.getProperty(Properties.BootloaderVersion);
const version: BootloaderVersion = {
bugfix: response.raw[12],
minor: response.raw[13],
major: response.raw[14],
protocolName: String.fromCharCode(response.raw[15])
};
logger('bootloader version %o');
return version;
}
// TODO: Implement other get/set property wrappers
// ================= End read properties ==================
async flashSecurityDisable(key: number[]): Promise<void> {
if (key.length !== 8) {
throw new Error('Flash security key must be 8 byte');
}
const command: CommandOption = {
command: Commands.FlashSecurityDisable,
params: [...key]
};
const response = await this.peripheral.sendCommand(command);
if (response.tag !== ResponseTags.Generic) {
throw new Error('Response tag is not generic response');
}
if (response.code !== ResponseCodes.Success) {
throw new Error(`Can not disable flash security`);
}
}
async flashEraseRegion(startAddress: number, count: number): Promise<void> {
const command: CommandOption = {
command: Commands.FlashEraseRegion,
params: [
...pack(startAddress, { bits: 32 }),
...pack(count, { bits: 32 })
]
};
const response = await this.peripheral.sendCommand(command);
if (response.tag !== ResponseTags.Generic) {
throw new Error('Response tag is not generic response');
}
if (response.code !== ResponseCodes.Success) {
throw new Error(`Can not disable flash security`);
}
}
async flashEraseAllUnsecure(): Promise<void> {
const command: CommandOption = {
command: Commands.FlashEraseAllUnsecure,
params: []
};
const response = await this.peripheral.sendCommand(command);
if (response.tag !== ResponseTags.Generic) {
throw new Error('Response tag is not generic response');
}
if (response.code !== ResponseCodes.Success) {
throw new Error(`Can not disable flash security`);
}
}
async readMemory(startAddress: number, count: number): Promise<any> {
return this.peripheral.readMemory(startAddress, count);
}
async writeMemory(options: DataOption): Promise<void> {
return this.peripheral.writeMemory(options);
}
/**
* Reset the bootloader
*/
async reset(): Promise<void> {
const command: CommandOption = {
command: Commands.Reset,
params: []
};
let response: CommandResponse;
try {
response = await this.peripheral.sendCommand(command);
} catch (error) {
if (error.message === 'could not read from HID device') {
logger('Ignoring missing response from reset command.');
this.close();
return;
}
throw error;
}
if (response.tag !== ResponseTags.Generic) {
throw new Error('Response tag is not generic response');
}
if (response.code !== ResponseCodes.Success) {
throw new Error(`Unknown error. Error code:${response.code}`);
}
}
/**
* Call it before send data to I2C
* @param address - The address of the I2C
* @param [speed=64] - Speed of the I2C
*/
async configureI2c(address: number, speed = 64): Promise<void> {
if (address > 127) {
throw new Error('Only 7-bit i2c address is supported');
}
const command: CommandOption = {
command: Commands.ConfigureI2c,
params: [
...pack(address, { bits: 32 }),
...pack(speed, { bits: 32 })
]
};
const response = await this.peripheral.sendCommand(command);
if (response.tag !== ResponseTags.Generic) {
throw new Error('Response tag is not generic response');
}
if (response.code !== ResponseCodes.Success) {
throw new Error(`Unknown error. Error code:${response.code}`);
}
}
}

View File

@@ -0,0 +1,6 @@
export interface BootloaderVersion {
major: number;
minor: number;
bugfix: number;
protocolName: string;
}

View File

@@ -0,0 +1,7 @@
import { Commands } from '../enums';
export interface CommandOption {
command: Commands;
hasDataPhase?: boolean;
params?: number[];
}

View File

@@ -0,0 +1,7 @@
import { ResponseCodes, ResponseTags } from '../enums';
export interface CommandResponse {
tag: ResponseTags;
code: ResponseCodes;
raw: Buffer;
}

View File

@@ -0,0 +1,4 @@
export interface DataOption {
startAddress: number;
data: Buffer;
}

View File

@@ -0,0 +1,5 @@
export * from './bootloader-version';
export * from './command-option';
export * from './command-response';
export * from './data-option';
export * from './usb';

View File

@@ -0,0 +1,7 @@
export interface USB {
vendorId: number;
productId: number;
interface?: number;
usage?: number;
usePage?: number;
}

View File

@@ -0,0 +1,13 @@
import { CommandOption, CommandResponse, DataOption } from './models';
export interface Peripheral {
open(): void;
close(): void;
sendCommand(options: CommandOption): Promise<CommandResponse>;
writeMemory(data: DataOption): Promise<void>;
readMemory(startAddress: number, count: number): Promise<Buffer>;
}

View File

@@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "../dist"
}
}

View File

@@ -0,0 +1,265 @@
import { debug } from 'debug';
import { devices, HID } from 'node-hid';
import { pack } from 'byte-data';
import { Peripheral } from './peripheral';
import { CommandOption, CommandResponse, DataOption, USB } from './models';
import { convertLittleEndianNumber, convertToHexString, deviceFinder, encodeCommandOption } from './util';
import { decodeCommandResponse } from './util/usb/decode-command-response';
import { validateCommandParams } from './util/usb/encode-command-option';
import { Commands, ResponseTags } from './enums';
import { snooze } from './util/snooze';
const logger = debug('kboot:usb');
const WRITE_DATA_STREAM_PACKAGE_LENGTH = 32;
export class UsbPeripheral implements Peripheral {
private _device: HID;
private _responseBuffer: Buffer;
private _dataBuffer: Buffer;
private _hidError: any;
constructor(private options: USB) {
logger('constructor options: %o', options);
}
open(): void {
this._hidError = undefined;
if (this._device) {
return;
}
logger('Available devices');
const device = devices()
.map(x => {
logger('%O', x);
return x;
})
.find(deviceFinder(this.options));
if (!device) {
throw new Error('USB device can not be found');
}
this._responseBuffer = new Buffer(0);
this._dataBuffer = new Buffer(0);
this._device = new HID(device.path);
this._device.on('data', this._usbDataListener.bind(this));
this._device.on('error', this._usbErrorListener.bind(this));
}
close(): void {
if (this._device) {
this._device.close();
this._device = undefined;
}
}
async sendCommand(options: CommandOption): Promise<CommandResponse> {
validateCommandParams(options.params);
const data = encodeCommandOption(options);
this._send(data);
return this._getNextCommandResponse();
}
writeMemory(option: DataOption): Promise<void> {
return new Promise<void>(async (resolve, reject) => {
try {
const command: CommandOption = {
command: Commands.WriteMemory,
hasDataPhase: true,
params: [
...pack(option.startAddress, { bits: 32 }),
...pack(option.data.length, { bits: 32 })
]
};
const firsCommandResponse = await this.sendCommand(command);
if (firsCommandResponse.tag !== ResponseTags.Generic) {
return reject(new Error('Invalid write memory response!'));
}
if (firsCommandResponse.code !== 0) {
return reject(new Error(`Non zero write memory response! Response code: ${firsCommandResponse.code}`));
}
for (let i = 0; i < option.data.length; i = i + WRITE_DATA_STREAM_PACKAGE_LENGTH) {
if (this._hidError) {
logger('Throw USB error %O', this._hidError);
return reject(new Error('USB error while write data'));
}
const slice = option.data.slice(i, i + WRITE_DATA_STREAM_PACKAGE_LENGTH);
const writeData = [
2, // USB channel
0,
slice.length,
0, // TODO: What is it?
...slice
];
logger('send data %o', convertToHexString(writeData));
this._device.write(writeData);
}
const secondCommandResponse = await this._getNextCommandResponse();
if (secondCommandResponse.tag !== ResponseTags.Generic) {
return reject(new Error('Invalid write memory final response!'));
}
if (secondCommandResponse.code !== 0) {
const msg = `Non zero write memory final response! Response code: ${secondCommandResponse.code}`;
return reject(new Error(msg));
}
resolve();
} catch (err) {
logger('Can not write memory data %O', err);
reject(err);
}
});
}
readMemory(startAddress: number, count: number): Promise<Buffer> {
return new Promise<Buffer>(async (resolve, reject) => {
try {
const command: CommandOption = {
command: Commands.ReadMemory,
params: [
...pack(startAddress, { bits: 32 }),
...pack(count, { bits: 32 })
]
};
this._resetDataBuffer();
this._resetResponseBuffer();
const firsCommandResponse = await this.sendCommand(command);
if (firsCommandResponse.tag !== ResponseTags.ReadMemory) {
return reject(new Error('Invalid read memory response!'));
}
if (firsCommandResponse.code !== 0) {
return reject(new Error(`Non zero read memory response! Response code: ${firsCommandResponse.code}`));
}
const byte4Number = firsCommandResponse.raw.slice(12, 15);
const arrivingData = convertLittleEndianNumber(byte4Number);
const memoryDataBuffer = await this._readFromDataStream(arrivingData);
const secondCommandResponse = await this._getNextCommandResponse();
if (secondCommandResponse.tag !== ResponseTags.Generic) {
return reject(new Error('Invalid read memory final response!'));
}
if (secondCommandResponse.code !== 0) {
const msg = `Non zero read memory final response! Response code: ${secondCommandResponse.code}`;
return reject(new Error(msg));
}
resolve(memoryDataBuffer);
} catch (error) {
logger('Read memory error %O', error);
reject(error);
}
});
}
private _send(data: number[]): void {
this.open();
logger('send data %o', `<${convertToHexString(data)}>`);
this._device.write(data);
}
private _usbDataListener(data: Buffer): void {
logger('received data %o', `[${convertToHexString(data)}]`);
const channel = data[0];
switch (channel) {
case 3:
this._responseBuffer = Buffer.concat([this._responseBuffer, data]);
break;
case 4:
this._dataBuffer = Buffer.concat([this._dataBuffer, data]);
break;
default:
logger('Unknown USB channel %o', channel);
break;
}
}
private _usbErrorListener(error: any): void {
logger('USB stream error %O', error);
this._hidError = error;
}
private _readFromCommandStream(byte = 36, timeout = 15000): Promise<Buffer> {
return this._readFromBuffer('_responseBuffer', byte, timeout);
}
private _readFromDataStream(byte = 36, timeout = 15000): Promise<Buffer> {
return this._readFromBuffer('_dataBuffer', byte, timeout);
}
private _readFromBuffer(bufferName: string, byte: number, timeout: number): Promise<Buffer> {
return new Promise<Buffer>(async (resolve, reject) => {
const startTime = new Date();
while (startTime.getTime() + timeout > new Date().getTime()) {
if (this._hidError) {
const err = this._hidError;
return reject(err);
}
const buffer: Buffer = this[bufferName];
if (buffer.length >= byte) {
const data = buffer.slice(0, byte);
if (buffer.length === byte) {
this[bufferName] = new Buffer(0);
} else {
const newDataBuffer = new Buffer(buffer.length - byte);
buffer.copy(newDataBuffer, 0, byte);
this[bufferName] = newDataBuffer;
}
logger(`read from ${bufferName}: %O`, convertToHexString(data));
return resolve(data);
}
await snooze(100);
}
reject(new Error('Timeout while try to read from buffer'));
});
}
private _resetDataBuffer(): void {
this._dataBuffer = new Buffer(0);
}
private _resetResponseBuffer(): void {
this._responseBuffer = new Buffer(0);
}
private async _getNextCommandResponse(): Promise<CommandResponse> {
const response = await this._readFromCommandStream();
const commandResponse = decodeCommandResponse(response);
logger('next command response: %o', commandResponse);
return commandResponse;
}
}

View File

@@ -0,0 +1,6 @@
export const encodeStringToParams = (data: string): Int8Array => {
// const buffer = Buffer.from(data);
// return new Int8Array(buffer, 0);
return new Int8Array(0);
};

View File

@@ -0,0 +1,22 @@
export * from './encode-string-to-parameters';
export * from './response-parser';
export * from './usb';
export const convertToHexString = (arr: number[] | Buffer): string => {
let str = '';
for (const n of arr) {
let hex = n.toString(16);
if (hex.length < 2) {
hex = '0' + hex;
}
if (str.length > 0) {
str += ' ';
}
str += hex;
}
return str;
};

View File

@@ -0,0 +1,21 @@
import { ResponseCodes, ResponseTags } from '../enums';
export const convertLittleEndianNumber = (data: Buffer): number => {
let value = 0;
for (let i = 0; i < data.length; i++) {
value += data[i] << (8 * i);
}
return value;
};
export const getResponseCode = (response: Buffer): ResponseCodes => {
const data = response.slice(8, 11);
return convertLittleEndianNumber(data);
};
export const getResponseTag = (response: Buffer): ResponseTags => {
return response[4];
};

View File

@@ -0,0 +1 @@
export const snooze = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

View File

@@ -0,0 +1,18 @@
import { CommandResponse } from '../../models';
import { getResponseCode, getResponseTag } from '../response-parser';
export const decodeCommandResponse = (response: Buffer): CommandResponse => {
if (response.length < 8) {
throw new Error('Invalid response length!');
}
if (response[0] !== 3) {
throw new Error(`Invalid response command channel!`);
}
return {
code: getResponseCode(response),
tag: getResponseTag(response),
raw: response
};
};

View File

@@ -0,0 +1,22 @@
import { isNullOrUndefined } from 'util';
import { Device } from 'node-hid';
import { USB } from '../../models';
export const deviceFinder = (usb: USB) => {
return (device: Device): boolean => {
if (device.productId !== usb.productId) {
return false;
}
if (device.vendorId !== usb.vendorId) {
return false;
}
// TODO: Add interface, usage and usePage filtering
// if (!isNullOrUndefined(usb.interface) && device.interface !== -1 && device.interface === usb.interface) {
// return true;
// }
return true;
};
};

View File

@@ -0,0 +1,46 @@
import { isNullOrUndefined } from 'util';
import { pack } from 'byte-data';
import { CommandOption } from '../../models';
/**
* Encode the USB Command.
* @param option
*/
export const encodeCommandOption = (option: CommandOption): number[] => {
const payload = [
option.command,
option.hasDataPhase ? 1 : 0,
0, // Reserved. Should be 0
option.params ? option.params.length / 4 >> 0 : 0 // number of parameters
];
if (option.params) {
payload.push(...option.params);
}
const header = [
1, // Communication channel
0, // TODO: What is it?
...pack(payload.length, { bits: 16 }) // payload length in 2 byte
];
const placeholders = new Array(32 - payload.length)
.fill(0);
return [...header, ...payload, ...placeholders];
};
export const validateCommandParams = (params: any[]): void => {
if (isNullOrUndefined(params)) {
return;
}
if (!Array.isArray(params)) {
throw new Error('Command parameters must be an array!');
}
if (params.length > 28) {
throw new Error('Maximum 7 (28 bytes) command parameters allowed!');
}
};

View File

@@ -0,0 +1,2 @@
export { deviceFinder } from './device-finder';
export { encodeCommandOption } from './encode-command-option';

View File

@@ -0,0 +1,36 @@
import { BootloaderVersion, CommandResponse, Commands, KBoot, Peripheral, Properties, ResponseCodes, ResponseTags } from '../src';
import { TestPeripheral } from './test-peripheral';
describe('kboot', () => {
let kboot: KBoot;
let testPeripheral: Peripheral;
beforeEach(() => {
testPeripheral = new TestPeripheral();
kboot = new KBoot(testPeripheral);
});
describe('getBootloaderVersion', () => {
it('should works', async () => {
const sendCommandResponse: CommandResponse = {
code: ResponseCodes.Success,
tag: ResponseTags.Property,
// tslint:disable-next-line:max-line-length
raw: Buffer.from([0x03, 0x00, 0x0c, 0x00, 0xa7, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x4b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
};
spyOn(testPeripheral, 'sendCommand').and.returnValue(Promise.resolve(sendCommandResponse));
const version = await kboot.getBootloaderVersion();
const expectedVersion: BootloaderVersion = {
protocolName: 'K',
major: 2,
minor: 0,
bugfix: 0
};
expect(version).toEqual(expectedVersion);
expect(testPeripheral.sendCommand).toHaveBeenCalledWith({
command: Commands.GetProperty,
params: [1, 0, 0, 0, 0, 0, 0, 0]
});
});
});
});

View File

@@ -0,0 +1,27 @@
import { CommandOption, CommandResponse, DataOption, Peripheral, ResponseCodes, ResponseTags } from '../src';
export class TestPeripheral implements Peripheral {
close(): void {
}
open(): void {
}
sendCommand(options: CommandOption): Promise<CommandResponse> {
const response = {
tag: ResponseTags.Generic,
code: ResponseCodes.Success,
raw: new Buffer(0)
};
return Promise.resolve(response);
}
writeMemory(data: DataOption): Promise<void> {
return Promise.resolve();
}
readMemory(startAddress: number, count: number): Promise<Buffer> {
return Promise.resolve(new Buffer(0));
}
}

View File

@@ -0,0 +1,33 @@
import { execSync } from 'child_process';
import { join } from 'path';
import { readFileSync } from 'fs';
import * as MemoryMap from 'nrf-intel-hex';
export enum UhkReenumerationModes {
Bootloader = 'bootloader',
Buspal = 'buspal',
NormalKeyboard = 'normalKeyboard',
CompatibleKeyboard = 'compatibleKeyboard'
}
const USB_SCRIPTS_DIR = join(__dirname, '../../../usb');
export const reenumerate = (mode: UhkReenumerationModes): void => {
const reenumerateScriptFile = join(USB_SCRIPTS_DIR, 'reenumerate.js');
const command = [reenumerateScriptFile, mode.toString()].join(' ');
execSync(
command,
{
cwd: USB_SCRIPTS_DIR,
stdio: [0, 1, 2]
}
);
};
export const readBootloaderFirmwareFromHexFile = (): Map<any, any> => {
const hexFilePath = join(__dirname, '../../../../tmp/packages/firmware/devices/uhk60-right/firmware.hex');
const fileContent = readFileSync(hexFilePath, { encoding: 'utf8' });
const memoryMap = MemoryMap.fromHex(fileContent);
return memoryMap;
};

View File

@@ -0,0 +1,10 @@
import { encodeStringToParams } from '../../src/util';
describe('encodeStringToParams', () => {
xit('should convert 8 character to little endian 4 byte array', () => {
const result = encodeStringToParams('0403020108070605');
const expectedResult = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
expect(result).toEqual(expectedResult);
});
});

View File

@@ -0,0 +1,17 @@
import { getResponseCode, ResponseCodes } from '../../src';
describe('response-parser', () => {
describe('getResponseCode', () => {
it('should return with success', () => {
const buffer = Buffer.from([0x03, 0x00, 0x08, 0x00, 0xa7, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]);
const responseCode = getResponseCode(buffer);
expect(responseCode).toEqual(ResponseCodes.Success);
});
it('should return with UnknownProperty', () => {
const buffer = Buffer.from([0x03, 0x00, 0x08, 0x00, 0xa7, 0x00, 0x00, 0x01, 0x3c, 0x28, 0x00, 0x00]);
const responseCode = getResponseCode(buffer);
expect(responseCode).toEqual(ResponseCodes.UnknownProperty);
});
});
});

View File

@@ -0,0 +1,16 @@
import { CommandOption, Commands } from '../../../src';
import { encodeCommandOption } from '../../../src/util';
describe('usb encodeCommandOption', () => {
it('should convert correctly', () => {
const option: CommandOption = {
command: Commands.GetProperty,
params: [1, 0, 0, 0, 0, 0, 0, 0]
};
const result = encodeCommandOption(option);
// tslint:disable-next-line:max-line-length
const expected = [1, 0, 0x0c, 0, 0x07, 0x00, 0x00, 0x02, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
expect(result).toEqual(expected);
});
});

View File

@@ -0,0 +1,81 @@
import { reenumerate, UhkReenumerationModes, readBootloaderFirmwareFromHexFile } from './uhk-helpers';
import { BootloaderVersion, UsbPeripheral } from '../src';
import { KBoot } from '../src/kboot';
import { DataOption } from '../index';
xdescribe('UHK Integration tests', () => {
describe('bootloader', () => {
let usb: UsbPeripheral;
let kboot: KBoot;
beforeEach(() => {
reenumerate(UhkReenumerationModes.Bootloader);
usb = new UsbPeripheral({ vendorId: 0x1d50, productId: 0x6120 });
kboot = new KBoot(usb);
});
afterEach(() => {
if (usb) {
usb.close();
}
if (kboot) {
kboot.close();
}
});
it('get bootloader version', async () => {
const expectedVersion: BootloaderVersion = {
protocolName: 'K',
major: 2,
minor: 0,
bugfix: 0
};
const version = await kboot.getBootloaderVersion();
expect(version).toEqual(expectedVersion);
});
it('disable flash security', () => {
const backdoorKey = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
return kboot
.flashSecurityDisable(backdoorKey)
.catch(err => {
expect(err).toBeFalsy();
});
});
it('flash erase region', () => {
return kboot
.flashEraseRegion(0xc000, 475136)
.catch(err => {
expect(err).toBeFalsy();
});
});
it('read memory', () => {
const dataLength = 128;
return kboot
.readMemory(0xc000, dataLength)
.then((data: number[]) => {
expect(data).toBeTruthy();
expect(data.length).toEqual(dataLength);
})
.catch(err => {
expect(err).toBeFalsy();
});
});
it('write memory', async () => {
const bootloaderMemoryMap = readBootloaderFirmwareFromHexFile();
for (const [startAddress, data] of bootloaderMemoryMap.entries()) {
const dataOption: DataOption = {
startAddress,
data
};
await kboot.writeMemory(dataOption);
}
});
});
});

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -16,7 +16,6 @@ import { logger } from './services/logger.service';
import { AppUpdateService } from './services/app-update.service';
import { AppService } from './services/app.service';
import { SudoService } from './services/sudo.service';
import { UhkBlhost } from '../../uhk-usb/src';
import * as isDev from 'electron-is-dev';
import { setMenu } from './electron-menu';
@@ -36,7 +35,6 @@ let win: Electron.BrowserWindow;
autoUpdater.logger = logger;
let deviceService: DeviceService;
let uhkBlhost: UhkBlhost;
let uhkHidDeviceService: UhkHidDevice;
let uhkOperations: UhkOperations;
let appUpdateService: AppUpdateService;
@@ -103,8 +101,7 @@ function createWindow() {
setMenu(win);
win.maximize();
uhkHidDeviceService = new UhkHidDevice(logger, options, packagesDir);
uhkBlhost = new UhkBlhost(logger, packagesDir);
uhkOperations = new UhkOperations(logger, uhkBlhost, uhkHidDeviceService, packagesDir);
uhkOperations = new UhkOperations(logger, uhkHidDeviceService, packagesDir);
deviceService = new DeviceService(logger, win, uhkHidDeviceService, uhkOperations, packagesDir);
appUpdateService = new AppUpdateService(logger, win, app);
appService = new AppService(logger, win, deviceService, options, uhkHidDeviceService);

View File

@@ -150,6 +150,11 @@ export class DeviceService {
}
this.logService.error('[DeviceService] Read hardware modules information failed', err);
return {
leftModuleInfo: {},
rightModuleInfo: {}
};
}
}
@@ -170,8 +175,8 @@ export class DeviceService {
this.logService.debug('Device right firmware version:', hardwareModules.rightModuleInfo.firmwareVersion);
this.logService.debug('Device left firmware version:', hardwareModules.leftModuleInfo.firmwareVersion);
this.device.resetDeviceCache();
this.stopPollTimer();
this.device.resetDeviceCache();
if (data.firmware) {
firmwarePathData = await saveTmpFirmware(data.firmware);

View File

@@ -235,6 +235,11 @@
"set-blocking": "~2.0.0"
}
},
"nrf-intel-hex": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/nrf-intel-hex/-/nrf-intel-hex-1.3.0.tgz",
"integrity": "sha512-oXwBJxX/0Jc4fe2Jxjv3Mw9/qw9JdToDLvJuozfVx+twpkc2oSUm8W/OODX6W4kmWOaYA11ORpGLfQ8BP7mndw=="
},
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",

View File

@@ -12,7 +12,9 @@
"@types/node": "8.0.28"
},
"dependencies": {
"kboot": "0.0.0",
"node-hid": "0.7.3",
"nrf-intel-hex": "1.3.0",
"tslib": "1.9.3",
"uhk-common": "1.0.0"
}

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

View File

@@ -8,13 +8,6 @@ const copyOptions = {
const promises = [];
promises.push(
fse.copy(
path.join(__dirname, '../packages/usb/blhost'),
path.join(__dirname, '../tmp/packages/blhost'),
copyOptions)
);
promises.push(
fse.copy(
path.join(__dirname, '../rules'),

View File

@@ -95,7 +95,7 @@ if (TEST_BUILD || gitTag) {
const rootJson = require('../package.json');
update2ndPackageJson(rootJson);
// Add firmware and blhost to extra resources
// Add firmware to extra resources
const extractedFirmwareDir = path.join(__dirname, '../tmp/packages');
extraResources.push({from: extractedFirmwareDir, to: 'packages/'});