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

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