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
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

View File

@@ -1,6 +1,6 @@
machine: machine:
node: node:
version: 4.0.0 version: 4.7.0
dependencies: dependencies:
post: post:
@@ -8,4 +8,4 @@ dependencies:
test: test:
override: override:
- gulp - gulp

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

653
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().
// This fixes a bug in BlueZ that causes transfers to stall.
var currentServer = null;
var LITTLE_ENDIAN = true; const SERVICE_UUID = 0xFE59;
const CONTROL_UUID = "8ec90001-f315-4f60-9fb8-838830daea50";
var packetSize = 20; const PACKET_UUID = "8ec90002-f315-4f60-9fb8-838830daea50";
var notifySteps = 40;
var serviceUUID = "00001530-1212-efde-1523-785feabcd123"; const LITTLE_ENDIAN = true;
var controlUUID = "00001531-1212-efde-1523-785feabcd123"; const PACKET_SIZE = 20;
var packetUUID = "00001532-1212-efde-1523-785feabcd123";
var versionUUID = "00001534-1212-efde-1523-785feabcd123";
var ImageType = { const OPERATIONS = {
None: 0, CREATE_COMMAND: [0x01, 0x01],
SoftDevice: 1, CREATE_DATA: [0x01, 0x02],
Bootloader: 2, RECEIPT_NOTIFICATIONS: [0x02],
SoftDevice_Bootloader: 3, // Will not work right now CACULATE_CHECKSUM: [0x03],
Application: 4 EXECUTE: [0x04],
}; SELECT_COMMAND: [0x06, 0x01],
SELECT_DATA: [0x06, 0x02],
// TODO: This should be configurable by the user. For now this will work with any of Nordic's SDK examples. RESPONSE: [0x60]
var initPacket = {
device_type: 0xFFFF,
device_rev: 0xFFFF,
app_version: 0xFFFFFFFF,
softdevice_len: 0x0001,
softdevice: 0xFFFE,
crc: 0x0000
};
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
}; };
var loggers = []; const RESPONSE = {
function addLogger(loggerFn) { 0x00: "Invalid code",
if (typeof loggerFn === "function") { 0x01: "Success",
loggers.push(loggerFn); 0x02: "Opcode not supported",
0x03: "Invalid parameter",
0x04: "Insufficient resources",
0x05: "Invalid object",
0x07: "Unsupported type",
0x08: "Operation not permitted",
0x0A: "Operation failed",
0x0B: "Extended error"
};
function secureDfu(crc32) {
this.crc32 = crc32;
this.events = {};
this.notifyFns = {};
this.connected = false;
this.controlChar = null;
this.packetChar = null;
this.buffer = null;
}
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 removeEventListener(type, callback, capture) {
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.requestDevice = function(filters) {
if (!filters) {
filters = [{
services: [SERVICE_UUID]
}];
} }
}
function log(message) {
loggers.forEach(function(logger) {
logger(message);
});
}
function findDevice(filters) {
return bluetooth.requestDevice({ return bluetooth.requestDevice({
filters: [ filters ], filters: filters,
optionalServices: [serviceUUID] optionalServices: [SERVICE_UUID]
});
};
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) {
* Switch to bootloader/DFU mode by writing to the control point of the DFU Service. return this.sendOperation(OPERATIONS.SELECT_COMMAND)
* The DFU Controller is not responsible for disconnecting from the application (DFU Target) after the write. .then(response => {
* 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; let maxSize = response.getUint32(0, LITTLE_ENDIAN);
function disconnectHandler() { let offset = response.getUint32(4, LITTLE_ENDIAN);
if (!resolved) { let crc = response.getInt32(8, LITTLE_ENDIAN);
resolved = true;
log("DFU target issued GAP disconnect and reset into bootloader/DFU mode"); if (offset === buffer.byteLength && this.checkCrc(buffer, crc)) {
resolve(device); this.log("init packet already available, skipping transfer");
} return;
} }
device.addEventListener("gattserverdisconnected", disconnectHandler);
var characteristics = null; this.buffer = buffer;
return this.transferObject(OPERATIONS.CREATE_COMMAND, maxSize, offset);
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);
});
}); });
} }
/** secureDfu.prototype.transferFirmware = function(buffer) {
* Contains basic functionality for performing safety checks on software updates for nRF5 based devices. return this.sendOperation(OPERATIONS.SELECT_DATA)
* Init packet used for pre-checking to ensure the following image is compatible with the device. .then(response => {
* 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) { let maxSize = response.getUint32(0, LITTLE_ENDIAN);
return new Promise(function(resolve, reject) { let offset = response.getUint32(4, LITTLE_ENDIAN);
imageType = imageType || ImageType.Application; let crc = response.getInt32(8, LITTLE_ENDIAN);
initPacket.crc = crc || crc16(arrayBuffer);
var versionChar = null;
connect(device) this.buffer = buffer;
.then(function(chars) { return this.transferObject(OPERATIONS.CREATE_DATA, maxSize, offset);
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) { secureDfu.prototype.transferObject = function(createType, maxSize, offset) {
return new Promise(function(resolve, reject) { let start = offset - offset % maxSize;
var service = null; let end = Math.min(start + maxSize, this.buffer.byteLength);
var controlChar = null;
var packetChar = null;
var versionChar = null;
function complete() { let view = new DataView(new ArrayBuffer(4));
resolve({ view.setUint32(0, end - start, LITTLE_ENDIAN);
controlChar: controlChar,
packetChar: packetChar,
versionChar: versionChar
});
}
device.gatt.connect() return this.sendOperation(createType, view.buffer)
.then(function(gattServer) { .then(response => {
log("connected to device"); let data = this.buffer.slice(start, end);
currentServer = gattServer; return this.transferData(data, start);
// This delay is needed because BlueZ needs time to update it's cache. })
return new Promise(function(resolve, reject) { .then(() => {
setTimeout(resolve, 2000); return this.sendOperation(OPERATIONS.CACULATE_CHECKSUM);
}); })
}) .then(response => {
.then(function() { let crc = response.getInt32(4, LITTLE_ENDIAN);
return currentServer.getPrimaryService(serviceUUID); let transferred = response.getUint32(0, LITTLE_ENDIAN);
}) let data = this.buffer.slice(0, transferred);
.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; if (this.checkCrc(data, crc)) {
var offset; this.log(`written ${transferred} bytes`);
function transfer(chars, arrayBuffer, imageType, majorVersion, minorVersion) { offset = transferred;
return new Promise(function(resolve, reject) { return this.sendOperation(OPERATIONS.EXECUTE);
var controlChar = chars.controlChar; } else {
var packetChar = chars.packetChar; this.log("object failed to validate");
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) { .then(() => {
error = "writePacket error: " + error; if (end < this.buffer.byteLength) {
log(error); return this.transferObject(createType, maxSize, offset);
} else {
this.log("transfer complete");
}
}); });
} }
return { secureDfu.prototype.transferData = function(data, offset, start) {
addLogger: addLogger, start = start || 0;
ImageType: ImageType, let end = start + PACKET_SIZE;
findDevice: findDevice, let packet = data.slice(start, end);
writeMode: writeMode,
provision: provision 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();
}
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(); process.exit();
}); }
process.stdin.setEncoding('utf8');
process.stdin.on('readable', () => {
var input = process.stdin.read();
if (input === '\u0003') {
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

@@ -7,4 +7,4 @@ gulp.task('lint', function() {
.pipe(jshint.reporter('default')); .pipe(jshint.reporter('default'));
}); });
gulp.task('default', ['lint']); gulp.task('default', ['lint']);

View File

@@ -2,4 +2,4 @@
<head> <head>
<meta http-equiv="refresh" content="0; url='example_web.html'" /> <meta http-equiv="refresh" content="0; url='example_web.html'" />
</head> </head>
</body> </body>

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",