Merge remote-tracking branch 'refs/remotes/thegecko/master'

This commit is contained in:
Michael Dietz
2016-02-24 15:57:31 +01:00
9 changed files with 10075 additions and 136 deletions

View File

@@ -1,29 +1,31 @@
# Web Bluetooth DFU # Web Bluetooth DFU
Device firmware update with Web Bluetooth Device firmware update with Web Bluetooth
Update device firmware via [Web Bluetooth](https://webbluetoothcg.github.io/web-bluetooth/) using the protocol here: Update device firmware via [Web Bluetooth](https://webbluetoothcg.github.io/web-bluetooth/) following the protocol here:
[http://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v8.x.x/doc/8.1.0/s110/html/a00103.html](http://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v8.x.x/doc/8.1.0/s110/html/a00103.html) http://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk52.v0.9.2/bledfu_transport.html?cp=4_0_2_4_2_4
## Device Configuration ## Device Configuration
Put this firmware onto an [nrf51822](https://www.nordicsemi.com/eng/Products/nRF51-DK): You will need an [nRF51](https://www.nordicsemi.com/Products/nRF51-DK) or [nRF52](https://www.nordicsemi.com/Products/Bluetooth-Smart-Bluetooth-low-energy/nRF52-DK) development kit, flashed with the appropriate image:
[NRF51822_DFU_Test_BOOT.hex](https://thegecko.github.io/web-bluetooth-dfu/firmware/NRF51822_DFU_Test_BOOT.hex) [nrf51_boot_s110.hex](https://thegecko.github.io/web-bluetooth-dfu/firmware/nrf51_boot_s110.hex)
Then reset the device. [nrf52_boot_s132.hex](https://thegecko.github.io/web-bluetooth-dfu/firmware/nrf52_boot_s132.hex)
## Web Example ## Web Example
Open this site in a Web Bluetooth enabled browser: Open this site in a [Web Bluetooth](https://webbluetoothcg.github.io/web-bluetooth/) enabled browser:
[https://thegecko.github.io/web-bluetooth-dfu/](https://thegecko.github.io/web-bluetooth-dfu/) [https://thegecko.github.io/web-bluetooth-dfu/](https://thegecko.github.io/web-bluetooth-dfu/)
## Node Example ## Node Example
Install the npm dependencies and run. Clone this repository, install the npm dependencies and execute.
``` ```
npm install npm install
node example_node node example_node <device-type>
``` ```
Where ```<device-type>``` is one of ```nrf51``` or ```nrf52```.

293
dist/dfu.js vendored
View File

@@ -1,10 +1,10 @@
/* @license /* @license
* *
* Device firmware update with Web Bluetooth * Device firmware update with Web Bluetooth
* Version: 0.0.1 * Version: 0.0.2
* *
* Protocol from: * Protocol from:
* http://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v8.x.x/doc/8.1.0/s110/html/a00103.html * http://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk52.v0.9.2/bledfu_transport.html?cp=4_0_2_4_2_4
* *
* The MIT License (MIT) * The MIT License (MIT)
* *
@@ -44,6 +44,8 @@
}(this, function(Promise, bluetooth) { }(this, function(Promise, bluetooth) {
"use strict"; "use strict";
var LITTLE_ENDIAN = true;
var packetSize = 20; var packetSize = 20;
var notifySteps = 40; var notifySteps = 40;
@@ -60,11 +62,29 @@
Application: 4 Application: 4
}; };
var littleEndian = (function() { // TODO: This should be configurable by the user. For now this will work with any of Nordic's SDK examples.
var buffer = new ArrayBuffer(2); var initPacket = {
new DataView(buffer).setInt16(0, 256, true); device_type: 0xFFFF,
return new Int16Array(buffer)[0] === 256; 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 = []; var loggers = [];
function addLogger(loggerFn) { function addLogger(loggerFn) {
@@ -85,28 +105,41 @@
}); });
} }
/**
* 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) { function writeMode(device) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
/* /*
// Disconnect event currently not implemented // TODO: once disconnect event is implemented we should resolve in its callback...
device.addEventListener("gattserverdisconnected", () => { device.addEventListener("gattserverdisconnected", () => {
log("modeData written"); log("DFU Target issued GAP Disconnect and reset into Bootloader/DFU mode.");
resolve(); resolve(device);
}); });
*/ */
var characteristics = null;
connect(device) connect(device)
.then(chars => { .then(chars => {
log("writing modeData..."); log("enabling notifications");
chars.controlChar.writeValue(new Uint8Array([1])); characteristics = chars;
return characteristics.controlChar.startNotifications();
})
.then(() => {
log("writing modeData");
return characteristics.controlChar.writeValue(new Uint8Array([1, 4]));
})
.then(() => {
log("modeData written");
// Hack to gracefully disconnect without disconnect event // Hack to gracefully disconnect without disconnect event
setTimeout(() => { setTimeout(() => {
chars.server.disconnect(); characteristics.server.disconnect();
setTimeout(() => { resolve(device);
log("modeData written"); }, 2000);
resolve(device);
}, 3000);
}, 3000);
}) })
.catch(error => { .catch(error => {
error = "writeMode error: " + error; error = "writeMode error: " + error;
@@ -116,22 +149,44 @@
}); });
} }
/**
* 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) { function provision(device, arrayBuffer, imageType) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var versionChar = null;
imageType = imageType || ImageType.Application; imageType = imageType || ImageType.Application;
connect(device) connect(device)
.then(chars => { .then(chars => {
if (chars.versionChar) { versionChar = chars.versionChar;
return chars.versionChar.readValue() if (versionChar) { // Older DFU implementations (from older Nordic SDKs < 7.0) have no DFU Version characteristic.
return versionChar.readValue()
.then(data => { .then(data => {
console.log('read versionChar');
var view = new DataView(data); var view = new DataView(data);
var major = view.getUint8(0); var major = view.getUint8(0);
var minor = view.getUint8(1); var minor = view.getUint8(1);
return transfer(chars, arrayBuffer, imageType, major, minor); return transfer(chars, arrayBuffer, imageType, major, minor);
}); });
} else { } else {
// Default to version 6.0 // Default to version 6.0 (mbed).
return transfer(chars, arrayBuffer, imageType, 6, 0); return transfer(chars, arrayBuffer, imageType, 6, 0);
} }
}) })
@@ -183,12 +238,14 @@
log("found packet characteristic"); log("found packet characteristic");
packetChar = characteristic; packetChar = characteristic;
service.getCharacteristic(versionUUID) service.getCharacteristic(versionUUID)
// Older DFU implementations (from older Nordic SDKs) have no DFU Version characteristic. So this may fail.
.then(characteristic => { .then(characteristic => {
log("found version characteristic"); log("found version characteristic");
versionChar = characteristic; versionChar = characteristic;
complete(); complete();
}) })
.catch(error => { .catch(error => {
log("info: no version characteristic found");
complete(); complete();
}); });
}) })
@@ -213,9 +270,9 @@
offset = 0; offset = 0;
if (!controlChar.properties.notify) { if (!controlChar.properties.notify) {
var error = "controlChar missing notify property"; var err = "controlChar missing notify property";
log(error); log(err);
return reject(error); return reject(err);
} }
log("enabling notifications"); log("enabling notifications");
@@ -223,7 +280,7 @@
.then(() => { .then(() => {
controlChar.addEventListener('characteristicvaluechanged', handleControl); controlChar.addEventListener('characteristicvaluechanged', handleControl);
log("sending imagetype: " + imageType); log("sending imagetype: " + imageType);
return controlChar.writeValue(new Uint8Array([1, imageType])); return controlChar.writeValue(new Uint8Array([OPCODE.START_DFU, imageType]));
}) })
.then(() => { .then(() => {
log("sent start"); log("sent start");
@@ -234,15 +291,14 @@
var buffer = new ArrayBuffer(12); var buffer = new ArrayBuffer(12);
var view = new DataView(buffer); var view = new DataView(buffer);
view.setUint32(0, softLength, littleEndian); view.setUint32(0, softLength, LITTLE_ENDIAN);
view.setUint32(4, bootLength, littleEndian); view.setUint32(4, bootLength, LITTLE_ENDIAN);
view.setUint32(8, appLength, littleEndian); view.setUint32(8, appLength, LITTLE_ENDIAN);
// Set firmware length
return packetChar.writeValue(view); return packetChar.writeValue(view);
}) })
.then(() => { .then(() => {
log("sent buffer size: " + arrayBuffer.byteLength); log("sent image size: " + arrayBuffer.byteLength);
}) })
.catch(error => { .catch(error => {
error = "start error: " + error; error = "start error: " + error;
@@ -253,103 +309,112 @@
function handleControl(event) { function handleControl(event) {
var data = event.target.value; var data = event.target.value;
var view = new DataView(data); var view = new DataView(data);
var opCode = view.getUint8(0); var opCode = view.getUint8(0);
var req_opcode = view.getUint8(1);
var resp_code = view.getUint8(2);
if (opCode === 16) { // response if (opCode === OPCODE.RESPONSE_CODE) {
var resp_code = view.getUint8(2);
if (resp_code !== 1) { if (resp_code !== 1) {
var error = "error from control: " + resp_code; var err = "error from control point notification, resp_code: " + resp_code;
log(error); log(err);
return reject(error); return reject(err);
} }
var req_opcode = view.getUint8(1); switch(req_opcode) {
if (req_opcode === 1 && majorVersion > 6) { case OPCODE.START_DFU:
log('write null init packet'); 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(() => {
return packetChar.writeValue(generateInitPacket());
})
.then(() => {
return controlChar.writeValue(new Uint8Array([OPCODE.INITIALIZE_DFU_PARAMETERS, 1]));
})
.catch(error => {
error = "error writing dfu init parameters: " + error;
log(error);
reject(error);
});
break;
}
controlChar.writeValue(new Uint8Array([2,0])) log('send packet count');
.then(() => {
return packetChar.writeValue(new Uint8Array([0]));
})
.then(() => {
return controlChar.writeValue(new Uint8Array([2,1]));
})
.catch(error => {
error = "error writing init: " + error;
log(error);
reject(error);
});
} else if (req_opcode === 1 || req_opcode === 2) { var buffer = new ArrayBuffer(3);
log('complete, send packet count'); view = new DataView(buffer);
view.setUint8(0, OPCODE.PACKET_RECEIPT_NOTIFICATION_REQUEST);
view.setUint16(1, interval, LITTLE_ENDIAN);
var buffer = new ArrayBuffer(3); controlChar.writeValue(view)
view = new DataView(buffer); .then(() => {
view.setUint8(0, 8); log("sent packet count: " + interval);
view.setUint16(1, interval, littleEndian); return controlChar.writeValue(new Uint8Array([OPCODE.RECEIVE_FIRMWARE_IMAGE]));
})
.then(() => {
log("sent receive");
return writePacket(packetChar, arrayBuffer, 0);
})
.catch(error => {
error = "error sending packet count: " + error;
log(error);
reject(error);
});
break;
case OPCODE.RECEIVE_FIRMWARE_IMAGE:
log('check length');
controlChar.writeValue(view) controlChar.writeValue(new Uint8Array([OPCODE.REPORT_RECEIVED_IMAGE_SIZE]))
.then(() => { .catch(error => {
log("sent packet count: " + interval); error = "error checking length: " + error;
return controlChar.writeValue(new Uint8Array([3])); log(error);
}) reject(error);
.then(() => { });
log("sent receive"); break;
return writePacket(packetChar, arrayBuffer, 0); case OPCODE.REPORT_RECEIVED_IMAGE_SIZE:
}) var bytesReceived = view.getUint32(3, LITTLE_ENDIAN);
.catch(error => { log('length: ' + bytesReceived);
error = "error sending packet count: " + error; log('validate...');
log(error);
reject(error);
});
} else if (req_opcode === 3) { controlChar.writeValue(new Uint8Array([OPCODE.VALIDATE_FIRMWARE]))
log('complete, check length'); .catch(error => {
error = "error validating: " + error;
controlChar.writeValue(new Uint8Array([7])) log(error);
.catch(error => { reject(error);
error = "error checking length: " + error; });
log(error); break;
reject(error); case OPCODE.VALIDATE_FIRMWARE:
}); log('complete, reset...');
} else if (req_opcode === 7) {
var byteCount = view.getUint32(3, littleEndian);
log('length: ' + byteCount);
log('complete, validate...');
controlChar.writeValue(new Uint8Array([4]))
.catch(error => {
error = "error validating: " + error;
log(error);
reject(error);
});
} else if (req_opcode === 4) {
log('complete, reset...');
/* /*
// Disconnect event currently not implemented // TODO: Resolve in disconnect event handler when implemented in Web Bluetooth API.
controlChar.service.device.addEventListener("gattserverdisconnected", () => { controlChar.service.device.addEventListener("gattserverdisconnected", () => {
resolve();
});
*/
controlChar.writeValue(new Uint8Array([5]))
.then(() => {
// Hack to gracefully disconnect without disconnect event
setTimeout(() => {
chars.server.disconnect();
resolve(); resolve();
}, 3000); });
}) */
.catch(error => { controlChar.writeValue(new Uint8Array([OPCODE.ACTIVATE_IMAGE_AND_RESET]))
error = "error resetting: " + error; .then(() => {
log(error); log('image activated and dfu target reset');
reject(error); // Hack to gracefully disconnect without disconnect event
}); setTimeout(() => {
chars.server.disconnect();
resolve();
}, 2000);
})
.catch(error => {
error = "error resetting: " + error;
log(error);
reject(error);
});
break;
default:
log('unexpected req opCode - ERROR');
break;
} }
} else if (opCode === 17) { } else if (opCode === OPCODE.PACKET_RECEIPT_NOTIFICATION) {
var bytes = view.getUint32(1, littleEndian); var bytes = view.getUint32(1, LITTLE_ENDIAN);
log('transferred: ' + bytes); log('transferred: ' + bytes);
writePacket(packetChar, arrayBuffer, 0); writePacket(packetChar, arrayBuffer, 0);
} }

