diff --git a/package-lock.json b/package-lock.json index a9c4d918..f07b2a36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 33393ed7..223c0b48 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/packages/kboot/index.ts b/packages/kboot/index.ts new file mode 100644 index 00000000..8420b109 --- /dev/null +++ b/packages/kboot/index.ts @@ -0,0 +1 @@ +export * from './src'; diff --git a/packages/kboot/jasmine.json b/packages/kboot/jasmine.json new file mode 100644 index 00000000..41ed3fad --- /dev/null +++ b/packages/kboot/jasmine.json @@ -0,0 +1,8 @@ +{ + "spec_dir": "test", + "spec_files": [ + "**/*[sS]pec.ts" + ], + "stopSpecOnExpectationFailure": true, + "random": false +} diff --git a/packages/kboot/package-lock.json b/packages/kboot/package-lock.json new file mode 100644 index 00000000..e4524637 --- /dev/null +++ b/packages/kboot/package-lock.json @@ -0,0 +1,524 @@ +{ + "name": "kboot", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "bindings": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz", + "integrity": "sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==" + }, + "bl": { + "version": "1.2.2", + "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, + "byte-data": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/byte-data/-/byte-data-16.0.3.tgz", + "integrity": "sha512-IzV3mzv8OnnzPdb9CoESQr2ikPX/gkHUesRu+vff9XB7KwMxyflPDewtPFWXPvF+Xukl52ceor2IRLbnQZf3PQ==", + "requires": { + "endianness": "^8.0.2", + "ieee754-buffer": "^0.2.1", + "twos-complement-buffer": "0.0.1", + "uint-buffer": "^0.1.0", + "utf8-buffer": "^0.2.0" + } + }, + "chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, + "endianness": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/endianness/-/endianness-8.0.2.tgz", + "integrity": "sha512-IU+77+jJ7lpw2qZ3NUuqBZFy3GuioNgXUdsL1L9tooDNTaw0TgOnwNuc+8Ns+haDaTifK97QLzmOANJtI/rGvw==" + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "ieee754-buffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ieee754-buffer/-/ieee754-buffer-0.2.1.tgz", + "integrity": "sha512-dDlJhYk8BAmH1HDncTjCt6xOm2+kT+MxGhRKB+mUoF8nocDzPAgZPEWTRI9QgkGvbDkbJgCqyxweGlIV0yhbUQ==" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "nan": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", + "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==" + }, + "napi-build-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.1.tgz", + "integrity": "sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==" + }, + "node-abi": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.5.1.tgz", + "integrity": "sha512-oDbFc7vCFx0RWWCweTer3hFm1u+e60N5FtGnmRV6QqvgATGFH/XRR6vqWIeBVosCYCqt6YdIr2L0exLZuEdVcQ==", + "requires": { + "semver": "^5.4.1" + } + }, + "node-hid": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/node-hid/-/node-hid-0.7.4.tgz", + "integrity": "sha512-gvgNDPoszObn7avIDYMUvVv1T0xQB4/CZFJWckra/LXAc0qHYho4M1LCnCKlLIocL2R5/3qGv0J4AjRMdwgjxg==", + "requires": { + "bindings": "^1.3.0", + "nan": "^2.10.0", + "prebuild-install": "^5.2.1" + } + }, + "noop-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "prebuild-install": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.2.2.tgz", + "integrity": "sha512-4e8VJnP3zJdZv/uP0eNWmr2r9urp4NECw7Mt1OSAi3rcLrbBRxGiAkfUFtre2MhQ5wfREAjRV+K1gubvs/GPsA==", + "requires": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.2.0", + "noop-logger": "^0.1.1", + "npmlog": "^4.0.1", + "os-homedir": "^1.0.1", + "pump": "^2.0.1", + "rc": "^1.2.7", + "simple-get": "^2.7.0", + "tar-fs": "^1.13.0", + "tunnel-agent": "^0.6.0", + "which-pm-runs": "^1.0.0" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" + }, + "simple-get": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz", + "integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==", + "requires": { + "decompress-response": "^3.3.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "tar-fs": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", + "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", + "requires": { + "chownr": "^1.0.1", + "mkdirp": "^0.5.1", + "pump": "^1.0.0", + "tar-stream": "^1.1.2" + }, + "dependencies": { + "pump": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", + "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "twos-complement-buffer": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/twos-complement-buffer/-/twos-complement-buffer-0.0.1.tgz", + "integrity": "sha512-Ev3p2GfB2GO8pcyb7jIvctS9RAjSZrF/K+u5s3KN00ajY11Dda2oMqI72nXaHVU7doGYNXc0mJG6exWAbmzZiA==", + "requires": { + "uint-buffer": "^0.1.0" + } + }, + "uint-buffer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/uint-buffer/-/uint-buffer-0.1.0.tgz", + "integrity": "sha512-7xjpjCTijFIXAMxN7OMRfykpCMVfbCrlAmAt2RIlihvkHgvkNV5DBFzyc8OpIQeVpRXJkgXBwmKos4hD8DrX1g==" + }, + "utf8-buffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/utf8-buffer/-/utf8-buffer-0.2.0.tgz", + "integrity": "sha512-DygDeOmOPQRjxnnv8LdfjoSQgG9EgJFH1m/1QcrKkDOxzoOcLLqZ2ONzRYHmiRqJYQYnAvV+zv2Wgk5tXjr4aA==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + } + } +} diff --git a/packages/kboot/package.json b/packages/kboot/package.json new file mode 100644 index 00000000..ecdbf866 --- /dev/null +++ b/packages/kboot/package.json @@ -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" + } +} diff --git a/packages/kboot/readme.md b/packages/kboot/readme.md new file mode 100644 index 00000000..ca1bd95b --- /dev/null +++ b/packages/kboot/readme.md @@ -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 diff --git a/packages/kboot/src/constants.ts b/packages/kboot/src/constants.ts new file mode 100644 index 00000000..ef2b72bd --- /dev/null +++ b/packages/kboot/src/constants.ts @@ -0,0 +1,2 @@ +export const DEFAULT_USB_PID = 0x0073; +export const DEFAULT_USB_VID = 0x15A2; diff --git a/packages/kboot/src/enums/commands.ts b/packages/kboot/src/enums/commands.ts new file mode 100644 index 00000000..52287ccf --- /dev/null +++ b/packages/kboot/src/enums/commands.ts @@ -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 +} diff --git a/packages/kboot/src/enums/index.ts b/packages/kboot/src/enums/index.ts new file mode 100644 index 00000000..8e98c500 --- /dev/null +++ b/packages/kboot/src/enums/index.ts @@ -0,0 +1,5 @@ +export * from './commands'; +export * from './memory-ids'; +export * from './properties'; +export * from './response-codes'; +export * from './response-tags'; diff --git a/packages/kboot/src/enums/memory-ids.ts b/packages/kboot/src/enums/memory-ids.ts new file mode 100644 index 00000000..2be2e277 --- /dev/null +++ b/packages/kboot/src/enums/memory-ids.ts @@ -0,0 +1,5 @@ +export enum MemoryIds { + Internal = 0, + Spi0 = 1, + ExecuteOnly = 0x10 +} diff --git a/packages/kboot/src/enums/properties.ts b/packages/kboot/src/enums/properties.ts new file mode 100644 index 00000000..d738da72 --- /dev/null +++ b/packages/kboot/src/enums/properties.ts @@ -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 +} diff --git a/packages/kboot/src/enums/response-codes.ts b/packages/kboot/src/enums/response-codes.ts new file mode 100644 index 00000000..a3187228 --- /dev/null +++ b/packages/kboot/src/enums/response-codes.ts @@ -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 +} diff --git a/packages/kboot/src/enums/response-tags.ts b/packages/kboot/src/enums/response-tags.ts new file mode 100644 index 00000000..b802b18b --- /dev/null +++ b/packages/kboot/src/enums/response-tags.ts @@ -0,0 +1,7 @@ +export enum ResponseTags { + Generic = 0xA0, + ReadMemory = 0xA3, + Property = 0xA7, + FlashReadOnce = 0xAF, + FlashReadResource = 0xB0 +} diff --git a/packages/kboot/src/index.ts b/packages/kboot/src/index.ts new file mode 100644 index 00000000..97ab7049 --- /dev/null +++ b/packages/kboot/src/index.ts @@ -0,0 +1,6 @@ +export * from './kboot'; +export * from './enums'; +export * from './models'; +export * from './peripheral'; +export * from './usb-peripheral'; +export * from './util'; diff --git a/packages/kboot/src/kboot.ts b/packages/kboot/src/kboot.ts new file mode 100644 index 00000000..0e0e6d3f --- /dev/null +++ b/packages/kboot/src/kboot.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + return this.peripheral.readMemory(startAddress, count); + } + + async writeMemory(options: DataOption): Promise { + return this.peripheral.writeMemory(options); + } + + /** + * Reset the bootloader + */ + async reset(): Promise { + 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 { + + 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}`); + } + } +} diff --git a/packages/kboot/src/models/bootloader-version.ts b/packages/kboot/src/models/bootloader-version.ts new file mode 100644 index 00000000..baeb3048 --- /dev/null +++ b/packages/kboot/src/models/bootloader-version.ts @@ -0,0 +1,6 @@ +export interface BootloaderVersion { + major: number; + minor: number; + bugfix: number; + protocolName: string; +} diff --git a/packages/kboot/src/models/command-option.ts b/packages/kboot/src/models/command-option.ts new file mode 100644 index 00000000..129449af --- /dev/null +++ b/packages/kboot/src/models/command-option.ts @@ -0,0 +1,7 @@ +import { Commands } from '../enums'; + +export interface CommandOption { + command: Commands; + hasDataPhase?: boolean; + params?: number[]; +} diff --git a/packages/kboot/src/models/command-response.ts b/packages/kboot/src/models/command-response.ts new file mode 100644 index 00000000..7ae28c81 --- /dev/null +++ b/packages/kboot/src/models/command-response.ts @@ -0,0 +1,7 @@ +import { ResponseCodes, ResponseTags } from '../enums'; + +export interface CommandResponse { + tag: ResponseTags; + code: ResponseCodes; + raw: Buffer; +} diff --git a/packages/kboot/src/models/data-option.ts b/packages/kboot/src/models/data-option.ts new file mode 100644 index 00000000..ec623701 --- /dev/null +++ b/packages/kboot/src/models/data-option.ts @@ -0,0 +1,4 @@ +export interface DataOption { + startAddress: number; + data: Buffer; +} diff --git a/packages/kboot/src/models/index.ts b/packages/kboot/src/models/index.ts new file mode 100644 index 00000000..bce0d6dd --- /dev/null +++ b/packages/kboot/src/models/index.ts @@ -0,0 +1,5 @@ +export * from './bootloader-version'; +export * from './command-option'; +export * from './command-response'; +export * from './data-option'; +export * from './usb'; diff --git a/packages/kboot/src/models/usb.ts b/packages/kboot/src/models/usb.ts new file mode 100644 index 00000000..5f5ced8f --- /dev/null +++ b/packages/kboot/src/models/usb.ts @@ -0,0 +1,7 @@ +export interface USB { + vendorId: number; + productId: number; + interface?: number; + usage?: number; + usePage?: number; +} diff --git a/packages/kboot/src/peripheral.ts b/packages/kboot/src/peripheral.ts new file mode 100644 index 00000000..06decd05 --- /dev/null +++ b/packages/kboot/src/peripheral.ts @@ -0,0 +1,13 @@ +import { CommandOption, CommandResponse, DataOption } from './models'; + +export interface Peripheral { + open(): void; + + close(): void; + + sendCommand(options: CommandOption): Promise; + + writeMemory(data: DataOption): Promise; + + readMemory(startAddress: number, count: number): Promise; +} diff --git a/packages/kboot/src/tsconfig.json b/packages/kboot/src/tsconfig.json new file mode 100644 index 00000000..1ac46647 --- /dev/null +++ b/packages/kboot/src/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "../dist" + } +} diff --git a/packages/kboot/src/usb-peripheral.ts b/packages/kboot/src/usb-peripheral.ts new file mode 100644 index 00000000..d5506139 --- /dev/null +++ b/packages/kboot/src/usb-peripheral.ts @@ -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 { + validateCommandParams(options.params); + const data = encodeCommandOption(options); + this._send(data); + + return this._getNextCommandResponse(); + } + + writeMemory(option: DataOption): Promise { + return new Promise(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 { + return new Promise(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 { + return this._readFromBuffer('_responseBuffer', byte, timeout); + } + + private _readFromDataStream(byte = 36, timeout = 15000): Promise { + return this._readFromBuffer('_dataBuffer', byte, timeout); + } + + private _readFromBuffer(bufferName: string, byte: number, timeout: number): Promise { + return new Promise(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 { + const response = await this._readFromCommandStream(); + const commandResponse = decodeCommandResponse(response); + logger('next command response: %o', commandResponse); + + return commandResponse; + } +} diff --git a/packages/kboot/src/util/encode-string-to-parameters.ts b/packages/kboot/src/util/encode-string-to-parameters.ts new file mode 100644 index 00000000..81956d43 --- /dev/null +++ b/packages/kboot/src/util/encode-string-to-parameters.ts @@ -0,0 +1,6 @@ +export const encodeStringToParams = (data: string): Int8Array => { + // const buffer = Buffer.from(data); + + // return new Int8Array(buffer, 0); + return new Int8Array(0); +}; diff --git a/packages/kboot/src/util/index.ts b/packages/kboot/src/util/index.ts new file mode 100644 index 00000000..824d45e8 --- /dev/null +++ b/packages/kboot/src/util/index.ts @@ -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; +}; diff --git a/packages/kboot/src/util/response-parser.ts b/packages/kboot/src/util/response-parser.ts new file mode 100644 index 00000000..82151464 --- /dev/null +++ b/packages/kboot/src/util/response-parser.ts @@ -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]; +}; diff --git a/packages/kboot/src/util/snooze.ts b/packages/kboot/src/util/snooze.ts new file mode 100644 index 00000000..60650cd8 --- /dev/null +++ b/packages/kboot/src/util/snooze.ts @@ -0,0 +1 @@ +export const snooze = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/packages/kboot/src/util/usb/decode-command-response.ts b/packages/kboot/src/util/usb/decode-command-response.ts new file mode 100644 index 00000000..92623875 --- /dev/null +++ b/packages/kboot/src/util/usb/decode-command-response.ts @@ -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 + }; +}; diff --git a/packages/kboot/src/util/usb/device-finder.ts b/packages/kboot/src/util/usb/device-finder.ts new file mode 100644 index 00000000..46146640 --- /dev/null +++ b/packages/kboot/src/util/usb/device-finder.ts @@ -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; + }; +}; diff --git a/packages/kboot/src/util/usb/encode-command-option.ts b/packages/kboot/src/util/usb/encode-command-option.ts new file mode 100644 index 00000000..bf18873a --- /dev/null +++ b/packages/kboot/src/util/usb/encode-command-option.ts @@ -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!'); + } +}; diff --git a/packages/kboot/src/util/usb/index.ts b/packages/kboot/src/util/usb/index.ts new file mode 100644 index 00000000..e3af9a55 --- /dev/null +++ b/packages/kboot/src/util/usb/index.ts @@ -0,0 +1,2 @@ +export { deviceFinder } from './device-finder'; +export { encodeCommandOption } from './encode-command-option'; diff --git a/packages/kboot/test/kboot.spec.ts b/packages/kboot/test/kboot.spec.ts new file mode 100644 index 00000000..7ab5702a --- /dev/null +++ b/packages/kboot/test/kboot.spec.ts @@ -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] + }); + }); + }); +}); diff --git a/packages/kboot/test/test-peripheral.ts b/packages/kboot/test/test-peripheral.ts new file mode 100644 index 00000000..f742ceed --- /dev/null +++ b/packages/kboot/test/test-peripheral.ts @@ -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 { + const response = { + tag: ResponseTags.Generic, + code: ResponseCodes.Success, + raw: new Buffer(0) + }; + + return Promise.resolve(response); + } + + writeMemory(data: DataOption): Promise { + return Promise.resolve(); + } + + readMemory(startAddress: number, count: number): Promise { + return Promise.resolve(new Buffer(0)); + } +} diff --git a/packages/kboot/test/uhk-helpers/index.ts b/packages/kboot/test/uhk-helpers/index.ts new file mode 100644 index 00000000..d8f6ba07 --- /dev/null +++ b/packages/kboot/test/uhk-helpers/index.ts @@ -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 => { + 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; +}; diff --git a/packages/kboot/test/util/encode-string-to-parameters.spec.ts b/packages/kboot/test/util/encode-string-to-parameters.spec.ts new file mode 100644 index 00000000..dec7f965 --- /dev/null +++ b/packages/kboot/test/util/encode-string-to-parameters.spec.ts @@ -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); + }); +}); diff --git a/packages/kboot/test/util/response-parser.spec.ts b/packages/kboot/test/util/response-parser.spec.ts new file mode 100644 index 00000000..d71169d7 --- /dev/null +++ b/packages/kboot/test/util/response-parser.spec.ts @@ -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); + }); + }); +}); diff --git a/packages/kboot/test/util/usb/encode-command-option.spec.ts b/packages/kboot/test/util/usb/encode-command-option.spec.ts new file mode 100644 index 00000000..da896d69 --- /dev/null +++ b/packages/kboot/test/util/usb/encode-command-option.spec.ts @@ -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); + }); +}); diff --git a/packages/kboot/test/zzz-uhk-integration-tests.spec.ts b/packages/kboot/test/zzz-uhk-integration-tests.spec.ts new file mode 100644 index 00000000..7a825513 --- /dev/null +++ b/packages/kboot/test/zzz-uhk-integration-tests.spec.ts @@ -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); + } + }); + }); +}); diff --git a/packages/kboot/tsconfig.json b/packages/kboot/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/packages/kboot/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/packages/uhk-agent/src/electron-main.ts b/packages/uhk-agent/src/electron-main.ts index 549ebc27..f31bf86b 100644 --- a/packages/uhk-agent/src/electron-main.ts +++ b/packages/uhk-agent/src/electron-main.ts @@ -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); diff --git a/packages/uhk-agent/src/services/device.service.ts b/packages/uhk-agent/src/services/device.service.ts index c67c3701..74c1615b 100644 --- a/packages/uhk-agent/src/services/device.service.ts +++ b/packages/uhk-agent/src/services/device.service.ts @@ -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); diff --git a/packages/uhk-usb/package-lock.json b/packages/uhk-usb/package-lock.json index bdcdc6ee..aae1d73e 100644 --- a/packages/uhk-usb/package-lock.json +++ b/packages/uhk-usb/package-lock.json @@ -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", diff --git a/packages/uhk-usb/package.json b/packages/uhk-usb/package.json index cb68923f..cb26fc20 100644 --- a/packages/uhk-usb/package.json +++ b/packages/uhk-usb/package.json @@ -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" } diff --git a/packages/uhk-usb/src/constants.ts b/packages/uhk-usb/src/constants.ts index d2bdbbef..9ec86270 100644 --- a/packages/uhk-usb/src/constants.ts +++ b/packages/uhk-usb/src/constants.ts @@ -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 { diff --git a/packages/uhk-usb/src/index.ts b/packages/uhk-usb/src/index.ts index 7ebda342..bb17b021 100644 --- a/packages/uhk-usb/src/index.ts +++ b/packages/uhk-usb/src/index.ts @@ -1,5 +1,4 @@ export * from './constants'; -export * from './uhk-blhost'; export * from './uhk-hid-device'; export * from './uhk-operations'; export * from './util'; diff --git a/packages/uhk-usb/src/uhk-blhost.ts b/packages/uhk-usb/src/uhk-blhost.ts deleted file mode 100644 index 8d10b515..00000000 --- a/packages/uhk-usb/src/uhk-blhost.ts +++ /dev/null @@ -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): Promise { - const self = this; - return new Promise((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, maxTry = 100): Promise { - 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; - } -} diff --git a/packages/uhk-usb/src/uhk-hid-device.ts b/packages/uhk-usb/src/uhk-hid-device.ts index 3a48e06b..a7761fdf 100644 --- a/packages/uhk-usb/src/uhk-hid-device.ts +++ b/packages/uhk-usb/src/uhk-hid-device.ts @@ -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); } diff --git a/packages/uhk-usb/src/uhk-operations.ts b/packages/uhk-usb/src/uhk-operations.ts index 01339590..39ee4c24 100644 --- a/packages/uhk-usb/src/uhk-operations.ts +++ b/packages/uhk-usb/src/uhk-operations.ts @@ -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); } diff --git a/packages/uhk-usb/src/util.ts b/packages/uhk-usb/src/util.ts index de41dc31..f4cbdc0b 100644 --- a/packages/uhk-usb/src/util.ts +++ b/packages/uhk-usb/src/util.ts @@ -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 x.trim()) .filter(x => !x.startsWith('#') && x.length > 0); }; + +export const readBootloaderFirmwareFromHexFileAsync = async (hexFilePath: string): Promise> => { + const fileContent = await readFile(hexFilePath, { encoding: 'utf8' }); + const memoryMap = MemoryMap.fromHex(fileContent); + + return memoryMap; +}; + +export const waitForDevice = async (vendorId: number, productId: number): Promise => { + 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}`); +}; diff --git a/scripts/copy-to-tmp-folder.js b/scripts/copy-to-tmp-folder.js index 5edeba34..a0917088 100644 --- a/scripts/copy-to-tmp-folder.js +++ b/scripts/copy-to-tmp-folder.js @@ -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'), diff --git a/scripts/release.js b/scripts/release.js index e4850738..b66b6795 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -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/'});