mirror of
https://github.com/thegecko/web-bluetooth-dfu.git
synced 2025-12-12 03:58:12 +08:00
TypeScript rewrite
This commit is contained in:
18
.eslintrc
18
.eslintrc
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"globals": {
|
||||
"define": true
|
||||
},
|
||||
rules: {
|
||||
"semi": ["error"],
|
||||
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||
"no-irregular-whitespace": ["error"],
|
||||
"linebreak-style": ["warn", "unix"],
|
||||
"no-undef": ["warn"],
|
||||
"no-unused-vars": ["warn"]
|
||||
}
|
||||
}
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,2 +1,8 @@
|
||||
.DS_Store
|
||||
.vscode
|
||||
node_modules
|
||||
dist
|
||||
lib
|
||||
types
|
||||
npm-debug.log
|
||||
package-lock.json
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
_flash
|
||||
firmware
|
||||
.eslintrc
|
||||
.gitignore
|
||||
bower.json
|
||||
circle.yml
|
||||
gulpfile.js
|
||||
index.html
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
bower.json
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Rob Moran
|
||||
Copyright (c) 2018 Rob Moran
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -36,7 +36,7 @@ https://thegecko.github.io/web-bluetooth-dfu/
|
||||
|
||||
## Prerequisites
|
||||
|
||||
[Node.js > v4.7.0](https://nodejs.org), which includes `npm`.
|
||||
[Node.js > v4.8.0](https://nodejs.org), which includes `npm`.
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
"ignore": [
|
||||
"_flash",
|
||||
"firmware",
|
||||
".eslintrc",
|
||||
".gitignore",
|
||||
"circle.yml",
|
||||
"gulpfile.js",
|
||||
"index.html",
|
||||
"tsconfig.json",
|
||||
"tslint.json",
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
29
circle.yml
29
circle.yml
@@ -1,11 +1,30 @@
|
||||
machine:
|
||||
node:
|
||||
version: 4.7.0
|
||||
version: 4.8.0
|
||||
environment:
|
||||
LIVE_BRANCH: gh-pages
|
||||
|
||||
dependencies:
|
||||
post:
|
||||
- npm install
|
||||
compile:
|
||||
override:
|
||||
- npm run gulp
|
||||
|
||||
test:
|
||||
override:
|
||||
- npm run gulp
|
||||
- exit 0
|
||||
|
||||
deployment:
|
||||
staging:
|
||||
branch: master
|
||||
commands:
|
||||
- echo Syncing to $LIVE_BRANCH on GitHub...
|
||||
- git config --global user.name thegecko
|
||||
- git config --global user.email github@thegecko.org
|
||||
- git add --force dist lib types
|
||||
- git stash save
|
||||
- git checkout $LIVE_BRANCH
|
||||
- git merge master --no-commit
|
||||
- git checkout stash -- .
|
||||
- git commit --allow-empty --message "Automatic Deployment [skip ci]"
|
||||
- git push
|
||||
- 'echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc'
|
||||
- npm publish || true
|
||||
|
||||
@@ -6,7 +6,7 @@ var crc = require("crc-32");
|
||||
var JSZip = require("jszip");
|
||||
var progress = require("progress");
|
||||
var Bluetooth = require("webbluetooth").Bluetooth;
|
||||
var Dfu = require("../index");
|
||||
var secureDfu = require("../lib");
|
||||
|
||||
var bluetoothDevices = [];
|
||||
var progressBar = null;
|
||||
@@ -125,8 +125,8 @@ function update() {
|
||||
})
|
||||
.then(content => {
|
||||
manifest = JSON.parse(content).manifest;
|
||||
dfu = new Dfu(crc.buf);
|
||||
dfu.addEventListener("progress", event => {
|
||||
dfu = new secureDfu(crc.buf, bluetooth);
|
||||
dfu.addEventListener(secureDfu.EVENT_PROGRESS, event => {
|
||||
if (progressBar && event.object === "firmware") {
|
||||
progressBar.update(event.currentBytes / event.totalBytes);
|
||||
}
|
||||
@@ -135,8 +135,7 @@ function update() {
|
||||
console.log("Scanning for DFU devices...");
|
||||
return bluetooth.requestDevice({
|
||||
acceptAllDevices: true,
|
||||
optionalServices: [Dfu.SERVICE_UUID],
|
||||
deviceFound: handleDeviceFound
|
||||
optionalServices: [secureDfu.SERVICE_UUID]
|
||||
});
|
||||
})
|
||||
.then(device => {
|
||||
@@ -147,13 +146,7 @@ function update() {
|
||||
if (device) return device;
|
||||
|
||||
console.log("DFU mode set");
|
||||
return bluetooth.requestDevice({
|
||||
filters: [{ services: [Dfu.SERVICE_UUID] }],
|
||||
deviceFound: () => {
|
||||
// Select first device found with correct service
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return dfu.requestDevice();
|
||||
})
|
||||
.then(selectedDevice => {
|
||||
device = selectedDevice;
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/crc-32/1.0.2/crc32.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.min.js"></script>
|
||||
<script src="../dist/dfu.js"></script>
|
||||
<script src="../dist/secure-dfu.js"></script>
|
||||
|
||||
<script>
|
||||
let dropEl = document.getElementById("drop");
|
||||
|
||||
108
gulpfile.js
108
gulpfile.js
@@ -1,14 +1,102 @@
|
||||
var path = require("path");
|
||||
var browserify = require("browserify");
|
||||
var del = require("del");
|
||||
var merge = require("merge2");
|
||||
var tslint = require("tslint");
|
||||
var buffer = require("vinyl-buffer");
|
||||
var source = require("vinyl-source-stream");
|
||||
var gulp = require("gulp");
|
||||
var eslint = require("gulp-eslint");
|
||||
var sourcemaps = require("gulp-sourcemaps");
|
||||
var gulpTs = require("gulp-typescript");
|
||||
var gulpTslint = require("gulp-tslint");
|
||||
var uglify = require("gulp-uglify");
|
||||
|
||||
gulp.task("lint", function() {
|
||||
return gulp.src([
|
||||
"dist/*.js",
|
||||
"examples/*.js"
|
||||
])
|
||||
.pipe(eslint(".eslintrc"))
|
||||
.pipe(eslint.format())
|
||||
.pipe(eslint.failOnError());
|
||||
// Source
|
||||
var srcDir = "src";
|
||||
var srcFiles = srcDir + "/**/*.ts";
|
||||
|
||||
// Node
|
||||
var nodeDir = "lib";
|
||||
var typesDir = "types";
|
||||
|
||||
// Browser bundles
|
||||
var bundleDir = "dist";
|
||||
var bundleGlobal = "SecureDfu";
|
||||
var bundleIgnore = "webbluetooth";
|
||||
|
||||
var watching = false;
|
||||
|
||||
// Error handler suppresses exists during watch
|
||||
function handleError() {
|
||||
if (watching) this.emit("end");
|
||||
else process.exit(1);
|
||||
}
|
||||
|
||||
// Set watching
|
||||
gulp.task("setWatch", () => {
|
||||
watching = true;
|
||||
});
|
||||
|
||||
gulp.task("default", ["lint"]);
|
||||
// Clear built directories
|
||||
gulp.task("clean", () => {
|
||||
return del([nodeDir, typesDir, bundleDir]);
|
||||
});
|
||||
|
||||
// Lint the source
|
||||
gulp.task("lint", () => {
|
||||
var program = tslint.Linter.createProgram("./");
|
||||
|
||||
gulp.src(srcFiles)
|
||||
.pipe(gulpTslint({
|
||||
program: program,
|
||||
formatter: "stylish"
|
||||
}))
|
||||
.pipe(gulpTslint.report({
|
||||
emitError: !watching
|
||||
}))
|
||||
});
|
||||
|
||||
// Build TypeScript source into CommonJS Node modules
|
||||
gulp.task("compile", ["clean"], () => {
|
||||
var tsResult = gulp.src(srcFiles)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(gulpTs.createProject("tsconfig.json")())
|
||||
.on("error", handleError);
|
||||
|
||||
return merge([
|
||||
tsResult.js.pipe(sourcemaps.write(".", {
|
||||
sourceRoot: path.relative(nodeDir, srcDir)
|
||||
})).pipe(gulp.dest(nodeDir)),
|
||||
tsResult.dts.pipe(gulp.dest(typesDir))
|
||||
]);
|
||||
});
|
||||
|
||||
// Build CommonJS modules into browser bundles
|
||||
gulp.task("bundle", ["compile"], () => {
|
||||
var fileName = bundleGlobal.replace(/([A-Z]+)/g, (match, submatch, offset) => {
|
||||
return `${offset > 0 ? "-" : ""}${match.toLowerCase()}`;
|
||||
});
|
||||
|
||||
return browserify(nodeDir, {
|
||||
standalone: bundleGlobal
|
||||
})
|
||||
.ignore(bundleIgnore)
|
||||
.bundle()
|
||||
.on("error", handleError)
|
||||
.pipe(source(`${fileName}.js`))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({
|
||||
loadMaps: true
|
||||
}))
|
||||
.pipe(uglify())
|
||||
.pipe(sourcemaps.write(".", {
|
||||
sourceRoot: path.relative(bundleDir, nodeDir)
|
||||
}))
|
||||
.pipe(gulp.dest(bundleDir));
|
||||
});
|
||||
|
||||
gulp.task("watch", ["setWatch", "default"], () => {
|
||||
gulp.watch(srcFiles, ["default"]);
|
||||
});
|
||||
|
||||
gulp.task("default", ["lint", "bundle"]);
|
||||
|
||||
27
package.json
27
package.json
@@ -5,7 +5,11 @@
|
||||
"homepage": "https://github.com/thegecko/web-bluetooth-dfu",
|
||||
"author": "Rob Moran <github@thegecko.org>",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"types": "./types/index.d.ts",
|
||||
"main": "./lib/index.js",
|
||||
"browser": {
|
||||
"./lib/index.js": "./dist/secure-dfu.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/thegecko/web-bluetooth-dfu.git"
|
||||
@@ -23,14 +27,29 @@
|
||||
"gulp": "gulp",
|
||||
"example": "node examples/node.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^8.0.54",
|
||||
"browserify": "^15.0.0",
|
||||
"crc-32": "^1.0.2",
|
||||
"del": "^3.0.0",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-eslint": "^3.0.1",
|
||||
"gulp-sourcemaps": "^2.6.1",
|
||||
"gulp-tslint": "^8.1.2",
|
||||
"gulp-typescript": "^3.2.3",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"jszip": "^3.1.3",
|
||||
"progress": "^2.0.0"
|
||||
"merge2": "^1.2.0",
|
||||
"progress": "^2.0.0",
|
||||
"tslint": "^5.8.0",
|
||||
"tslint-eslint-rules": "^4.1.1",
|
||||
"typescript": "^2.6.2",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"webbluetooth": "^1.0.0"
|
||||
"webbluetooth": "^1.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
/* @license
|
||||
*
|
||||
* Secure device firmware update with Web Bluetooth
|
||||
*
|
||||
* Protocol from:
|
||||
* http://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk5.v13.0.0/lib_dfu_transport_ble.html
|
||||
/*
|
||||
* Node Web Bluetooth
|
||||
* Copyright (c) 2017 Rob Moran
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2017 Rob Moran
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
46
src/dispatcher.ts
Normal file
46
src/dispatcher.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Web Bluetooth DFU
|
||||
* Copyright (c) 2018 Rob Moran
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
export class EventDispatcher extends EventEmitter {
|
||||
|
||||
// tslint:disable-next-line:array-type
|
||||
public addEventListener(event: string | symbol, listener: (...args: any[]) => void) {
|
||||
return super.addListener(event, listener);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:array-type
|
||||
public removeEventListener(event: string | symbol, listener: (...args: any[]) => void) {
|
||||
return super.removeListener(event, listener);
|
||||
}
|
||||
|
||||
public dispatchEvent(eventType: string | symbol, event?: any) {
|
||||
return super.emit(eventType, event);
|
||||
}
|
||||
}
|
||||
27
src/index.ts
Normal file
27
src/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Web Bluetooth DFU
|
||||
* Copyright (c) 2018 Rob Moran
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import dfu = require("./secure-dfu");
|
||||
module.exports = dfu.SecureDfu;
|
||||
472
src/secure-dfu.ts
Normal file
472
src/secure-dfu.ts
Normal file
@@ -0,0 +1,472 @@
|
||||
/*
|
||||
* Web Bluetooth DFU
|
||||
* Copyright (c) 2018 Rob Moran
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Bluetooth, BluetoothDevice } from "webbluetooth";
|
||||
import { BluetoothRemoteGATTCharacteristic } from "webbluetooth";
|
||||
import { EventDispatcher } from "./dispatcher";
|
||||
|
||||
declare global {
|
||||
interface Navigator {
|
||||
bluetooth: any;
|
||||
}
|
||||
}
|
||||
|
||||
const CONTROL_UUID = "8ec90001-f315-4f60-9fb8-838830daea50";
|
||||
const PACKET_UUID = "8ec90002-f315-4f60-9fb8-838830daea50";
|
||||
const BUTTON_UUID = "8ec90003-f315-4f60-9fb8-838830daea50";
|
||||
|
||||
const LITTLE_ENDIAN = true;
|
||||
const PACKET_SIZE = 20;
|
||||
|
||||
const OPERATIONS = {
|
||||
BUTTON_COMMAND: [ 0x01 ],
|
||||
CREATE_COMMAND: [ 0x01, 0x01 ],
|
||||
CREATE_DATA: [ 0x01, 0x02 ],
|
||||
RECEIPT_NOTIFICATIONS: [ 0x02 ],
|
||||
CACULATE_CHECKSUM: [ 0x03 ],
|
||||
EXECUTE: [ 0x04 ],
|
||||
SELECT_COMMAND: [ 0x06, 0x01 ],
|
||||
SELECT_DATA: [ 0x06, 0x02 ],
|
||||
RESPONSE: [ 0x60, 0x20 ]
|
||||
};
|
||||
|
||||
const RESPONSE = {
|
||||
0x00: "Invalid code", // Invalid opcode.
|
||||
0x01: "Success", // Operation successful.
|
||||
0x02: "Opcode not supported", // Opcode not supported.
|
||||
0x03: "Invalid parameter", // Missing or invalid parameter value.
|
||||
0x04: "Insufficient resources", // Not enough memory for the data object.
|
||||
0x05: "Invalid object", // Data object does not match the firmware and hardware requirements, the signature is wrong, or parsing the command failed.
|
||||
0x07: "Unsupported type", // Not a valid object type for a Create request.
|
||||
0x08: "Operation not permitted", // The state of the DFU process does not allow this operation.
|
||||
0x0A: "Operation failed", // Operation failed.
|
||||
0x0B: "Extended error" // Extended error.
|
||||
};
|
||||
|
||||
const EXTENDED_ERROR = {
|
||||
0x00: "No error", // No extended error code has been set. This error indicates an implementation problem.
|
||||
0x01: "Invalid error code", // Invalid error code. This error code should never be used outside of development.
|
||||
0x02: "Wrong command format", // The format of the command was incorrect.
|
||||
0x03: "Unknown command", // The command was successfully parsed, but it is not supported or unknown.
|
||||
0x04: "Init command invalid", // The init command is invalid. The init packet either has an invalid update type or it is missing required fields for the update type.
|
||||
0x05: "Firmware version failure", // The firmware version is too low. For an application, the version must be greater than the current application. For a bootloader, it must be greater than or equal to the current version.
|
||||
0x06: "Hardware version failure", // The hardware version of the device does not match the required hardware version for the update.
|
||||
0x07: "Softdevice version failure", // The array of supported SoftDevices for the update does not contain the FWID of the current SoftDevice.
|
||||
0x08: "Signature missing", // The init packet does not contain a signature.
|
||||
0x09: "Wrong hash type", // The hash type that is specified by the init packet is not supported by the DFU bootloader.
|
||||
0x0A: "Hash failed", // The hash of the firmware image cannot be calculated.
|
||||
0x0B: "Wrong signature type", // The type of the signature is unknown or not supported by the DFU bootloader.
|
||||
0x0C: "Verification failed", // The hash of the received firmware image does not match the hash in the init packet.
|
||||
0x0D: "Insufficient space" // The available space on the device is insufficient to hold the firmware.
|
||||
};
|
||||
|
||||
/**
|
||||
* BluetoothLE Scan Filter Init interface
|
||||
*/
|
||||
export interface BluetoothLEScanFilterInit {
|
||||
/**
|
||||
* An array of service UUIDs to filter on
|
||||
*/
|
||||
services?: Array<string | number>;
|
||||
|
||||
/**
|
||||
* The device name to filter on
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* The device name prefix to filter on
|
||||
*/
|
||||
namePrefix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure Device Firmware Update class
|
||||
*/
|
||||
export class SecureDfu extends EventDispatcher {
|
||||
|
||||
/**
|
||||
* DFU Service unique identifier
|
||||
*/
|
||||
public static SERVICE_UUID: number = 0xFE59;
|
||||
|
||||
/**
|
||||
* Log event
|
||||
* @event
|
||||
*/
|
||||
public static EVENT_LOG: string = "log";
|
||||
|
||||
/**
|
||||
* Progress event
|
||||
* @event
|
||||
*/
|
||||
public static EVENT_PROGRESS: string = "progress";
|
||||
|
||||
private notifyFns: {} = {};
|
||||
private controlChar: BluetoothRemoteGATTCharacteristic = null;
|
||||
private packetChar: BluetoothRemoteGATTCharacteristic = null;
|
||||
|
||||
/**
|
||||
* Characteristic constructor
|
||||
* @param bluetooth A bluetooth instance
|
||||
* @param crc32 A CRC32 function
|
||||
*/
|
||||
constructor(private crc32: (data: Array<number> | Uint8Array, seed?: number) => number, private bluetooth?: Bluetooth) {
|
||||
super();
|
||||
|
||||
if (!this.bluetooth && window && window.navigator && window.navigator.bluetooth) {
|
||||
this.bluetooth = navigator.bluetooth;
|
||||
}
|
||||
}
|
||||
|
||||
private log(message: string) {
|
||||
this.dispatchEvent(SecureDfu.EVENT_LOG, {
|
||||
message: message
|
||||
});
|
||||
}
|
||||
|
||||
private progress(bytes: number) {
|
||||
this.dispatchEvent(SecureDfu.EVENT_PROGRESS, {
|
||||
object: "unknown",
|
||||
totalBytes: 0,
|
||||
currentBytes: bytes
|
||||
});
|
||||
}
|
||||
|
||||
private connect(device: BluetoothDevice): Promise<BluetoothDevice> {
|
||||
device.addEventListener("gattserverdisconnected", () => {
|
||||
this.controlChar = null;
|
||||
this.packetChar = null;
|
||||
});
|
||||
|
||||
return this.gattConnect(device)
|
||||
.then(characteristics => {
|
||||
this.log(`found ${characteristics.length} characteristic(s)`);
|
||||
|
||||
this.packetChar = characteristics.find(characteristic => {
|
||||
return (characteristic.uuid === PACKET_UUID);
|
||||
});
|
||||
if (!this.packetChar) throw new Error("Unable to find packet characteristic");
|
||||
this.log("found packet characteristic");
|
||||
|
||||
this.controlChar = characteristics.find(characteristic => {
|
||||
return (characteristic.uuid === CONTROL_UUID);
|
||||
});
|
||||
if (!this.controlChar) throw new Error("Unable to find control characteristic");
|
||||
this.log("found control characteristic");
|
||||
|
||||
if (!this.controlChar.properties.notify && !this.controlChar.properties.indicate) {
|
||||
throw new Error("Control characteristic does not allow notifications");
|
||||
}
|
||||
return this.controlChar.startNotifications();
|
||||
})
|
||||
.then(() => {
|
||||
this.controlChar.addEventListener("characteristicvaluechanged", this.handleNotification.bind(this));
|
||||
this.log("enabled control notifications");
|
||||
return device;
|
||||
});
|
||||
}
|
||||
|
||||
private gattConnect(device: BluetoothDevice): Promise<Array<BluetoothRemoteGATTCharacteristic>> {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (device.gatt.connected) return device.gatt;
|
||||
return device.gatt.connect();
|
||||
})
|
||||
.then(server => {
|
||||
this.log("connected to gatt server");
|
||||
return server.getPrimaryService(SecureDfu.SERVICE_UUID)
|
||||
.catch(() => {
|
||||
throw new Error("Unable to find DFU service");
|
||||
});
|
||||
})
|
||||
.then(service => {
|
||||
this.log("found DFU service");
|
||||
return service.getCharacteristics();
|
||||
});
|
||||
}
|
||||
|
||||
private handleNotification(event: any) {
|
||||
const view = event.target.value;
|
||||
|
||||
if (OPERATIONS.RESPONSE.indexOf(view.getUint8(0)) < 0) {
|
||||
throw new Error("Unrecognised control characteristic response notification");
|
||||
}
|
||||
|
||||
const operation = view.getUint8(1);
|
||||
if (this.notifyFns[operation]) {
|
||||
const result = view.getUint8(2);
|
||||
let error = null;
|
||||
|
||||
if (result === 0x01) {
|
||||
const data = new DataView(view.buffer, 3);
|
||||
this.notifyFns[operation].resolve(data);
|
||||
} else if (result === 0x0B) {
|
||||
const code = view.getUint8(3);
|
||||
error = `Error: ${EXTENDED_ERROR[code]}`;
|
||||
} else {
|
||||
error = `Error: ${RESPONSE[result]}`;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.log(`notify: ${error}`);
|
||||
this.notifyFns[operation].reject(error);
|
||||
}
|
||||
delete this.notifyFns[operation];
|
||||
}
|
||||
}
|
||||
|
||||
private sendOperation(characteristic: BluetoothRemoteGATTCharacteristic, operation: Array<number>, buffer?: ArrayBuffer): Promise<DataView> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let size = operation.length;
|
||||
if (buffer) size += buffer.byteLength;
|
||||
|
||||
const value = new Uint8Array(size);
|
||||
value.set(operation);
|
||||
if (buffer) {
|
||||
const data = new Uint8Array(buffer);
|
||||
value.set(data, operation.length);
|
||||
}
|
||||
|
||||
this.notifyFns[operation[0]] = {
|
||||
resolve: resolve,
|
||||
reject: reject
|
||||
};
|
||||
|
||||
characteristic.writeValue(value);
|
||||
});
|
||||
}
|
||||
|
||||
private sendControl(operation: Array<number>, buffer?: ArrayBuffer): Promise<DataView> {
|
||||
return this.sendOperation(this.controlChar, operation, buffer);
|
||||
}
|
||||
|
||||
private transferInit(buffer: ArrayBuffer): Promise<DataView> {
|
||||
return this.transfer(buffer, "init", OPERATIONS.SELECT_COMMAND, OPERATIONS.CREATE_COMMAND);
|
||||
}
|
||||
|
||||
private transferFirmware(buffer: ArrayBuffer): Promise<DataView> {
|
||||
return this.transfer(buffer, "firmware", OPERATIONS.SELECT_DATA, OPERATIONS.CREATE_DATA);
|
||||
}
|
||||
|
||||
private transfer(buffer: ArrayBuffer, type: string, selectType: Array<number>, createType: Array<number>): Promise<DataView> {
|
||||
return this.sendControl(selectType)
|
||||
.then(response => {
|
||||
|
||||
const maxSize = response.getUint32(0, LITTLE_ENDIAN);
|
||||
const offset = response.getUint32(4, LITTLE_ENDIAN);
|
||||
const crc = response.getInt32(8, LITTLE_ENDIAN);
|
||||
|
||||
if (type === "init" && offset === buffer.byteLength && this.checkCrc(buffer, crc)) {
|
||||
this.log("init packet already available, skipping transfer");
|
||||
return;
|
||||
}
|
||||
|
||||
this.progress = bytes => {
|
||||
this.dispatchEvent(SecureDfu.EVENT_PROGRESS, {
|
||||
object: type,
|
||||
totalBytes: buffer.byteLength,
|
||||
currentBytes: bytes
|
||||
});
|
||||
};
|
||||
this.progress(0);
|
||||
|
||||
return this.transferObject(buffer, createType, maxSize, offset);
|
||||
});
|
||||
}
|
||||
|
||||
private transferObject(buffer: ArrayBuffer, createType: Array<number>, maxSize: number, offset: number): Promise<DataView> {
|
||||
const start = offset - offset % maxSize;
|
||||
const end = Math.min(start + maxSize, buffer.byteLength);
|
||||
|
||||
const view = new DataView(new ArrayBuffer(4));
|
||||
view.setUint32(0, end - start, LITTLE_ENDIAN);
|
||||
|
||||
return this.sendControl(createType, view.buffer)
|
||||
.then(() => {
|
||||
const data = buffer.slice(start, end);
|
||||
return this.transferData(data, start);
|
||||
})
|
||||
.then(() => {
|
||||
return this.sendControl(OPERATIONS.CACULATE_CHECKSUM);
|
||||
})
|
||||
.then(response => {
|
||||
const crc = response.getInt32(4, LITTLE_ENDIAN);
|
||||
const transferred = response.getUint32(0, LITTLE_ENDIAN);
|
||||
const data = buffer.slice(0, transferred);
|
||||
|
||||
if (this.checkCrc(data, crc)) {
|
||||
this.log(`written ${transferred} bytes`);
|
||||
offset = transferred;
|
||||
return this.sendControl(OPERATIONS.EXECUTE);
|
||||
} else {
|
||||
this.log("object failed to validate");
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (end < buffer.byteLength) {
|
||||
return this.transferObject(buffer, createType, maxSize, offset);
|
||||
} else {
|
||||
this.log("transfer complete");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private transferData(data: ArrayBuffer, offset: number, start?: number) {
|
||||
start = start || 0;
|
||||
const end = Math.min(start + PACKET_SIZE, data.byteLength);
|
||||
const packet = data.slice(start, end);
|
||||
|
||||
return this.packetChar.writeValue(packet)
|
||||
.then(() => {
|
||||
this.progress(offset + end);
|
||||
|
||||
if (end < data.byteLength) {
|
||||
return this.transferData(data, offset, end);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private checkCrc(buffer: ArrayBuffer, crc: number): boolean {
|
||||
if (!this.crc32) {
|
||||
this.log("crc32 not found, skipping CRC check");
|
||||
return true;
|
||||
}
|
||||
|
||||
return crc === this.crc32(new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans for a device to update
|
||||
* @param buttonLess Scans for all devices and will automatically call `setDfuMode`
|
||||
* @param filters Alternative filters to use when scanning
|
||||
* @returns Promise containing the device
|
||||
*/
|
||||
public requestDevice(buttonLess: boolean, filters: Array<BluetoothLEScanFilterInit>): Promise<BluetoothDevice> {
|
||||
if (!buttonLess && !filters) {
|
||||
filters = [ { services: [ SecureDfu.SERVICE_UUID ] } ];
|
||||
}
|
||||
|
||||
const options: any = {
|
||||
optionalServices: [ SecureDfu.SERVICE_UUID ]
|
||||
};
|
||||
|
||||
if (filters) options.filters = filters;
|
||||
else options.acceptAllDevices = true;
|
||||
|
||||
return this.bluetooth.requestDevice(options)
|
||||
.then(device => {
|
||||
if (buttonLess) {
|
||||
return this.setDfuMode(device);
|
||||
}
|
||||
return device;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the DFU mode of a device, preparing it for update
|
||||
* @param device The device to switch mode
|
||||
* @returns Promise containing the device
|
||||
*/
|
||||
public setDfuMode(device: BluetoothDevice): Promise<BluetoothDevice> {
|
||||
return this.gattConnect(device)
|
||||
.then(characteristics => {
|
||||
this.log(`found ${characteristics.length} characteristic(s)`);
|
||||
|
||||
const controlChar = characteristics.find(characteristic => {
|
||||
return (characteristic.uuid === CONTROL_UUID);
|
||||
});
|
||||
const packetChar = characteristics.find(characteristic => {
|
||||
return (characteristic.uuid === PACKET_UUID);
|
||||
});
|
||||
|
||||
if (controlChar && packetChar) {
|
||||
return device;
|
||||
}
|
||||
|
||||
const buttonChar = characteristics.find(characteristic => {
|
||||
return (characteristic.uuid === BUTTON_UUID);
|
||||
});
|
||||
|
||||
if (!buttonChar) {
|
||||
throw new Error("Unsupported device");
|
||||
}
|
||||
|
||||
// Support buttonless devices
|
||||
this.log("found buttonless characteristic");
|
||||
if (!buttonChar.properties.notify && !buttonChar.properties.indicate) {
|
||||
throw new Error("Buttonless characteristic does not allow notifications");
|
||||
}
|
||||
|
||||
return buttonChar.startNotifications()
|
||||
.then(() => {
|
||||
this.log("enabled buttonless notifications");
|
||||
buttonChar.addEventListener("characteristicvaluechanged", this.handleNotification.bind(this));
|
||||
return this.sendOperation(buttonChar, OPERATIONS.BUTTON_COMMAND);
|
||||
})
|
||||
.then(() => {
|
||||
this.log("sent dfu mode");
|
||||
return new Promise<BluetoothDevice>((resolve, _reject) => {
|
||||
device.addEventListener("gattserverdisconnected", () => {
|
||||
resolve(device);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a device
|
||||
* @param device The device to switch mode
|
||||
* @param init The initialisation packet to send
|
||||
* @param firmware The firmware to update
|
||||
* @returns Promise containing the device
|
||||
*/
|
||||
public update(device: BluetoothDevice, init: ArrayBuffer, firmware: ArrayBuffer): Promise<BluetoothDevice> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!device) return reject("Device not specified");
|
||||
if (!init) return reject("Init not specified");
|
||||
if (!firmware) return reject("Firmware not specified");
|
||||
|
||||
this.connect(device)
|
||||
.then(() => {
|
||||
this.log("transferring init");
|
||||
return this.transferInit(init);
|
||||
})
|
||||
.then(() => {
|
||||
this.log("transferring firmware");
|
||||
return this.transferFirmware(firmware);
|
||||
})
|
||||
.then(() => {
|
||||
this.log("complete, disconnecting...");
|
||||
|
||||
device.addEventListener("gattserverdisconnected", () => {
|
||||
this.log("disconnected");
|
||||
resolve(device);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"lib": [
|
||||
"dom", // window.navigator
|
||||
"es5",
|
||||
"es2015.core", // Array.find
|
||||
"es2015.promise", // Promise
|
||||
"es2015.symbol.wellknown" // Map
|
||||
],
|
||||
"alwaysStrict": true,
|
||||
"noEmitOnError": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"declaration": true
|
||||
}
|
||||
}
|
||||
34
tslint.json
Normal file
34
tslint.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"extends": [
|
||||
"tslint:recommended",
|
||||
"tslint-eslint-rules"
|
||||
],
|
||||
"rules": {
|
||||
"array-bracket-spacing": [true, "always"],
|
||||
"arrow-parens": [true, "ban-single-arg-parens"],
|
||||
"array-type": [true, "generic"],
|
||||
"block-spacing": [true, "always"],
|
||||
"brace-style": [true, "1tbs", { "allowSingleLine": true }],
|
||||
"curly": [true, "ignore-same-line"],
|
||||
"eofline": true,
|
||||
"interface-name": [true, "never-prefix"],
|
||||
"linebreak-style": [true, "LF"],
|
||||
"max-line-length": [false],
|
||||
"member-ordering": [true, { "order": ["static-field", "instance-field", "constructor", "static-method", "instance-method"] }],
|
||||
"no-bitwise": false,
|
||||
"no-console": [true],
|
||||
"no-empty-interface": false,
|
||||
"no-trailing-whitespace": [true],
|
||||
"no-unused-variable": [true],
|
||||
"object-curly-spacing": [true, "always"],
|
||||
"object-literal-shorthand": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"ordered-imports": [true, { "import-sources-order": "any", "named-imports-order": "any" }],
|
||||
"semicolon": [true, "always"],
|
||||
"ter-indent": [true, 4],
|
||||
"ter-no-irregular-whitespace": [true],
|
||||
"trailing-comma": [true, "never"],
|
||||
"triple-equals": [true],
|
||||
"variable-name": [true, "allow-leading-underscore"]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user