Initial update to version 13 with secure DFU and node example

This commit is contained in:
Rob Moran
2017-05-08 01:50:33 +01:00
parent 20adcf1e15
commit b224d57e55
11 changed files with 357 additions and 725 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,2 @@
.DS_Store .DS_Store
node_modules node_modules
cert
app.js

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015 Rob Moran Copyright (c) 2017 Rob Moran
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,6 +1,6 @@
machine: machine:
node: node:
version: 4.0.0 version: 4.7.0
dependencies: dependencies:
post: post:

64
dist/crc16.js vendored
View File

@@ -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);
};
}));

683
dist/dfu.js vendored
View File

@@ -1,13 +1,13 @@
/* @license /* @license
* *
* Device firmware update with Web Bluetooth * Secure device firmware update with Web Bluetooth
* *
* Protocol from: * Protocol from:
* http://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk52.v0.9.2/bledfu_transport.html?cp=4_0_2_4_2_4 * http://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk5.v13.0.0/lib_dfu_transport_ble.html
* *
* The MIT License (MIT) * The MIT License (MIT)
* *
* Copyright (c) 2016 Rob Moran * Copyright (c) 2017 Rob Moran
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -32,439 +32,288 @@
(function (root, factory) { (function (root, factory) {
if (typeof define === 'function' && define.amd) { if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module. // AMD. Register as an anonymous module.
define(['es6-promise', 'bleat', 'crc16'], factory); define(['bleat'], factory);
} else if (typeof exports === 'object') { } else if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS // Node. Does not work with strict CommonJS
module.exports = factory(Promise, require('bleat').webbluetooth, require('./crc16')); module.exports = factory(require('bleat').webbluetooth);
} else { } else {
// Browser globals with support for web workers (root is window) // 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"; "use strict";
// Make server a global variable (initialized in connect(). const SERVICE_UUID = 0xFE59;
// This fixes a bug in BlueZ that causes transfers to stall. const CONTROL_UUID = "8ec90001-f315-4f60-9fb8-838830daea50";
var currentServer = null; const PACKET_UUID = "8ec90002-f315-4f60-9fb8-838830daea50";
var LITTLE_ENDIAN = true; const LITTLE_ENDIAN = true;
const PACKET_SIZE = 20;
var packetSize = 20; const OPERATIONS = {
var notifySteps = 40; CREATE_COMMAND: [0x01, 0x01],
CREATE_DATA: [0x01, 0x02],
var serviceUUID = "00001530-1212-efde-1523-785feabcd123"; RECEIPT_NOTIFICATIONS: [0x02],
var controlUUID = "00001531-1212-efde-1523-785feabcd123"; CACULATE_CHECKSUM: [0x03],
var packetUUID = "00001532-1212-efde-1523-785feabcd123"; EXECUTE: [0x04],
var versionUUID = "00001534-1212-efde-1523-785feabcd123"; SELECT_COMMAND: [0x06, 0x01],
SELECT_DATA: [0x06, 0x02],
var ImageType = { RESPONSE: [0x60]
None: 0,
SoftDevice: 1,
Bootloader: 2,
SoftDevice_Bootloader: 3, // Will not work right now
Application: 4
}; };
// TODO: This should be configurable by the user. For now this will work with any of Nordic's SDK examples. const RESPONSE = {
var initPacket = { 0x00: "Invalid code",
device_type: 0xFFFF, 0x01: "Success",
device_rev: 0xFFFF, 0x02: "Opcode not supported",
app_version: 0xFFFFFFFF, 0x03: "Invalid parameter",
softdevice_len: 0x0001, 0x04: "Insufficient resources",
softdevice: 0xFFFE, 0x05: "Invalid object",
crc: 0x0000 0x07: "Unsupported type",
0x08: "Operation not permitted",
0x0A: "Operation failed",
0x0B: "Extended error"
}; };
var OPCODE = { function secureDfu(crc32) {
RESERVED: 0, this.crc32 = crc32;
START_DFU: 1, this.events = {};
INITIALIZE_DFU_PARAMETERS: 2, this.notifyFns = {};
RECEIVE_FIRMWARE_IMAGE: 3, this.connected = false;
VALIDATE_FIRMWARE: 4, this.controlChar = null;
ACTIVATE_IMAGE_AND_RESET: 5, this.packetChar = null;
RESET_SYSTEM: 6, this.buffer = null;
REPORT_RECEIVED_IMAGE_SIZE: 7,
PACKET_RECEIPT_NOTIFICATION_REQUEST: 8,
RESPONSE_CODE: 16,
PACKET_RECEIPT_NOTIFICATION: 17
};
var loggers = [];
function addLogger(loggerFn) {
if (typeof loggerFn === "function") {
loggers.push(loggerFn);
} }
function createListenerFn(eventTypes) {
return function(type, callback, capture) {
if (eventTypes.indexOf(type) < 0) return;
if (!this.events[type]) this.events[type] = [];
this.events[type].push(callback);
};
} }
function log(message) { function removeEventListener(type, callback, capture) {
loggers.forEach(function(logger) { if (!this.events[type]) return;
logger(message); 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);
}); });
} }
function findDevice(filters) { secureDfu.prototype.log = function(message) {
this.dispatchEvent({
type: "log",
message: message
});
};
secureDfu.prototype.requestDevice = function(filters) {
if (!filters) {
filters = [{
services: [SERVICE_UUID]
}];
}
return bluetooth.requestDevice({ return bluetooth.requestDevice({
filters: [ filters ], filters: filters,
optionalServices: [serviceUUID] optionalServices: [SERVICE_UUID]
}); });
}
/**
* 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);
}
}
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);
});
});
}
/**
* 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;
}
function provision(device, arrayBuffer, imageType, crc) {
return new Promise(function(resolve, reject) {
imageType = imageType || ImageType.Application;
initPacket.crc = crc || crc16(arrayBuffer);
var versionChar = null;
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);
});
} 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;
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, reject) {
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(error) {
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');
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);
packetChar.writeValue(view)
.then(function() {
count ++;
offset += packetSize;
if (count < interval && offset < arrayBuffer.byteLength) {
writePacket(packetChar, arrayBuffer, count);
}
})
.catch(function(error) {
error = "writePacket error: " + error;
log(error);
});
}
return {
addLogger: addLogger,
ImageType: ImageType,
findDevice: findDevice,
writeMode: writeMode,
provision: provision
}; };
secureDfu.prototype.connect = function(device) {
let service = null;
device.addEventListener("gattserverdisconnected", event => {
this.connected = false;
this.controlChar = null;
this.packetChar = null;
this.buffer = null;
this.log("disconnected");
});
return device.gatt.connect()
.then(gattServer => {
this.log("connected to gatt server");
return gattServer.getPrimaryService(SERVICE_UUID);
})
.then(primaryService => {
this.log("found DFU service");
service = primaryService;
return service.getCharacteristic(CONTROL_UUID);
})
.then(characteristic => {
this.log("found control characteristic");
if (!characteristic.properties.notify) {
throw new Error("control characterisitc does not allow notifications");
}
this.controlChar = characteristic;
return characteristic.startNotifications();
})
.then(() => {
this.log("enabled control notifications");
this.controlChar.addEventListener("characteristicvaluechanged", this.handleNotification.bind(this));
return service.getCharacteristic(PACKET_UUID);
})
.then(characteristic => {
this.log("found packet characteristic");
this.packetChar = characteristic;
this.connected = true;
});
};
secureDfu.prototype.handleNotification = function(event) {
let view = event.target.value;
if (view.getUint8(0) !== OPERATIONS.RESPONSE[0]) {
throw new Error("unrecognised control characteristic response notification");
}
let operation = view.getUint8(1);
if (this.notifyFns[operation]) {
let result = view.getUint8(2);
let response = RESPONSE[result];
if (result === 0x01) {
let data = new DataView(view.buffer, 3);
this.notifyFns[operation].resolve(data);
} else {
this.log(`notify: ${response}`);
this.notifyFns[operation].reject(response);
}
delete this.notifyFns[operation];
}
};
secureDfu.prototype.sendOperation = function(operation, buffer) {
return new Promise((resolve, reject) => {
if (!this.connected) throw new Error("device not connected");
if (!this.controlChar) throw new Error("control characteristic not found");
if (!this.packetChar) throw new Error("packet characteristic not found");
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
};
this.controlChar.writeValue(value);
});
}
secureDfu.prototype.transferInit = function(buffer) {
return this.sendOperation(OPERATIONS.SELECT_COMMAND)
.then(response => {
let maxSize = response.getUint32(0, LITTLE_ENDIAN);
let offset = response.getUint32(4, LITTLE_ENDIAN);
let crc = response.getInt32(8, LITTLE_ENDIAN);
if (offset === buffer.byteLength && this.checkCrc(buffer, crc)) {
this.log("init packet already available, skipping transfer");
return;
}
this.buffer = buffer;
return this.transferObject(OPERATIONS.CREATE_COMMAND, maxSize, offset);
});
}
secureDfu.prototype.transferFirmware = function(buffer) {
return this.sendOperation(OPERATIONS.SELECT_DATA)
.then(response => {
let maxSize = response.getUint32(0, LITTLE_ENDIAN);
let offset = response.getUint32(4, LITTLE_ENDIAN);
let crc = response.getInt32(8, LITTLE_ENDIAN);
this.buffer = buffer;
return this.transferObject(OPERATIONS.CREATE_DATA, maxSize, offset);
});
}
secureDfu.prototype.transferObject = function(createType, maxSize, offset) {
let start = offset - offset % maxSize;
let end = Math.min(start + maxSize, this.buffer.byteLength);
let view = new DataView(new ArrayBuffer(4));
view.setUint32(0, end - start, LITTLE_ENDIAN);
return this.sendOperation(createType, view.buffer)
.then(response => {
let data = this.buffer.slice(start, end);
return this.transferData(data, start);
})
.then(() => {
return this.sendOperation(OPERATIONS.CACULATE_CHECKSUM);
})
.then(response => {
let crc = response.getInt32(4, LITTLE_ENDIAN);
let transferred = response.getUint32(0, LITTLE_ENDIAN);
let data = this.buffer.slice(0, transferred);
if (this.checkCrc(data, crc)) {
this.log(`written ${transferred} bytes`);
offset = transferred;
return this.sendOperation(OPERATIONS.EXECUTE);
} else {
this.log("object failed to validate");
}
})
.then(() => {
if (end < this.buffer.byteLength) {
return this.transferObject(createType, maxSize, offset);
} else {
this.log("transfer complete");
}
});
}
secureDfu.prototype.transferData = function(data, offset, start) {
start = start || 0;
let end = start + PACKET_SIZE;
let packet = data.slice(start, end);
return this.packetChar.writeValue(packet)
.then(() => {
this.dispatchEvent({
type: "progress",
currentBytes: offset + end,
totalBytes: this.buffer.byteLength
});
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;
return secureDfu;
})); }));

191
dist/hex2bin.js vendored
View File

@@ -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
};
}));

View File

@@ -1,51 +1,98 @@
var dfu = require('./index').dfu;
var hex2bin = require('./index').hex2bin;
var fs = require('fs'); var fs = require('fs');
var bluetooth = require("bleat").webbluetooth;
var crc = require("crc-32");
var secureDfu = require("./index");
var progress = require('progress');
var bluetoothDevices = [];
var log = console.log; var dat = fs.readFileSync("test_images_update_nrf52832/dfu_test_app_hrm_s132/nrf52832_xxaa.dat");
dfu.addLogger(log); var bin = fs.readFileSync("test_images_update_nrf52832/dfu_test_app_hrm_s132/nrf52832_xxaa.bin");
hex2bin.addLogger(log);
var fileMask = ""; function logError(error) {
var fileName = null; console.log(error);
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(); process.exit();
} }
dfu.findDevice({ services: [0x180D] }) process.stdin.setEncoding('utf8');
.then(device => { process.stdin.on('readable', () => {
fileName = fileMask.replace("{0}", device.name === "Hi_Rob" ? "bye" : "hi"); var input = process.stdin.read();
log("found device: " + device.name); if (input === '\u0003') {
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(); process.exit();
} else {
var index = parseInt(input);
if (index && index <= bluetoothDevices.length) {
process.stdin.setRawMode(false);
selectDevice(index - 1);
}
}
}); });
function toArrayBuffer(buffer) {
var ab = new ArrayBuffer(buffer.length);
var view = new Uint8Array(ab);
for (var i = 0; i < buffer.length; ++i) {
view[i] = buffer[i];
}
return ab;
}
function selectDevice(index) {
var bar = null;
var device = bluetoothDevices[index];
var dfu = new secureDfu(crc.buf);
dfu.addEventListener("progress", event => {
if (bar) bar.update(event.currentBytes / event.totalBytes);
});
console.log();
dfu.connect(device)
.then(() => {
process.stdout.write("Transferring init packet...");
return dfu.transferInit(toArrayBuffer(dat));
})
.then(data => {
console.log("Done");
var data = toArrayBuffer(bin);
bar = new progress('Transferring firmware [:bar] :percent :etas', {
complete: '=',
incomplete: ' ',
width: 20,
total: data.byteLength
});
return dfu.transferFirmware(data);
})
.then(() => {
console.log("\nComplete!");
process.exit();
})
.catch(logError);
}
function handleDeviceFound(bluetoothDevice) {
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(bluetoothDevice);
console.log(bluetoothDevices.length + ": " + bluetoothDevice.name);
}
console.log("Scanning for DFU devices...");
bluetooth.requestDevice({
filters: [{ services: [0xFE59] }],
deviceFound: handleDeviceFound
})
.then(() => {
if (bluetoothDevices.length === 0) {
console.log("no devices found");
process.exit();
}
})
.catch(logError);

View File

@@ -1,11 +1,2 @@
require('bleat'); require('bleat');
module.exports = require('./dist/dfu');
var dfu = require('./dist/dfu');
var hex2bin = require('./dist/hex2bin');
var crc16 = require('./dist/crc16');
module.exports = {
dfu: dfu,
hex2bin: hex2bin,
crc16: crc16
};

View File

@@ -1,9 +1,9 @@
{ {
"name": "web-bluetooth-dfu", "name": "web-bluetooth-dfu",
"version": "0.1.0", "version": "0.2.0",
"description": "Device firmware update with Web Bluetooth", "description": "Device firmware update with Web Bluetooth",
"homepage": "https://github.com/thegecko/web-bluetooth-dfu", "homepage": "https://github.com/thegecko/web-bluetooth-dfu",
"author": "Rob Moran <rob@thegecko.org>", "author": "Rob Moran <github@thegecko.org>",
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -21,7 +21,9 @@
"web-bluetooth" "web-bluetooth"
], ],
"dependencies": { "dependencies": {
"bleat": "^0.1.0" "bleat": "^0.1.0",
"crc-32": "^1.0.2",
"progress": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"gulp": "^3.9.1", "gulp": "^3.9.1",