feat: kboot package (#894)
* feat: kboot package * feat: kboot package * fix: wait 1 sec after device is available * test: fix unit test * refactor: clean unused codes * doc: improve readme.md * doc: improve readme.md * test: fix unit test * chore: fix lint settings * style: fix linting issues
This commit is contained in:
committed by
László Monda
parent
bfc08edfce
commit
3964698cf7
2
packages/kboot/src/constants.ts
Normal file
2
packages/kboot/src/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const DEFAULT_USB_PID = 0x0073;
|
||||
export const DEFAULT_USB_VID = 0x15A2;
|
||||
23
packages/kboot/src/enums/commands.ts
Normal file
23
packages/kboot/src/enums/commands.ts
Normal 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
|
||||
}
|
||||
5
packages/kboot/src/enums/index.ts
Normal file
5
packages/kboot/src/enums/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './commands';
|
||||
export * from './memory-ids';
|
||||
export * from './properties';
|
||||
export * from './response-codes';
|
||||
export * from './response-tags';
|
||||
5
packages/kboot/src/enums/memory-ids.ts
Normal file
5
packages/kboot/src/enums/memory-ids.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum MemoryIds {
|
||||
Internal = 0,
|
||||
Spi0 = 1,
|
||||
ExecuteOnly = 0x10
|
||||
}
|
||||
26
packages/kboot/src/enums/properties.ts
Normal file
26
packages/kboot/src/enums/properties.ts
Normal 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
|
||||
}
|
||||
76
packages/kboot/src/enums/response-codes.ts
Normal file
76
packages/kboot/src/enums/response-codes.ts
Normal 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
|
||||
}
|
||||
7
packages/kboot/src/enums/response-tags.ts
Normal file
7
packages/kboot/src/enums/response-tags.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum ResponseTags {
|
||||
Generic = 0xA0,
|
||||
ReadMemory = 0xA3,
|
||||
Property = 0xA7,
|
||||
FlashReadOnce = 0xAF,
|
||||
FlashReadResource = 0xB0
|
||||
}
|
||||
6
packages/kboot/src/index.ts
Normal file
6
packages/kboot/src/index.ts
Normal 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
194
packages/kboot/src/kboot.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
packages/kboot/src/models/bootloader-version.ts
Normal file
6
packages/kboot/src/models/bootloader-version.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface BootloaderVersion {
|
||||
major: number;
|
||||
minor: number;
|
||||
bugfix: number;
|
||||
protocolName: string;
|
||||
}
|
||||
7
packages/kboot/src/models/command-option.ts
Normal file
7
packages/kboot/src/models/command-option.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Commands } from '../enums';
|
||||
|
||||
export interface CommandOption {
|
||||
command: Commands;
|
||||
hasDataPhase?: boolean;
|
||||
params?: number[];
|
||||
}
|
||||
7
packages/kboot/src/models/command-response.ts
Normal file
7
packages/kboot/src/models/command-response.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { ResponseCodes, ResponseTags } from '../enums';
|
||||
|
||||
export interface CommandResponse {
|
||||
tag: ResponseTags;
|
||||
code: ResponseCodes;
|
||||
raw: Buffer;
|
||||
}
|
||||
4
packages/kboot/src/models/data-option.ts
Normal file
4
packages/kboot/src/models/data-option.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface DataOption {
|
||||
startAddress: number;
|
||||
data: Buffer;
|
||||
}
|
||||
5
packages/kboot/src/models/index.ts
Normal file
5
packages/kboot/src/models/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './bootloader-version';
|
||||
export * from './command-option';
|
||||
export * from './command-response';
|
||||
export * from './data-option';
|
||||
export * from './usb';
|
||||
7
packages/kboot/src/models/usb.ts
Normal file
7
packages/kboot/src/models/usb.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface USB {
|
||||
vendorId: number;
|
||||
productId: number;
|
||||
interface?: number;
|
||||
usage?: number;
|
||||
usePage?: number;
|
||||
}
|
||||
13
packages/kboot/src/peripheral.ts
Normal file
13
packages/kboot/src/peripheral.ts
Normal 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>;
|
||||
}
|
||||
7
packages/kboot/src/tsconfig.json
Normal file
7
packages/kboot/src/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "../dist"
|
||||
}
|
||||
}
|
||||
265
packages/kboot/src/usb-peripheral.ts
Normal file
265
packages/kboot/src/usb-peripheral.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
6
packages/kboot/src/util/encode-string-to-parameters.ts
Normal file
6
packages/kboot/src/util/encode-string-to-parameters.ts
Normal 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);
|
||||
};
|
||||
22
packages/kboot/src/util/index.ts
Normal file
22
packages/kboot/src/util/index.ts
Normal 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;
|
||||
};
|
||||
21
packages/kboot/src/util/response-parser.ts
Normal file
21
packages/kboot/src/util/response-parser.ts
Normal 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];
|
||||
};
|
||||
1
packages/kboot/src/util/snooze.ts
Normal file
1
packages/kboot/src/util/snooze.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const snooze = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
18
packages/kboot/src/util/usb/decode-command-response.ts
Normal file
18
packages/kboot/src/util/usb/decode-command-response.ts
Normal 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
|
||||
};
|
||||
};
|
||||
22
packages/kboot/src/util/usb/device-finder.ts
Normal file
22
packages/kboot/src/util/usb/device-finder.ts
Normal 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;
|
||||
};
|
||||
};
|
||||
46
packages/kboot/src/util/usb/encode-command-option.ts
Normal file
46
packages/kboot/src/util/usb/encode-command-option.ts
Normal 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!');
|
||||
}
|
||||
};
|
||||
2
packages/kboot/src/util/usb/index.ts
Normal file
2
packages/kboot/src/util/usb/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { deviceFinder } from './device-finder';
|
||||
export { encodeCommandOption } from './encode-command-option';
|
||||
Reference in New Issue
Block a user