diff --git a/README.md b/README.md index a0a7ed4..8e1bb2f 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,31 @@ # Web Bluetooth DFU 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 -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 -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/) ## Node Example -Install the npm dependencies and run. +Clone this repository, install the npm dependencies and execute. ``` npm install -node example_node -``` \ No newline at end of file +node example_node +``` + +Where `````` is one of ```nrf51``` or ```nrf52```. \ No newline at end of file diff --git a/dist/dfu.js b/dist/dfu.js index 312c2dd..94379eb 100644 --- a/dist/dfu.js +++ b/dist/dfu.js @@ -1,10 +1,10 @@ /* @license * * Device firmware update with Web Bluetooth - * Version: 0.0.1 + * Version: 0.0.2 * * 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) * @@ -44,6 +44,8 @@ }(this, function(Promise, bluetooth) { "use strict"; + var LITTLE_ENDIAN = true; + var packetSize = 20; var notifySteps = 40; @@ -59,12 +61,30 @@ SoftDevice_Bootloader: 3, Application: 4 }; - - var littleEndian = (function() { - var buffer = new ArrayBuffer(2); - new DataView(buffer).setInt16(0, 256, true); - return new Int16Array(buffer)[0] === 256; - })(); + + // 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 + }; + + 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 = []; 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) { 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", () => { - log("modeData written"); - resolve(); + log("DFU Target issued GAP Disconnect and reset into Bootloader/DFU mode."); + resolve(device); }); */ + var characteristics = null; + connect(device) .then(chars => { - log("writing modeData..."); - chars.controlChar.writeValue(new Uint8Array([1])); - + log("enabling notifications"); + 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 setTimeout(() => { - chars.server.disconnect(); - setTimeout(() => { - log("modeData written"); - resolve(device); - }, 3000); - }, 3000); + characteristics.server.disconnect(); + resolve(device); + }, 2000); }) .catch(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) { return new Promise(function(resolve, reject) { + var versionChar = null; imageType = imageType || ImageType.Application; connect(device) .then(chars => { - if (chars.versionChar) { - return chars.versionChar.readValue() + versionChar = chars.versionChar; + if (versionChar) { // Older DFU implementations (from older Nordic SDKs < 7.0) have no DFU Version characteristic. + return versionChar.readValue() .then(data => { + console.log('read versionChar'); var view = new DataView(data); var major = view.getUint8(0); var minor = view.getUint8(1); return transfer(chars, arrayBuffer, imageType, major, minor); }); } else { - // Default to version 6.0 + // Default to version 6.0 (mbed). return transfer(chars, arrayBuffer, imageType, 6, 0); } }) @@ -183,12 +238,14 @@ 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(characteristic => { log("found version characteristic"); versionChar = characteristic; complete(); }) .catch(error => { + log("info: no version characteristic found"); complete(); }); }) @@ -213,9 +270,9 @@ offset = 0; if (!controlChar.properties.notify) { - var error = "controlChar missing notify property"; - log(error); - return reject(error); + var err = "controlChar missing notify property"; + log(err); + return reject(err); } log("enabling notifications"); @@ -223,7 +280,7 @@ .then(() => { controlChar.addEventListener('characteristicvaluechanged', handleControl); log("sending imagetype: " + imageType); - return controlChar.writeValue(new Uint8Array([1, imageType])); + return controlChar.writeValue(new Uint8Array([OPCODE.START_DFU, imageType])); }) .then(() => { log("sent start"); @@ -234,15 +291,14 @@ var buffer = new ArrayBuffer(12); var view = new DataView(buffer); - view.setUint32(0, softLength, littleEndian); - view.setUint32(4, bootLength, littleEndian); - view.setUint32(8, appLength, littleEndian); + view.setUint32(0, softLength, LITTLE_ENDIAN); + view.setUint32(4, bootLength, LITTLE_ENDIAN); + view.setUint32(8, appLength, LITTLE_ENDIAN); - // Set firmware length return packetChar.writeValue(view); }) .then(() => { - log("sent buffer size: " + arrayBuffer.byteLength); + log("sent image size: " + arrayBuffer.byteLength); }) .catch(error => { error = "start error: " + error; @@ -253,103 +309,112 @@ function handleControl(event) { var data = event.target.value; var view = new DataView(data); + var opCode = view.getUint8(0); + var req_opcode = view.getUint8(1); + var resp_code = view.getUint8(2); - if (opCode === 16) { // response - var resp_code = view.getUint8(2); + if (opCode === OPCODE.RESPONSE_CODE) { if (resp_code !== 1) { - var error = "error from control: " + resp_code; - log(error); - return reject(error); + 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(() => { + 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; + } + + log('send packet count'); - var req_opcode = view.getUint8(1); - if (req_opcode === 1 && majorVersion > 6) { - log('write null init packet'); + 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(() => { + log("sent packet count: " + interval); + 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(new Uint8Array([2,0])) - .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) { - log('complete, send packet count'); - - var buffer = new ArrayBuffer(3); - view = new DataView(buffer); - view.setUint8(0, 8); - view.setUint16(1, interval, littleEndian); - - controlChar.writeValue(view) - .then(() => { - log("sent packet count: " + interval); - return controlChar.writeValue(new Uint8Array([3])); - }) - .then(() => { - log("sent receive"); - return writePacket(packetChar, arrayBuffer, 0); - }) - .catch(error => { - error = "error sending packet count: " + error; - log(error); - reject(error); - }); - - } else if (req_opcode === 3) { - log('complete, check length'); - - controlChar.writeValue(new Uint8Array([7])) - .catch(error => { - error = "error checking length: " + error; - log(error); - reject(error); - }); - - } 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...'); + controlChar.writeValue(new Uint8Array([OPCODE.REPORT_RECEIVED_IMAGE_SIZE])) + .catch(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(error => { + error = "error validating: " + error; + log(error); + reject(error); + }); + break; + case OPCODE.VALIDATE_FIRMWARE: + log('complete, reset...'); /* - // Disconnect event currently not implemented - controlChar.service.device.addEventListener("gattserverdisconnected", () => { - resolve(); - }); -*/ - controlChar.writeValue(new Uint8Array([5])) - .then(() => { - // Hack to gracefully disconnect without disconnect event - setTimeout(() => { - chars.server.disconnect(); + // TODO: Resolve in disconnect event handler when implemented in Web Bluetooth API. + controlChar.service.device.addEventListener("gattserverdisconnected", () => { resolve(); - }, 3000); - }) - .catch(error => { - error = "error resetting: " + error; - log(error); - reject(error); - }); + }); +*/ + controlChar.writeValue(new Uint8Array([OPCODE.ACTIVATE_IMAGE_AND_RESET])) + .then(() => { + log('image activated and dfu target reset'); + // 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) { - var bytes = view.getUint32(1, littleEndian); + } else if (opCode === OPCODE.PACKET_RECEIPT_NOTIFICATION) { + var bytes = view.getUint32(1, LITTLE_ENDIAN); log('transferred: ' + bytes); writePacket(packetChar, arrayBuffer, 0); } @@ -383,4 +448,4 @@ writeMode: writeMode, provision: provision }; -})); \ No newline at end of file +})); diff --git a/example_node.js b/example_node.js index 7330544..7bc7f72 100644 --- a/example_node.js +++ b/example_node.js @@ -5,12 +5,30 @@ var fs = require('fs'); var log = console.log; dfu.addLogger(log); -var fileMask = "firmware/NRF51822_{0}_Rob_OTA.hex"; +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"); + fileName = fileMask.replace("{0}", device.name === "Hi_Rob" ? "bye" : "hi"); log("found device: " + device.name); log("using file name: " + fileName); diff --git a/example_web.html b/example_web.html index 11112f7..1020141 100644 --- a/example_web.html +++ b/example_web.html @@ -2,19 +2,35 @@ web-bluetooth-dfu + + + - - - +
+ + + + +
+ + + + +