mirror of
https://github.com/thegecko/web-bluetooth-dfu.git
synced 2025-12-06 09:02:52 +08:00
Switch bleat to webbluetooth and remove insecure DFU
This commit is contained in:
@@ -10,7 +10,6 @@
|
||||
},
|
||||
"keywords": [
|
||||
"ble",
|
||||
"bleat",
|
||||
"bluetooth",
|
||||
"dfu",
|
||||
"firmware",
|
||||
@@ -19,9 +18,10 @@
|
||||
"web-bluetooth"
|
||||
],
|
||||
"dependencies": {
|
||||
"bleat": "git://github.com/thegecko/bleat.git"
|
||||
"webbluetooth": "git://github.com/thegecko/webbluetooth.git"
|
||||
},
|
||||
"ignore": [
|
||||
"_flash",
|
||||
"firmware",
|
||||
".eslintrc",
|
||||
".gitignore",
|
||||
|
||||
64
dist/crc16.js
vendored
64
dist/crc16.js
vendored
@@ -1,64 +0,0 @@
|
||||
/* @license
|
||||
*
|
||||
* CRC-CCITT (0xFFFF) library.
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2016
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// https://github.com/umdjs/umd
|
||||
(function(global, factory) {
|
||||
if (typeof exports === 'object') {
|
||||
// CommonJS (Node)
|
||||
module.exports = factory();
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
// AMD
|
||||
define(factory);
|
||||
} else {
|
||||
// Browser global (with support for web workers)
|
||||
global.crc16 = factory();
|
||||
}
|
||||
}(this, function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Copied from Nordic's command line tool nrf.exe.
|
||||
* https://github.com/NordicSemiconductor/pc-nrfutil/blob/master/nordicsemi/dfu/crc16.py
|
||||
* CRC-CCITT (0xFFFF).
|
||||
*/
|
||||
return function (binaryData) {
|
||||
var crc = 0xFFFF;
|
||||
var view = new Uint8Array(binaryData);
|
||||
|
||||
var i;
|
||||
|
||||
for (i = 0; i < view.byteLength; i++) {
|
||||
crc = (crc >> 8 & 0x00FF) | (crc << 8 & 0xFF00);
|
||||
crc ^= view[i];
|
||||
crc ^= (crc & 0x00FF) >> 4;
|
||||
crc ^= (crc << 8) << 4;
|
||||
crc ^= ((crc & 0x00FF) << 4) << 1;
|
||||
}
|
||||
|
||||
return (crc & 0xFFFF);
|
||||
};
|
||||
}));
|
||||
772
dist/dfu.js
vendored
772
dist/dfu.js
vendored
@@ -1,9 +1,9 @@
|
||||
/* @license
|
||||
*
|
||||
* Device firmware update with Web Bluetooth
|
||||
* Secure device firmware update with Web Bluetooth
|
||||
*
|
||||
* Protocol from:
|
||||
* http://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk5.v11.0.0/bledfu_transport.html
|
||||
* http://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk5.v13.0.0/lib_dfu_transport_ble.html
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
@@ -31,440 +31,410 @@
|
||||
// https://github.com/umdjs/umd
|
||||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['es6-promise', 'bleat', 'crc16'], factory);
|
||||
// AMD. Unsupported
|
||||
} else if (typeof exports === 'object') {
|
||||
// Node. Does not work with strict CommonJS
|
||||
module.exports = factory(Promise, require('bleat').webbluetooth, require('./crc16'));
|
||||
module.exports = factory(require('webbluetooth').bluetooth);
|
||||
} else {
|
||||
// Browser globals with support for web workers (root is window)
|
||||
root.dfu = factory(Promise, root.navigator.bluetooth, root.crc16);
|
||||
root.SecureDfu = factory(root.navigator.bluetooth);
|
||||
}
|
||||
}(this, function(Promise, bluetooth, crc16) {
|
||||
}(this, function(bluetooth) {
|
||||
"use strict";
|
||||
|
||||
// Make server a global variable (initialized in connect().
|
||||
// This fixes a bug in BlueZ that causes transfers to stall.
|
||||
var currentServer = null;
|
||||
const SERVICE_UUID = 0xFE59;
|
||||
const CONTROL_UUID = "8ec90001-f315-4f60-9fb8-838830daea50";
|
||||
const PACKET_UUID = "8ec90002-f315-4f60-9fb8-838830daea50";
|
||||
const BUTTON_UUID = "8ec90003-f315-4f60-9fb8-838830daea50";
|
||||
|
||||
var LITTLE_ENDIAN = true;
|
||||
const LITTLE_ENDIAN = true;
|
||||
const PACKET_SIZE = 20;
|
||||
|
||||
var packetSize = 20;
|
||||
var notifySteps = 40;
|
||||
|
||||
var serviceUUID = "00001530-1212-efde-1523-785feabcd123";
|
||||
var controlUUID = "00001531-1212-efde-1523-785feabcd123";
|
||||
var packetUUID = "00001532-1212-efde-1523-785feabcd123";
|
||||
var versionUUID = "00001534-1212-efde-1523-785feabcd123";
|
||||
|
||||
var ImageType = {
|
||||
None: 0,
|
||||
SoftDevice: 1,
|
||||
Bootloader: 2,
|
||||
SoftDevice_Bootloader: 3, // Will not work right now
|
||||
Application: 4
|
||||
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]
|
||||
};
|
||||
|
||||
// TODO: This should be configurable by the user. For now this will work with any of Nordic's SDK examples.
|
||||
var initPacket = {
|
||||
device_type: 0xFFFF,
|
||||
device_rev: 0xFFFF,
|
||||
app_version: 0xFFFFFFFF,
|
||||
softdevice_len: 0x0001,
|
||||
softdevice: 0xFFFE,
|
||||
crc: 0x0000
|
||||
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.
|
||||
};
|
||||
|
||||
var OPCODE = {
|
||||
RESERVED: 0,
|
||||
START_DFU: 1,
|
||||
INITIALIZE_DFU_PARAMETERS: 2,
|
||||
RECEIVE_FIRMWARE_IMAGE: 3,
|
||||
VALIDATE_FIRMWARE: 4,
|
||||
ACTIVATE_IMAGE_AND_RESET: 5,
|
||||
RESET_SYSTEM: 6,
|
||||
REPORT_RECEIVED_IMAGE_SIZE: 7,
|
||||
PACKET_RECEIPT_NOTIFICATION_REQUEST: 8,
|
||||
RESPONSE_CODE: 16,
|
||||
PACKET_RECEIPT_NOTIFICATION: 17
|
||||
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.
|
||||
};
|
||||
|
||||
var loggers = [];
|
||||
function addLogger(loggerFn) {
|
||||
if (typeof loggerFn === "function") {
|
||||
loggers.push(loggerFn);
|
||||
function secureDfu(crc32) {
|
||||
this.crc32 = crc32;
|
||||
this.events = {};
|
||||
this.notifyFns = {};
|
||||
this.controlChar = null;
|
||||
this.packetChar = null;
|
||||
}
|
||||
|
||||
function createListenerFn(eventTypes) {
|
||||
return function(type, callback) {
|
||||
if (eventTypes.indexOf(type) < 0) return;
|
||||
if (!this.events[type]) this.events[type] = [];
|
||||
this.events[type].push(callback);
|
||||
};
|
||||
}
|
||||
function removeEventListener(type, callback) {
|
||||
if (!this.events[type]) return;
|
||||
let i = this.events[type].indexOf(callback);
|
||||
if (i >= 0) this.events[type].splice(i, 1);
|
||||
if (this.events[type].length === 0) delete this.events[type];
|
||||
}
|
||||
function dispatchEvent(event) {
|
||||
if (!this.events[event.type]) return;
|
||||
event.target = this;
|
||||
this.events[event.type].forEach(callback => {
|
||||
if (typeof callback === "function") callback(event);
|
||||
});
|
||||
}
|
||||
|
||||
secureDfu.prototype.log = function(message) {
|
||||
this.dispatchEvent({
|
||||
type: "log",
|
||||
message: message
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.progress = function(bytes) {
|
||||
this.dispatchEvent({
|
||||
type: "progress",
|
||||
object: "unknown",
|
||||
totalBytes: 0,
|
||||
currentBytes: bytes
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.requestDevice = function(hiddenDfu, filters) {
|
||||
if (!hiddenDfu && !filters) {
|
||||
filters = [{ services: [SERVICE_UUID] }];
|
||||
}
|
||||
}
|
||||
function log(message) {
|
||||
loggers.forEach(function(logger) {
|
||||
logger(message);
|
||||
});
|
||||
}
|
||||
|
||||
function findDevice(filters) {
|
||||
return bluetooth.requestDevice({
|
||||
filters: [ filters ],
|
||||
optionalServices: [serviceUUID]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to bootloader/DFU mode by writing to the control point of the DFU Service.
|
||||
* The DFU Controller is not responsible for disconnecting from the application (DFU Target) after the write.
|
||||
* The application (DFU Target) will issue a GAP Disconnect and reset into bootloader/DFU mode.
|
||||
*
|
||||
* https://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk5.v11.0.0/bledfu_appswitching.html?cp=4_0_0_4_1_3_2_2
|
||||
*/
|
||||
function writeMode(device) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
|
||||
var resolved = false;
|
||||
function disconnectHandler() {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
log("DFU target issued GAP disconnect and reset into bootloader/DFU mode");
|
||||
resolve(device);
|
||||
}
|
||||
filters: filters,
|
||||
acceptAllDevices: !filters,
|
||||
optionalServices: [SERVICE_UUID]
|
||||
})
|
||||
.then(device => {
|
||||
if (hiddenDfu) {
|
||||
return this.setDfuMode(device);
|
||||
}
|
||||
device.addEventListener("gattserverdisconnected", disconnectHandler);
|
||||
|
||||
var characteristics = null;
|
||||
|
||||
connect(device)
|
||||
.then(function(chars) {
|
||||
log("enabling notifications");
|
||||
characteristics = chars;
|
||||
return characteristics.controlChar.startNotifications();
|
||||
})
|
||||
.then(function() {
|
||||
log("writing modeData");
|
||||
return characteristics.controlChar.writeValue(new Uint8Array([OPCODE.START_DFU, ImageType.Application]));
|
||||
})
|
||||
.then(function() {
|
||||
log("modeData written");
|
||||
// TODO: Remove this when gattserverdisconnected event is implemented and possibly put a timeout in that event handler before resolving
|
||||
setTimeout(function() {
|
||||
if (currentServer && currentServer.connected === true) {
|
||||
currentServer.disconnect();
|
||||
}
|
||||
disconnectHandler();
|
||||
}, 5000);
|
||||
})
|
||||
.catch(function(error) {
|
||||
error = "writeMode error: " + error;
|
||||
log(error);
|
||||
reject(error);
|
||||
});
|
||||
return device;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains basic functionality for performing safety checks on software updates for nRF5 based devices.
|
||||
* Init packet used for pre-checking to ensure the following image is compatible with the device.
|
||||
* Contains information on device type, revision, and supported SoftDevices along with a CRC or hash of firmware image.
|
||||
*
|
||||
* Not used in mbed bootloader (init packet was optional in SDK v6.x).
|
||||
*/
|
||||
function generateInitPacket() {
|
||||
var buffer = new ArrayBuffer(14);
|
||||
var view = new DataView(buffer);
|
||||
view.setUint16(0, initPacket.device_type, LITTLE_ENDIAN);
|
||||
view.setUint16(2, initPacket.device_rev, LITTLE_ENDIAN);
|
||||
view.setUint32(4, initPacket.app_version, LITTLE_ENDIAN); // Application version for the image software. This field allows for additional checking, for example ensuring that a downgrade is not allowed.
|
||||
view.setUint16(8, initPacket.softdevice_len, LITTLE_ENDIAN); // Number of different SoftDevice revisions compatible with this application.
|
||||
view.setUint16(10, initPacket.softdevice, LITTLE_ENDIAN); // Variable length array of SoftDevices compatible with this application. The length of the array is specified in the length (softdevice_len) field. 0xFFFE indicates any SoftDevice.
|
||||
view.setUint16(12, initPacket.crc, LITTLE_ENDIAN);
|
||||
return view;
|
||||
}
|
||||
secureDfu.prototype.setDfuMode = function(device) {
|
||||
return this.gattConnect(device)
|
||||
.then(characteristics => {
|
||||
this.log(`found ${characteristics.length} characteristic(s)`);
|
||||
|
||||
function provision(device, arrayBuffer, imageType, crc) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
imageType = imageType || ImageType.Application;
|
||||
initPacket.crc = crc || crc16(arrayBuffer);
|
||||
var versionChar = null;
|
||||
let controlChar = characteristics.find(characteristic => {
|
||||
return (characteristic.uuid === CONTROL_UUID);
|
||||
});
|
||||
let packetChar = characteristics.find(characteristic => {
|
||||
return (characteristic.uuid === PACKET_UUID);
|
||||
});
|
||||
|
||||
connect(device)
|
||||
.then(function(chars) {
|
||||
versionChar = chars.versionChar;
|
||||
// Older DFU implementations (from older Nordic SDKs < 7.0) have no DFU Version characteristic.
|
||||
if (versionChar) {
|
||||
return versionChar.readValue()
|
||||
.then(function(data) {
|
||||
log('read versionChar');
|
||||
var major = data.getUint8(0);
|
||||
var minor = data.getUint8(1);
|
||||
return transfer(chars, arrayBuffer, imageType, major, minor);
|
||||
if (controlChar && packetChar) {
|
||||
return device;
|
||||
}
|
||||
|
||||
let 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));
|
||||
this.sendOperation(buttonChar, OPERATIONS.BUTTON_COMMAND);
|
||||
})
|
||||
.then(() => {
|
||||
this.log("sent dfu mode");
|
||||
return new Promise((resolve) => {
|
||||
device.addEventListener("gattserverdisconnected", () => {
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
// Default to version 6.0 (mbed).
|
||||
return transfer(chars, arrayBuffer, imageType, 6, 0);
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
resolve();
|
||||
})
|
||||
.catch(function(error) {
|
||||
log(error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function connect(device) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var service = null;
|
||||
var controlChar = null;
|
||||
var packetChar = null;
|
||||
var versionChar = null;
|
||||
secureDfu.prototype.update = function(device, init, firmware) {
|
||||
if (!device) throw new Error("Device not specified");
|
||||
if (!init) throw new Error("Init not specified");
|
||||
if (!firmware) throw new Error("Firmware not specified");
|
||||
|
||||
function complete() {
|
||||
resolve({
|
||||
controlChar: controlChar,
|
||||
packetChar: packetChar,
|
||||
versionChar: versionChar
|
||||
});
|
||||
}
|
||||
|
||||
device.gatt.connect()
|
||||
.then(function(gattServer) {
|
||||
log("connected to device");
|
||||
currentServer = gattServer;
|
||||
// This delay is needed because BlueZ needs time to update it's cache.
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(resolve, 2000);
|
||||
});
|
||||
})
|
||||
.then(function() {
|
||||
return currentServer.getPrimaryService(serviceUUID);
|
||||
})
|
||||
.then(function(primaryService) {
|
||||
log("found DFU service");
|
||||
service = primaryService;
|
||||
return service.getCharacteristic(controlUUID);
|
||||
})
|
||||
.then(function(characteristic) {
|
||||
log("found control characteristic");
|
||||
controlChar = characteristic;
|
||||
return service.getCharacteristic(packetUUID);
|
||||
})
|
||||
.then(function(characteristic) {
|
||||
log("found packet characteristic");
|
||||
packetChar = characteristic;
|
||||
service.getCharacteristic(versionUUID)
|
||||
// Older DFU implementations (from older Nordic SDKs) have no DFU Version characteristic. So this may fail.
|
||||
.then(function(characteristic) {
|
||||
log("found version characteristic");
|
||||
versionChar = characteristic;
|
||||
complete();
|
||||
})
|
||||
.catch(function() {
|
||||
log("info: no version characteristic found");
|
||||
complete();
|
||||
});
|
||||
})
|
||||
.catch(function(error) {
|
||||
error = "connect error: " + error;
|
||||
log(error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var interval;
|
||||
var offset;
|
||||
function transfer(chars, arrayBuffer, imageType, majorVersion, minorVersion) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var controlChar = chars.controlChar;
|
||||
var packetChar = chars.packetChar;
|
||||
log('using dfu version ' + majorVersion + "." + minorVersion);
|
||||
|
||||
var resolved = false;
|
||||
function disconnectHandler() {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
log('disconnected and completed the DFU transfer');
|
||||
return 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...");
|
||||
return new Promise((resolve) => {
|
||||
device.addEventListener("gattserverdisconnected", () => {
|
||||
this.log("disconnected");
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
currentServer.device.addEventListener("gattserverdisconnected", disconnectHandler);
|
||||
|
||||
// Set up receipts
|
||||
interval = Math.floor(arrayBuffer.byteLength / (packetSize * notifySteps));
|
||||
offset = 0;
|
||||
|
||||
if (!controlChar.properties.notify) {
|
||||
var err = "controlChar missing notify property";
|
||||
log(err);
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
log("enabling notifications");
|
||||
controlChar.startNotifications()
|
||||
.then(function() {
|
||||
controlChar.addEventListener('characteristicvaluechanged', handleControl);
|
||||
log("sending imagetype: " + imageType);
|
||||
return controlChar.writeValue(new Uint8Array([OPCODE.START_DFU, imageType]));
|
||||
})
|
||||
.then(function() {
|
||||
log("sent start");
|
||||
|
||||
var softLength = (imageType === ImageType.SoftDevice) ? arrayBuffer.byteLength : 0;
|
||||
var bootLength = (imageType === ImageType.Bootloader) ? arrayBuffer.byteLength : 0;
|
||||
var appLength = (imageType === ImageType.Application) ? arrayBuffer.byteLength : 0;
|
||||
|
||||
var buffer = new ArrayBuffer(12);
|
||||
var view = new DataView(buffer);
|
||||
view.setUint32(0, softLength, LITTLE_ENDIAN);
|
||||
view.setUint32(4, bootLength, LITTLE_ENDIAN);
|
||||
view.setUint32(8, appLength, LITTLE_ENDIAN);
|
||||
|
||||
return packetChar.writeValue(view);
|
||||
})
|
||||
.then(function() {
|
||||
log("sent image size: " + arrayBuffer.byteLength);
|
||||
})
|
||||
.catch(function(error) {
|
||||
error = "start error: " + error;
|
||||
log(error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
function handleControl(event) {
|
||||
var view = event.target.value;
|
||||
|
||||
var opCode = view.getUint8(0);
|
||||
var req_opcode = view.getUint8(1);
|
||||
var resp_code = view.getUint8(2);
|
||||
|
||||
if (opCode === OPCODE.RESPONSE_CODE) {
|
||||
if (resp_code !== 1) {
|
||||
var err = "error from control point notification, resp_code: " + resp_code;
|
||||
log(err);
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
switch(req_opcode) {
|
||||
case OPCODE.START_DFU:
|
||||
case OPCODE.INITIALIZE_DFU_PARAMETERS:
|
||||
if(req_opcode === OPCODE.START_DFU && majorVersion > 6) { // init packet is not used in SDK v6 (so not used in mbed).
|
||||
log('write init packet');
|
||||
controlChar.writeValue(new Uint8Array([OPCODE.INITIALIZE_DFU_PARAMETERS, 0]))
|
||||
.then(function() {
|
||||
return packetChar.writeValue(generateInitPacket());
|
||||
})
|
||||
.then(function() {
|
||||
return controlChar.writeValue(new Uint8Array([OPCODE.INITIALIZE_DFU_PARAMETERS, 1]));
|
||||
})
|
||||
.catch(function(error) {
|
||||
error = "error writing dfu init parameters: " + error;
|
||||
log(error);
|
||||
reject(error);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
log('send packet count');
|
||||
|
||||
var buffer = new ArrayBuffer(3);
|
||||
view = new DataView(buffer);
|
||||
view.setUint8(0, OPCODE.PACKET_RECEIPT_NOTIFICATION_REQUEST);
|
||||
view.setUint16(1, interval, LITTLE_ENDIAN);
|
||||
|
||||
controlChar.writeValue(view)
|
||||
.then(function() {
|
||||
log("sent packet count: " + interval);
|
||||
return controlChar.writeValue(new Uint8Array([OPCODE.RECEIVE_FIRMWARE_IMAGE]));
|
||||
})
|
||||
.then(function() {
|
||||
log("sent receive");
|
||||
return writePacket(packetChar, arrayBuffer, 0);
|
||||
})
|
||||
.catch(function(error) {
|
||||
error = "error sending packet count: " + error;
|
||||
log(error);
|
||||
reject(error);
|
||||
});
|
||||
break;
|
||||
case OPCODE.RECEIVE_FIRMWARE_IMAGE:
|
||||
log('check length');
|
||||
|
||||
controlChar.writeValue(new Uint8Array([OPCODE.REPORT_RECEIVED_IMAGE_SIZE]))
|
||||
.catch(function(error) {
|
||||
error = "error checking length: " + error;
|
||||
log(error);
|
||||
reject(error);
|
||||
});
|
||||
break;
|
||||
case OPCODE.REPORT_RECEIVED_IMAGE_SIZE:
|
||||
var bytesReceived = view.getUint32(3, LITTLE_ENDIAN);
|
||||
log('length: ' + bytesReceived);
|
||||
log('validate...');
|
||||
|
||||
controlChar.writeValue(new Uint8Array([OPCODE.VALIDATE_FIRMWARE]))
|
||||
.catch(function(error) {
|
||||
error = "error validating: " + error;
|
||||
log(error);
|
||||
reject(error);
|
||||
});
|
||||
break;
|
||||
case OPCODE.VALIDATE_FIRMWARE:
|
||||
log('complete, reset...');
|
||||
|
||||
controlChar.writeValue(new Uint8Array([OPCODE.ACTIVATE_IMAGE_AND_RESET]))
|
||||
.then(function() {
|
||||
log('image activated and dfu target reset');
|
||||
// TODO: Remove this when gattserverdisconnected event is implemented and possibly put a timeout in that event handler before resolving
|
||||
setTimeout(function() {
|
||||
if (currentServer && currentServer.connected === true) {
|
||||
currentServer.disconnect();
|
||||
}
|
||||
disconnectHandler();
|
||||
}, 5000);
|
||||
})
|
||||
.catch(function(error) {
|
||||
error = "error resetting: " + error;
|
||||
log(error);
|
||||
reject(error);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
log('unexpected req opCode - ERROR');
|
||||
break;
|
||||
}
|
||||
|
||||
} else if (opCode === OPCODE.PACKET_RECEIPT_NOTIFICATION) {
|
||||
var bytes = view.getUint32(1, LITTLE_ENDIAN);
|
||||
log('transferred: ' + bytes);
|
||||
writePacket(packetChar, arrayBuffer, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function writePacket(packetChar, arrayBuffer, count) {
|
||||
var size = (offset + packetSize > arrayBuffer.byteLength) ? arrayBuffer.byteLength - offset : packetSize;
|
||||
var packet = arrayBuffer.slice(offset, offset + size);
|
||||
var view = new Uint8Array(packet);
|
||||
secureDfu.prototype.connect = function(device) {
|
||||
device.addEventListener("gattserverdisconnected", () => {
|
||||
this.controlChar = null;
|
||||
this.packetChar = null;
|
||||
});
|
||||
|
||||
packetChar.writeValue(view)
|
||||
.then(function() {
|
||||
count ++;
|
||||
offset += packetSize;
|
||||
if (count < interval && offset < arrayBuffer.byteLength) {
|
||||
writePacket(packetChar, arrayBuffer, count);
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.gattConnect = function(device) {
|
||||
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(SERVICE_UUID)
|
||||
.catch(() => {
|
||||
throw new Error("Unable to find DFU service");
|
||||
});
|
||||
})
|
||||
.then(service => {
|
||||
this.log("found DFU service");
|
||||
return service.getCharacteristics();
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.handleNotification = function(event) {
|
||||
let view = event.target.value;
|
||||
|
||||
if (OPERATIONS.RESPONSE.indexOf(view.getUint8(0)) < 0) {
|
||||
throw new Error("Unrecognised control characteristic response notification");
|
||||
}
|
||||
|
||||
let operation = view.getUint8(1);
|
||||
if (this.notifyFns[operation]) {
|
||||
let result = view.getUint8(2);
|
||||
let error = null;
|
||||
|
||||
if (result === 0x01) {
|
||||
let data = new DataView(view.buffer, 3);
|
||||
this.notifyFns[operation].resolve(data);
|
||||
} else if (result === 0x0B) {
|
||||
let 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];
|
||||
}
|
||||
};
|
||||
|
||||
secureDfu.prototype.sendOperation = function(characteristic, operation, buffer) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let size = operation.length;
|
||||
if (buffer) size += buffer.byteLength;
|
||||
|
||||
let value = new Uint8Array(size);
|
||||
value.set(operation);
|
||||
if (buffer) {
|
||||
let data = new Uint8Array(buffer);
|
||||
value.set(data, operation.length);
|
||||
}
|
||||
|
||||
this.notifyFns[operation[0]] = {
|
||||
resolve: resolve,
|
||||
reject: reject
|
||||
};
|
||||
|
||||
characteristic.writeValue(value);
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.sendControl = function(operation, buffer) {
|
||||
return this.sendOperation(this.controlChar, operation, buffer);
|
||||
};
|
||||
|
||||
secureDfu.prototype.transferInit = function(buffer) {
|
||||
return this.transfer(buffer, "init", OPERATIONS.SELECT_COMMAND, OPERATIONS.CREATE_COMMAND);
|
||||
};
|
||||
|
||||
secureDfu.prototype.transferFirmware = function(buffer) {
|
||||
return this.transfer(buffer, "firmware", OPERATIONS.SELECT_DATA, OPERATIONS.CREATE_DATA);
|
||||
};
|
||||
|
||||
secureDfu.prototype.transfer = function(buffer, type, selectType, createType) {
|
||||
return this.sendControl(selectType)
|
||||
.then(response => {
|
||||
|
||||
let maxSize = response.getUint32(0, LITTLE_ENDIAN);
|
||||
let offset = response.getUint32(4, LITTLE_ENDIAN);
|
||||
let 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 = function(bytes) {
|
||||
this.dispatchEvent({
|
||||
type: "progress",
|
||||
object: type,
|
||||
totalBytes: buffer.byteLength,
|
||||
currentBytes: bytes
|
||||
});
|
||||
};
|
||||
this.progress(0);
|
||||
|
||||
return this.transferObject(buffer, createType, maxSize, offset);
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.transferObject = function(buffer, createType, maxSize, offset) {
|
||||
let start = offset - offset % maxSize;
|
||||
let end = Math.min(start + maxSize, buffer.byteLength);
|
||||
|
||||
let view = new DataView(new ArrayBuffer(4));
|
||||
view.setUint32(0, end - start, LITTLE_ENDIAN);
|
||||
|
||||
return this.sendControl(createType, view.buffer)
|
||||
.then(() => {
|
||||
let data = buffer.slice(start, end);
|
||||
return this.transferData(data, start);
|
||||
})
|
||||
.then(() => {
|
||||
return this.sendControl(OPERATIONS.CACULATE_CHECKSUM);
|
||||
})
|
||||
.then(response => {
|
||||
let crc = response.getInt32(4, LITTLE_ENDIAN);
|
||||
let transferred = response.getUint32(0, LITTLE_ENDIAN);
|
||||
let 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");
|
||||
}
|
||||
})
|
||||
.catch(function(error) {
|
||||
error = "writePacket error: " + error;
|
||||
log(error);
|
||||
.then(() => {
|
||||
if (end < buffer.byteLength) {
|
||||
return this.transferObject(buffer, createType, maxSize, offset);
|
||||
} else {
|
||||
this.log("transfer complete");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
addLogger: addLogger,
|
||||
ImageType: ImageType,
|
||||
findDevice: findDevice,
|
||||
writeMode: writeMode,
|
||||
provision: provision
|
||||
};
|
||||
|
||||
secureDfu.prototype.transferData = function(data, offset, start) {
|
||||
start = start || 0;
|
||||
let end = Math.min(start + PACKET_SIZE, data.byteLength);
|
||||
let 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.checkCrc = function(buffer, crc) {
|
||||
if (!this.crc32) {
|
||||
this.log("crc32 not found, skipping CRC check");
|
||||
return true;
|
||||
}
|
||||
|
||||
return crc === this.crc32(new Uint8Array(buffer));
|
||||
};
|
||||
|
||||
secureDfu.prototype.addEventListener = createListenerFn([ "log", "progress" ]);
|
||||
secureDfu.prototype.removeEventListener = removeEventListener;
|
||||
secureDfu.prototype.dispatchEvent = dispatchEvent;
|
||||
|
||||
secureDfu.SERVICE_UUID = SERVICE_UUID;
|
||||
return secureDfu;
|
||||
}));
|
||||
|
||||
191
dist/hex2bin.js
vendored
191
dist/hex2bin.js
vendored
@@ -1,191 +0,0 @@
|
||||
/* @license
|
||||
*
|
||||
* Hex to Bin library
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2016 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
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// https://github.com/umdjs/umd
|
||||
(function(global, factory) {
|
||||
if (typeof exports === 'object') {
|
||||
// CommonJS (Node)
|
||||
module.exports = factory();
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
// AMD
|
||||
define(factory);
|
||||
} else {
|
||||
// Browser global (with support for web workers)
|
||||
global.hex2bin = factory();
|
||||
}
|
||||
}(this, function() {
|
||||
'use strict';
|
||||
|
||||
var RECORD_TYPE = { // I32HEX files use only record types 00, 01, 04, 05.
|
||||
DATA : '00',
|
||||
END_OF_FILE : '01',
|
||||
EXTENDED_SEGMENT_ADDRESS : '02',
|
||||
START_SEGMENT_ADDRESS : '03',
|
||||
EXTENDED_LINEAR_ADDRESS : '04',
|
||||
START_LINEAR_ADDRESS: '05'
|
||||
};
|
||||
|
||||
var loggers = [];
|
||||
function addLogger(loggerFn) {
|
||||
if (typeof loggerFn === "function") {
|
||||
loggers.push(loggerFn);
|
||||
}
|
||||
}
|
||||
function log(message) {
|
||||
loggers.forEach(function(logger) {
|
||||
logger(message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The first record of type extended linear address in the provided hex file will store the start base address of the binary.
|
||||
* Then the first data record's address offset will complete our start address.
|
||||
*/
|
||||
function helperGetBinaryStartAddress(hexLines) {
|
||||
var record;
|
||||
|
||||
do {
|
||||
record = hexLines.shift();
|
||||
} while (record.substr(7, 2) != RECORD_TYPE.EXTENDED_LINEAR_ADDRESS);
|
||||
|
||||
var firstBaseAddress = parseInt(record.substr(9, 4), 16) << 16;
|
||||
|
||||
do {
|
||||
record = hexLines.shift();
|
||||
} while (record.substr(7, 2) != RECORD_TYPE.DATA);
|
||||
var firstDataRecordAddressOffset = parseInt(record.substr(3, 4), 16);
|
||||
|
||||
var startAddress = firstBaseAddress + firstDataRecordAddressOffset;
|
||||
log('start address of binary: ' + startAddress);
|
||||
return startAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* The last record of type extended linear address in the provided hex file will store the base address of the last data segment in the binary.
|
||||
* Then the last record of type data will store the address offset and length of data to be stored here to complete the base address and obtain maxAddress.
|
||||
*/
|
||||
function helperGetBinaryEndAddress(hexLines, maxAddress) {
|
||||
var record;
|
||||
|
||||
do {
|
||||
record = hexLines.pop();
|
||||
} while (record.substr(7, 2) != RECORD_TYPE.DATA);
|
||||
|
||||
var lastDataRecordLength = parseInt(record.substr(1, 2), 16);
|
||||
var lastDataRecordAddressOffset = parseInt(record.substr(3, 4), 16);
|
||||
|
||||
do {
|
||||
record = hexLines.pop();
|
||||
} while (record.substr(7, 2) != RECORD_TYPE.EXTENDED_LINEAR_ADDRESS);
|
||||
|
||||
var lastBaseAddress = parseInt(record.substr(9, 4), 16) << 16;
|
||||
|
||||
var endAddress = lastBaseAddress + lastDataRecordAddressOffset + lastDataRecordLength;
|
||||
if (endAddress > maxAddress) {
|
||||
return helperGetBinaryEndAddress(hexLines, maxAddress);
|
||||
}
|
||||
log('end address of binary: ' + endAddress);
|
||||
return endAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a hex file to a binary blob and returns the data as a buffer.
|
||||
* @param hex - hex file of either SoftDevice, Application or Bootloader to be transferred OTA to the device. Must be a valid Intel 32 hex file.
|
||||
* @param minAddress - the first address (i.e. when updating the SoftDevice we don't send the Master Boot Record).
|
||||
* @param maxAddress - the last address in the devices flash.
|
||||
* @return a buffer of bytes that corresponds to the inputs of this function and is padded with 0xFF in gaps between data segments in the hex file.
|
||||
* Any data in addresses under minAddress will be cut off along with any data in addresses above maxAddress (required by Nordic's DFU protocol).
|
||||
* This is because we are not to send the Master Boot Record (under minAddress) when updating the SoftDevice.
|
||||
* And we are not to send UICR data (above maxAddress) when updating the bootloader or application.
|
||||
*/
|
||||
function convert(hex, minAddress, maxAddress) {
|
||||
maxAddress = maxAddress || 0x80000; // This will always cut off the UICR and the user will not have to every specify this parameter.
|
||||
minAddress = minAddress || 0x0;
|
||||
|
||||
var startAddress = helperGetBinaryStartAddress(hex.split("\n"), minAddress);
|
||||
var endAddress = helperGetBinaryEndAddress(hex.split("\n"), maxAddress);
|
||||
|
||||
if (startAddress < minAddress) {
|
||||
startAddress = minAddress;
|
||||
log('trimmed start address of binary: ' + startAddress);
|
||||
}
|
||||
|
||||
var binarySizeBytes = endAddress - startAddress;
|
||||
|
||||
var buffer = new ArrayBuffer(binarySizeBytes);
|
||||
var view = new Uint8Array(buffer);
|
||||
view.fill(0xFF); // Pad the binary blob with 0xFF as this corresponds to erased 'unwritten' flash.
|
||||
|
||||
var baseAddress;
|
||||
|
||||
var hexLines = hex.split("\n");
|
||||
hexLines.forEach(function(line) {
|
||||
|
||||
switch (line.substr(7, 2)) {
|
||||
|
||||
case RECORD_TYPE.DATA:
|
||||
var length = parseInt(line.substr(1, 2), 16);
|
||||
var addressOffset = parseInt(line.substr(3, 4), 16);
|
||||
var data = line.substr(9, length * 2);
|
||||
for (var i = 0; i < length * 2; i += 2) {
|
||||
var index = (baseAddress + addressOffset) - startAddress + (i / 2);
|
||||
if (index >= 0 && index < binarySizeBytes) { // This cuts off any data below minAddress and above maxAddress.
|
||||
view[index] = parseInt(data.substr(i, 2), 16);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RECORD_TYPE.END_OF_FILE:
|
||||
log('done converting hex file to binary');
|
||||
break;
|
||||
case RECORD_TYPE.EXTENDED_SEGMENT_ADDRESS:
|
||||
throw 'ERROR - invalid hex file - extended segment address is not handled';
|
||||
case RECORD_TYPE.START_SEGMENT_ADDRESS:
|
||||
throw 'ERROR - invalid hex file - start segment address is not handled';
|
||||
case RECORD_TYPE.EXTENDED_LINEAR_ADDRESS:
|
||||
baseAddress = parseInt(line.substr(9, 4), 16) << 16;
|
||||
break;
|
||||
case RECORD_TYPE.START_LINEAR_ADDRESS:
|
||||
log('ignore records of type start linear address');
|
||||
break;
|
||||
default:
|
||||
if (line === '') {
|
||||
break;
|
||||
} else {
|
||||
throw 'ERROR - invalid hex file - unexpected record type in provided hex file';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return {
|
||||
addLogger: addLogger,
|
||||
convert: convert
|
||||
};
|
||||
}));
|
||||
441
dist/secure-dfu.js
vendored
441
dist/secure-dfu.js
vendored
@@ -1,441 +0,0 @@
|
||||
/* @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
|
||||
*
|
||||
* 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
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// https://github.com/umdjs/umd
|
||||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['bleat'], factory);
|
||||
} else if (typeof exports === 'object') {
|
||||
// Node. Does not work with strict CommonJS
|
||||
module.exports = factory(require('bleat').webbluetooth);
|
||||
} else {
|
||||
// Browser globals with support for web workers (root is window)
|
||||
root.SecureDfu = factory(root.navigator.bluetooth);
|
||||
}
|
||||
}(this, function(bluetooth) {
|
||||
"use strict";
|
||||
|
||||
const SERVICE_UUID = 0xFE59;
|
||||
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.
|
||||
};
|
||||
|
||||
function secureDfu(crc32) {
|
||||
this.crc32 = crc32;
|
||||
this.events = {};
|
||||
this.notifyFns = {};
|
||||
this.controlChar = null;
|
||||
this.packetChar = null;
|
||||
}
|
||||
|
||||
function createListenerFn(eventTypes) {
|
||||
return function(type, callback) {
|
||||
if (eventTypes.indexOf(type) < 0) return;
|
||||
if (!this.events[type]) this.events[type] = [];
|
||||
this.events[type].push(callback);
|
||||
};
|
||||
}
|
||||
function removeEventListener(type, callback) {
|
||||
if (!this.events[type]) return;
|
||||
let i = this.events[type].indexOf(callback);
|
||||
if (i >= 0) this.events[type].splice(i, 1);
|
||||
if (this.events[type].length === 0) delete this.events[type];
|
||||
}
|
||||
function dispatchEvent(event) {
|
||||
if (!this.events[event.type]) return;
|
||||
event.target = this;
|
||||
this.events[event.type].forEach(callback => {
|
||||
if (typeof callback === "function") callback(event);
|
||||
});
|
||||
}
|
||||
|
||||
secureDfu.prototype.log = function(message) {
|
||||
this.dispatchEvent({
|
||||
type: "log",
|
||||
message: message
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.progress = function(bytes) {
|
||||
this.dispatchEvent({
|
||||
type: "progress",
|
||||
object: "unknown",
|
||||
totalBytes: 0,
|
||||
currentBytes: bytes
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.requestDevice = function(hiddenDfu, filters) {
|
||||
if (!hiddenDfu && !filters) {
|
||||
filters = [{ services: [SERVICE_UUID] }];
|
||||
}
|
||||
|
||||
return bluetooth.requestDevice({
|
||||
filters: filters,
|
||||
acceptAllDevices: !filters,
|
||||
optionalServices: [SERVICE_UUID]
|
||||
})
|
||||
.then(device => {
|
||||
if (hiddenDfu) {
|
||||
return this.setDfuMode(device);
|
||||
}
|
||||
return device;
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.setDfuMode = function(device) {
|
||||
return this.gattConnect(device)
|
||||
.then(characteristics => {
|
||||
this.log(`found ${characteristics.length} characteristic(s)`);
|
||||
|
||||
let controlChar = characteristics.find(characteristic => {
|
||||
return (characteristic.uuid === CONTROL_UUID);
|
||||
});
|
||||
let packetChar = characteristics.find(characteristic => {
|
||||
return (characteristic.uuid === PACKET_UUID);
|
||||
});
|
||||
|
||||
if (controlChar && packetChar) {
|
||||
return device;
|
||||
}
|
||||
|
||||
let 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));
|
||||
this.sendOperation(buttonChar, OPERATIONS.BUTTON_COMMAND);
|
||||
})
|
||||
.then(() => {
|
||||
this.log("sent dfu mode");
|
||||
return new Promise((resolve) => {
|
||||
device.addEventListener("gattserverdisconnected", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.update = function(device, init, firmware) {
|
||||
if (!device) throw new Error("Device not specified");
|
||||
if (!init) throw new Error("Init not specified");
|
||||
if (!firmware) throw new Error("Firmware not specified");
|
||||
|
||||
return 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...");
|
||||
return new Promise((resolve) => {
|
||||
device.addEventListener("gattserverdisconnected", () => {
|
||||
this.log("disconnected");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.connect = function(device) {
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.gattConnect = function(device) {
|
||||
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(SERVICE_UUID)
|
||||
.catch(() => {
|
||||
throw new Error("Unable to find DFU service");
|
||||
});
|
||||
})
|
||||
.then(service => {
|
||||
this.log("found DFU service");
|
||||
return service.getCharacteristics();
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.handleNotification = function(event) {
|
||||
let view = event.target.value;
|
||||
|
||||
if (OPERATIONS.RESPONSE.indexOf(view.getUint8(0)) < 0) {
|
||||
throw new Error("Unrecognised control characteristic response notification");
|
||||
}
|
||||
|
||||
let operation = view.getUint8(1);
|
||||
if (this.notifyFns[operation]) {
|
||||
let result = view.getUint8(2);
|
||||
let error = null;
|
||||
|
||||
if (result === 0x01) {
|
||||
let data = new DataView(view.buffer, 3);
|
||||
this.notifyFns[operation].resolve(data);
|
||||
} else if (result === 0x0B) {
|
||||
let 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];
|
||||
}
|
||||
};
|
||||
|
||||
secureDfu.prototype.sendOperation = function(characteristic, operation, buffer) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let size = operation.length;
|
||||
if (buffer) size += buffer.byteLength;
|
||||
|
||||
let value = new Uint8Array(size);
|
||||
value.set(operation);
|
||||
if (buffer) {
|
||||
let data = new Uint8Array(buffer);
|
||||
value.set(data, operation.length);
|
||||
}
|
||||
|
||||
this.notifyFns[operation[0]] = {
|
||||
resolve: resolve,
|
||||
reject: reject
|
||||
};
|
||||
|
||||
characteristic.writeValue(value);
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.sendControl = function(operation, buffer) {
|
||||
return this.sendOperation(this.controlChar, operation, buffer);
|
||||
};
|
||||
|
||||
secureDfu.prototype.transferInit = function(buffer) {
|
||||
return this.transfer(buffer, "init", OPERATIONS.SELECT_COMMAND, OPERATIONS.CREATE_COMMAND);
|
||||
};
|
||||
|
||||
secureDfu.prototype.transferFirmware = function(buffer) {
|
||||
return this.transfer(buffer, "firmware", OPERATIONS.SELECT_DATA, OPERATIONS.CREATE_DATA);
|
||||
};
|
||||
|
||||
secureDfu.prototype.transfer = function(buffer, type, selectType, createType) {
|
||||
return this.sendControl(selectType)
|
||||
.then(response => {
|
||||
|
||||
let maxSize = response.getUint32(0, LITTLE_ENDIAN);
|
||||
let offset = response.getUint32(4, LITTLE_ENDIAN);
|
||||
let 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 = function(bytes) {
|
||||
this.dispatchEvent({
|
||||
type: "progress",
|
||||
object: type,
|
||||
totalBytes: buffer.byteLength,
|
||||
currentBytes: bytes
|
||||
});
|
||||
};
|
||||
this.progress(0);
|
||||
|
||||
return this.transferObject(buffer, createType, maxSize, offset);
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.transferObject = function(buffer, createType, maxSize, offset) {
|
||||
let start = offset - offset % maxSize;
|
||||
let end = Math.min(start + maxSize, buffer.byteLength);
|
||||
|
||||
let view = new DataView(new ArrayBuffer(4));
|
||||
view.setUint32(0, end - start, LITTLE_ENDIAN);
|
||||
|
||||
return this.sendControl(createType, view.buffer)
|
||||
.then(() => {
|
||||
let data = buffer.slice(start, end);
|
||||
return this.transferData(data, start);
|
||||
})
|
||||
.then(() => {
|
||||
return this.sendControl(OPERATIONS.CACULATE_CHECKSUM);
|
||||
})
|
||||
.then(response => {
|
||||
let crc = response.getInt32(4, LITTLE_ENDIAN);
|
||||
let transferred = response.getUint32(0, LITTLE_ENDIAN);
|
||||
let 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");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.transferData = function(data, offset, start) {
|
||||
start = start || 0;
|
||||
let end = Math.min(start + PACKET_SIZE, data.byteLength);
|
||||
let 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
secureDfu.prototype.checkCrc = function(buffer, crc) {
|
||||
if (!this.crc32) {
|
||||
this.log("crc32 not found, skipping CRC check");
|
||||
return true;
|
||||
}
|
||||
|
||||
return crc === this.crc32(new Uint8Array(buffer));
|
||||
};
|
||||
|
||||
secureDfu.prototype.addEventListener = createListenerFn([ "log", "progress" ]);
|
||||
secureDfu.prototype.removeEventListener = removeEventListener;
|
||||
secureDfu.prototype.dispatchEvent = dispatchEvent;
|
||||
|
||||
secureDfu.SERVICE_UUID = SERVICE_UUID;
|
||||
return secureDfu;
|
||||
}));
|
||||
@@ -1,265 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Web Bluetooth Secure DFU</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Raleway:400,600" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Raleway', sans-serif;
|
||||
color: #d7ecfb;
|
||||
background-color: #072b44;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
font-weight: 400;
|
||||
}
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
#drop {
|
||||
position: relative;
|
||||
margin: 40px auto;
|
||||
max-width: 680px;
|
||||
background-color: rgba(255, 255, 255, 0.10);
|
||||
padding: 120px 0px 100px;
|
||||
outline: 2px dashed #072b44;
|
||||
outline-offset: -10px;
|
||||
}
|
||||
#drop.hover {
|
||||
outline-offset: -10px;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
#icon {
|
||||
width: 100%;
|
||||
fill: #d7ecfb;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
#file {
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
#label {
|
||||
cursor: pointer;
|
||||
}
|
||||
#label:hover strong {
|
||||
color: #8bb5ba;
|
||||
}
|
||||
#update {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
visibility: hidden;
|
||||
}
|
||||
#button {
|
||||
font-size: 12px;
|
||||
color: inherit;
|
||||
margin: 20px auto;
|
||||
border: 0px;
|
||||
background-color: #1b679c;
|
||||
height: 30px;
|
||||
padding: 0 30px;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
#button:hover {
|
||||
background: #2674ab;
|
||||
}
|
||||
#status {
|
||||
margin: 20px auto;
|
||||
border: 1px solid #d7ecfb;
|
||||
width: 400px;
|
||||
height: 24px;
|
||||
visibility: hidden;
|
||||
}
|
||||
#bar {
|
||||
background: #2674ab;
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
}
|
||||
#transfer {
|
||||
width: 100%;
|
||||
line-height: 24px;
|
||||
margin-top: -24px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Web Bluetooth Secure Device Firmware Update</h1>
|
||||
<div id="drop">
|
||||
<svg id="icon" xmlns="http://www.w3.org/2000/svg" width="50" height="43" viewBox="0 0 50 43">
|
||||
<path d="M48.4 26.5c-.9 0-1.7.7-1.7 1.7v11.6h-43.3v-11.6c0-.9-.7-1.7-1.7-1.7s-1.7.7-1.7 1.7v13.2c0 .9.7 1.7 1.7 1.7h46.7c.9 0 1.7-.7 1.7-1.7v-13.2c0-1-.7-1.7-1.7-1.7zm-24.5 6.1c.3.3.8.5 1.2.5.4 0 .9-.2 1.2-.5l10-11.6c.7-.7.7-1.7 0-2.4s-1.7-.7-2.4 0l-7.1 8.3v-25.3c0-.9-.7-1.7-1.7-1.7s-1.7.7-1.7 1.7v25.3l-7.1-8.3c-.7-.7-1.7-.7-2.4 0s-.7 1.7 0 2.4l10 11.6z"/>
|
||||
</svg>
|
||||
|
||||
<input id="file" type="file" />
|
||||
<label id="label" for="file">
|
||||
<strong>Choose a firmware package</strong>
|
||||
<span>or drag it here</span>
|
||||
</label>
|
||||
|
||||
<div id="update">
|
||||
<button id="button">Update</button>
|
||||
</div>
|
||||
|
||||
<div id="status">
|
||||
<div id="bar"></div>
|
||||
<div id="transfer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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/secure-dfu.js"></script>
|
||||
|
||||
<script>
|
||||
let dropEl = document.getElementById("drop");
|
||||
let fileEl = document.getElementById("file");
|
||||
let updateEl = document.getElementById("update");
|
||||
let labelEl = document.getElementById("label");
|
||||
let statusEl = document.getElementById("status");
|
||||
let transferEl = document.getElementById("transfer");
|
||||
let barEl = document.getElementById("bar");
|
||||
|
||||
let package = null;
|
||||
let manifest = null;
|
||||
|
||||
function updateStatus(state) {
|
||||
labelEl.textContent = state;
|
||||
}
|
||||
|
||||
function updateTransfer(state) {
|
||||
if (!state) {
|
||||
statusEl.style.visibility = "hidden";
|
||||
return;
|
||||
}
|
||||
updateEl.style.visibility = "hidden";
|
||||
statusEl.style.visibility = "visible";
|
||||
barEl.style.width = state.currentBytes / state.totalBytes * 100 + '%';
|
||||
transferEl.textContent = `${state.currentBytes}/${state.totalBytes} bytes written`;
|
||||
}
|
||||
|
||||
function updatePackage(file) {
|
||||
if (!file) return;
|
||||
|
||||
JSZip.loadAsync(file)
|
||||
.then(zipFile => {
|
||||
try {
|
||||
package = zipFile;
|
||||
return zipFile.file("manifest.json").async("string");
|
||||
} catch(e) {
|
||||
throw new Error("Unable to find manifest, is this a proper DFU package?");
|
||||
}
|
||||
})
|
||||
.then(content => {
|
||||
manifest = JSON.parse(content).manifest;
|
||||
updateStatus(`Package: ${file.name}`);
|
||||
updateEl.style.visibility = "visible";
|
||||
})
|
||||
.catch(error => {
|
||||
updateEl.style.visibility = "hidden";
|
||||
statusEl.style.visibility = "hidden";
|
||||
updateStatus(error);
|
||||
});
|
||||
}
|
||||
|
||||
function transfer() {
|
||||
if (!package) return;
|
||||
updateStatus(`Selecting device...`);
|
||||
updateTransfer();
|
||||
let device = null;
|
||||
|
||||
const dfu = new SecureDfu(CRC32.buf);
|
||||
dfu.addEventListener("log", event => {
|
||||
console.log(event.message);
|
||||
});
|
||||
dfu.addEventListener("progress", event => {
|
||||
updateTransfer(event);
|
||||
});
|
||||
|
||||
dfu.requestDevice()
|
||||
.then(selectedDevice => {
|
||||
device = selectedDevice;
|
||||
for (type of ["softdevice", "bootloader", "softdevice_bootloader"]) {
|
||||
if (manifest[type]) {
|
||||
return transferImage(device, dfu, manifest[type])
|
||||
.then(() => {
|
||||
if (manifest.application) {
|
||||
return new Promise((resolve, reject) => {
|
||||
device.addEventListener("gattserverdisconnected", event => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (manifest.application) {
|
||||
return transferImage(device, dfu, manifest.application);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
updateStatus("Update complete!");
|
||||
updateTransfer();
|
||||
})
|
||||
.catch(error => {
|
||||
statusEl.style.visibility = "hidden";
|
||||
updateStatus(error);
|
||||
});
|
||||
}
|
||||
|
||||
function transferImage(device, dfu, manifest) {
|
||||
return dfu.connect(device)
|
||||
.then(() => {
|
||||
return package.file(manifest.dat_file).async("arraybuffer");
|
||||
})
|
||||
.then(data => {
|
||||
updateStatus(`Transferring init: ${manifest.dat_file}...`);
|
||||
return dfu.transferInit(data);
|
||||
})
|
||||
.then(() => {
|
||||
return package.file(manifest.bin_file).async("arraybuffer");
|
||||
})
|
||||
.then(data => {
|
||||
updateStatus(`Transferring firmware: ${manifest.bin_file}...`);
|
||||
return dfu.transferFirmware(data);
|
||||
});
|
||||
}
|
||||
|
||||
fileEl.addEventListener("change", event => {
|
||||
updatePackage(event.target.files[0]);
|
||||
});
|
||||
|
||||
dropEl.addEventListener("drop", event => {
|
||||
updatePackage(event.dataTransfer.files[0]);
|
||||
});
|
||||
|
||||
updateEl.addEventListener("click", transfer);
|
||||
|
||||
["drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave", "drop"].forEach(eventName => {
|
||||
dropEl.addEventListener(eventName, event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
["dragover", "dragenter"].forEach(eventName => {
|
||||
dropEl.addEventListener(eventName, event => {
|
||||
dropEl.classList.add("hover");
|
||||
});
|
||||
});
|
||||
|
||||
["dragleave", "dragend", "drop"].forEach(eventName => {
|
||||
dropEl.addEventListener(eventName, event => {
|
||||
dropEl.classList.remove("hover");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,51 +0,0 @@
|
||||
var dfu = require('../index').dfu;
|
||||
var hex2bin = require('../index').hex2bin;
|
||||
var fs = require('fs');
|
||||
|
||||
var log = console.log;
|
||||
dfu.addLogger(log);
|
||||
hex2bin.addLogger(log);
|
||||
|
||||
var fileMask = "";
|
||||
var fileName = null;
|
||||
|
||||
var deviceType = process.argv[2];
|
||||
if (!deviceType) {
|
||||
deviceType = "nrf51";
|
||||
log("no device-type specified, defaulting to " + deviceType);
|
||||
}
|
||||
|
||||
switch(deviceType) {
|
||||
case "nrf51":
|
||||
fileMask = "firmware/nrf51_app_{0}.hex";
|
||||
break;
|
||||
case "nrf52":
|
||||
fileMask = "firmware/nrf52_app.hex";
|
||||
break;
|
||||
default:
|
||||
log("unknown device-type: " + deviceType);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
dfu.findDevice({ services: [0x180D] })
|
||||
.then(device => {
|
||||
fileName = fileMask.replace("{0}", device.name === "Hi_Rob" ? "bye" : "hi");
|
||||
log("found device: " + device.name);
|
||||
log("using file name: " + fileName);
|
||||
|
||||
return dfu.writeMode(device);
|
||||
})
|
||||
.then(() => dfu.findDevice({ name: "DfuTarg" }))
|
||||
.then(device => {
|
||||
var file = fs.readFileSync(fileName);
|
||||
var hex = file.toString();
|
||||
var buffer = hex2bin.convert(hex);
|
||||
log("file length: " + buffer.byteLength);
|
||||
|
||||
return dfu.provision(device, buffer);
|
||||
})
|
||||
.then(() => process.exit())
|
||||
.catch(error => {
|
||||
log(error);
|
||||
process.exit();
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>web-bluetooth-dfu</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<style>
|
||||
html, body { width: 100%; padding: 0; margin: 0; font-family: sans-serif; }
|
||||
input { font-size: 20px; }
|
||||
button { font-size: 20px; padding: 0 20px; display: block; margin: 20px auto; }
|
||||
.device-type { width: 160px; margin: 20px auto; white-space: nowrap; }
|
||||
#results { height: 300px; overflow: auto; border: 1px solid lightgray; margin: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="device-type">
|
||||
<input type="radio" name="device-type" id="nrf51" checked>
|
||||
<label for="nrf51">nRF51</label>
|
||||
<input type="radio" name="device-type" id="nrf52">
|
||||
<label for="nrf52">nRF52</label>
|
||||
</div>
|
||||
|
||||
<button onclick="setMode()">Set Mode</button>
|
||||
<button onclick="findDFU()">Transfer</button>
|
||||
<button onclick="both()">Both</button>
|
||||
|
||||
<div id="results"></div>
|
||||
|
||||
<script src="../dist/crc16.js"></script>
|
||||
<script src="../dist/dfu.js"></script>
|
||||
<script src="../dist/hex2bin.js"></script>
|
||||
|
||||
<script>
|
||||
var resultsEl = document.getElementById("results");
|
||||
var urlBase = "//thegecko.github.io/web-bluetooth-dfu/firmware/";
|
||||
|
||||
function log(message) {
|
||||
console.log(message);
|
||||
resultsEl.innerText += message + "\n";
|
||||
resultsEl.scrollTop = resultsEl.scrollHeight;
|
||||
}
|
||||
dfu.addLogger(log);
|
||||
hex2bin.addLogger(log);
|
||||
|
||||
function download(url) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.addEventListener("load", function() {
|
||||
resolve(this.responseText);
|
||||
});
|
||||
xhr.open("GET", url);
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
function transfer(device) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var url = "";
|
||||
var deviceType = document.querySelector('input[name="device-type"]:checked').id;
|
||||
|
||||
switch(deviceType) {
|
||||
case "nrf51":
|
||||
url = urlBase + "nrf51_app_{0}.hex";
|
||||
url = url.replace("{0}", device.name === "Hi_Rob" ? "bye" : "hi");
|
||||
break;
|
||||
case "nrf52":
|
||||
url = urlBase + "nrf52_app.hex";
|
||||
break;
|
||||
}
|
||||
|
||||
download(url)
|
||||
.then(hex => {
|
||||
var buffer = hex2bin.convert(hex);
|
||||
log("downloaded length: " + buffer.byteLength);
|
||||
|
||||
// dfu.ImageType.Application is the default value for the third parameter
|
||||
// Specify this if you want to upload a softdevice/bootloader
|
||||
return dfu.provision(device, buffer);
|
||||
})
|
||||
.then(() => {
|
||||
log('dfu complete');
|
||||
resolve();
|
||||
})
|
||||
.catch(error => {
|
||||
log(error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setMode() {
|
||||
dfu.findDevice({ services: [0x180D] })
|
||||
.then(device => dfu.writeMode(device))
|
||||
.catch(log);
|
||||
}
|
||||
|
||||
function findDFU() {
|
||||
dfu.findDevice({ name: "DfuTarg" })
|
||||
.then(device => transfer(device))
|
||||
.catch(log);
|
||||
}
|
||||
|
||||
function both() {
|
||||
dfu.findDevice({ services: [0x180D] })
|
||||
.then(device => dfu.writeMode(device))
|
||||
.then(device => transfer(device))
|
||||
.catch(log);
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -5,8 +5,8 @@ var readline = require("readline");
|
||||
var crc = require("crc-32");
|
||||
var JSZip = require("jszip");
|
||||
var progress = require("progress");
|
||||
var bluetooth = require("bleat").webbluetooth;
|
||||
var secureDfu = require("../index").secure;
|
||||
var Bluetooth = require("webbluetooth").Bluetooth;
|
||||
var Dfu = require("../index");
|
||||
|
||||
var bluetoothDevices = [];
|
||||
var progressBar = null;
|
||||
@@ -16,6 +16,25 @@ function logError(error) {
|
||||
process.exit();
|
||||
}
|
||||
|
||||
function handleDeviceFound(bluetoothDevice, selectFn) {
|
||||
var discovered = bluetoothDevices.some(device => {
|
||||
return (device.id === bluetoothDevice.id);
|
||||
});
|
||||
if (discovered) return;
|
||||
|
||||
if (bluetoothDevices.length === 0) {
|
||||
process.stdin.setRawMode(true);
|
||||
console.log("Select a device to update:");
|
||||
}
|
||||
|
||||
bluetoothDevices.push({ id: bluetoothDevice.id, select: selectFn });
|
||||
console.log(`${bluetoothDevices.length}: ${bluetoothDevice.name}`);
|
||||
}
|
||||
|
||||
var bluetooth = new Bluetooth({
|
||||
deviceFound: handleDeviceFound
|
||||
});
|
||||
|
||||
function getFileName() {
|
||||
return new Promise((resolve) => {
|
||||
if (process.argv[2]) {
|
||||
@@ -61,21 +80,6 @@ function loadFile(fileName) {
|
||||
});
|
||||
}
|
||||
|
||||
function handleDeviceFound(bluetoothDevice, selectFn) {
|
||||
var discovered = bluetoothDevices.some(device => {
|
||||
return (device.id === bluetoothDevice.id);
|
||||
});
|
||||
if (discovered) return;
|
||||
|
||||
if (bluetoothDevices.length === 0) {
|
||||
process.stdin.setRawMode(true);
|
||||
console.log("Select a device to update:");
|
||||
}
|
||||
|
||||
bluetoothDevices.push({ id: bluetoothDevice.id, select: selectFn });
|
||||
console.log(`${bluetoothDevices.length}: ${bluetoothDevice.name}`);
|
||||
}
|
||||
|
||||
function updateFirmware(dfu, package, manifest, device, type) {
|
||||
var init = null;
|
||||
|
||||
@@ -121,7 +125,7 @@ function update() {
|
||||
})
|
||||
.then(content => {
|
||||
manifest = JSON.parse(content).manifest;
|
||||
dfu = new secureDfu(crc.buf);
|
||||
dfu = new Dfu(crc.buf);
|
||||
dfu.addEventListener("progress", event => {
|
||||
if (progressBar && event.object === "firmware") {
|
||||
progressBar.update(event.currentBytes / event.totalBytes);
|
||||
@@ -131,7 +135,7 @@ function update() {
|
||||
console.log("Scanning for DFU devices...");
|
||||
return bluetooth.requestDevice({
|
||||
acceptAllDevices: true,
|
||||
optionalServices: [secureDfu.SERVICE_UUID],
|
||||
optionalServices: [Dfu.SERVICE_UUID],
|
||||
deviceFound: handleDeviceFound
|
||||
});
|
||||
})
|
||||
@@ -144,7 +148,7 @@ function update() {
|
||||
|
||||
console.log("DFU mode set");
|
||||
return bluetooth.requestDevice({
|
||||
filters: [{ services: [secureDfu.SERVICE_UUID] }],
|
||||
filters: [{ services: [Dfu.SERVICE_UUID] }],
|
||||
deviceFound: () => {
|
||||
// Select first device found with correct service
|
||||
return true;
|
||||
@@ -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/secure-dfu.js"></script>
|
||||
<script src="../dist/dfu.js"></script>
|
||||
|
||||
<script>
|
||||
let dropEl = document.getElementById("drop");
|
||||
@@ -1,5 +1,5 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0; url='examples/secure_dfu_web.html'" />
|
||||
<meta http-equiv="refresh" content="0; url='examples/web.html'" />
|
||||
</head>
|
||||
</body>
|
||||
|
||||
14
index.js
14
index.js
@@ -1,13 +1,3 @@
|
||||
require('bleat');
|
||||
var secure = require('./dist/dfu');
|
||||
|
||||
var dfu = require('./dist/dfu');
|
||||
var hex2bin = require('./dist/hex2bin');
|
||||
var crc16 = require('./dist/crc16');
|
||||
var secure = require('./dist/secure-dfu');
|
||||
|
||||
module.exports = {
|
||||
dfu: dfu,
|
||||
hex2bin: hex2bin,
|
||||
crc16: crc16,
|
||||
secure: secure
|
||||
};
|
||||
module.exports = secure;
|
||||
|
||||
13
package.json
13
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web-bluetooth-dfu",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"description": "Device firmware update with Web Bluetooth",
|
||||
"homepage": "https://github.com/thegecko/web-bluetooth-dfu",
|
||||
"author": "Rob Moran <github@thegecko.org>",
|
||||
@@ -12,7 +12,6 @@
|
||||
},
|
||||
"keywords": [
|
||||
"ble",
|
||||
"bleat",
|
||||
"bluetooth",
|
||||
"dfu",
|
||||
"firmware",
|
||||
@@ -21,11 +20,8 @@
|
||||
"web-bluetooth"
|
||||
],
|
||||
"scripts": {
|
||||
"example": "node examples/secure_dfu_node.js",
|
||||
"gulp": "gulp"
|
||||
},
|
||||
"dependencies": {
|
||||
"bleat": "^0.1.3"
|
||||
"gulp": "gulp",
|
||||
"example": "node examples/node.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"crc-32": "^1.0.2",
|
||||
@@ -33,5 +29,8 @@
|
||||
"gulp-eslint": "^3.0.1",
|
||||
"jszip": "^3.1.3",
|
||||
"progress": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"webbluetooth": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user