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
121
package-lock.json
generated
121
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
1
packages/kboot/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './src';
|
||||
8
packages/kboot/jasmine.json
Normal file
8
packages/kboot/jasmine.json
Normal 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
524
packages/kboot/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
packages/kboot/package.json
Normal file
27
packages/kboot/package.json
Normal 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
65
packages/kboot/readme.md
Normal 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
|
||||
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';
|
||||
36
packages/kboot/test/kboot.spec.ts
Normal file
36
packages/kboot/test/kboot.spec.ts
Normal 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]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
27
packages/kboot/test/test-peripheral.ts
Normal file
27
packages/kboot/test/test-peripheral.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
33
packages/kboot/test/uhk-helpers/index.ts
Normal file
33
packages/kboot/test/uhk-helpers/index.ts
Normal 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;
|
||||
};
|
||||
10
packages/kboot/test/util/encode-string-to-parameters.spec.ts
Normal file
10
packages/kboot/test/util/encode-string-to-parameters.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
17
packages/kboot/test/util/response-parser.spec.ts
Normal file
17
packages/kboot/test/util/response-parser.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
16
packages/kboot/test/util/usb/encode-command-option.spec.ts
Normal file
16
packages/kboot/test/util/usb/encode-command-option.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
81
packages/kboot/test/zzz-uhk-integration-tests.spec.ts
Normal file
81
packages/kboot/test/zzz-uhk-integration-tests.spec.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
3
packages/kboot/tsconfig.json
Normal file
3
packages/kboot/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
5
packages/uhk-usb/package-lock.json
generated
5
packages/uhk-usb/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './constants';
|
||||
export * from './uhk-blhost';
|
||||
export * from './uhk-hid-device';
|
||||
export * from './uhk-operations';
|
||||
export * from './util';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
};
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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/'});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user