View File

@@ -5,12 +5,30 @@ var fs = require('fs');
var log = console.log; var log = console.log;
dfu.addLogger(log); dfu.addLogger(log);
var fileMask = "firmware/NRF51822_{0}_Rob_OTA.hex"; var fileMask = "";
var fileName = null; 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] }) dfu.findDevice({ services: [0x180D] })
.then(device => { .then(device => {
fileName = fileMask.replace("{0}", device.name === "Hi_Rob" ? "Bye" : "Hi"); fileName = fileMask.replace("{0}", device.name === "Hi_Rob" ? "bye" : "hi");
log("found device: " + device.name); log("found device: " + device.name);
log("using file name: " + fileName); log("using file name: " + fileName);

View File

@@ -2,19 +2,35 @@
<html> <html>
<head> <head>
<title>web-bluetooth-dfu</title> <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; }
</style>
</head> </head>
<body> <body>
<button onclick="setMode()" style="font-size: 20px;">Set Mode</button> <div class="device-type">
<button onclick="findDFU()" style="font-size: 20px;">Transfer</button> <input type="radio" name="device-type" id="nrf51" checked>
<button onclick="both()" style="font-size: 20px;">Both</button> <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> <div id="results"></div>
<script src="dist/dfu.js"></script> <script src="dist/dfu.js"></script>
<script src="dist/hex2bin.js"></script> <script src="dist/hex2bin.js"></script>
<script> <script>
var urlMask = "//thegecko.github.io/web-bluetooth-dfu/firmware/NRF51822_{0}_Rob_OTA.hex";
var resultsEl = document.getElementById("results"); var resultsEl = document.getElementById("results");
var urlBase = "//thegecko.github.io/web-bluetooth-dfu/firmware/";
function log(message) { function log(message) {
console.log(message); console.log(message);
@@ -35,7 +51,18 @@
function transfer(device) { function transfer(device) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var url = urlMask.replace("{0}", device.name === "Hi_Rob" ? "Bye" : "Hi"); 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) download(url)
.then(hex => { .then(hex => {

1539
firmware/nrf52_app.hex Normal file

File diff suppressed because it is too large Load Diff

8288
firmware/nrf52_boot_s132.hex Normal file

File diff suppressed because it is too large Load Diff