From 1434b1440641d0524f689bbfb1cb414fc408b161 Mon Sep 17 00:00:00 2001 From: Rob Moran Date: Thu, 4 Feb 2016 10:40:31 -0600 Subject: [PATCH] First site --- .gitignore | 1 + README.md | 2 + dist/dfu.js | 344 ++++++++++++++++++++++++++++++++++++++++++++++++ dist/hex2bin.js | 45 +++++++ index.html | 84 ++++++++++++ 5 files changed, 476 insertions(+) create mode 100644 dist/dfu.js create mode 100644 dist/hex2bin.js create mode 100644 index.html diff --git a/.gitignore b/.gitignore index e43b0f9..91dfed8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +node_modules \ No newline at end of file diff --git a/README.md b/README.md index 944777a..80de2e7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # web-bluetooth-dfu Device firmware update with Web Bluetooth + +[Website](https://thegecko.github.io/web-bluetooth-dfu) \ No newline at end of file diff --git a/dist/dfu.js b/dist/dfu.js new file mode 100644 index 0000000..2c4a621 --- /dev/null +++ b/dist/dfu.js @@ -0,0 +1,344 @@ +/* + * Protocol from: + * http://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v8.x.x/doc/8.1.0/s110/html/a00103.html + */ + +// https://github.com/umdjs/umd +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['es6-promise', 'bleat'], factory); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS + module.exports = factory(Promise, require('bleat')); + } else { + // Browser globals with support for web workers (root is window) + root.dfu = factory(Promise, root.navigator.bluetooth); + } +}(this, function(Promise, bluetooth) { + "use strict"; + + var packetSize = 20; + var notifySteps = 40; + + var serviceUUID = "00001530-1212-efde-1523-785feabcd123"; + var controlUUID = "00001531-1212-efde-1523-785feabcd123"; + var packetUUID = "00001532-1212-efde-1523-785feabcd123"; + var versionUUID = "00001534-1212-efde-1523-785feabcd123"; + + var ImageType = { + None: 0, + SoftDevice: 1, + Bootloader: 2, + SoftDevice_Bootloader: 3, + Application: 4 + }; + + var littleEndian = (function() { + var buffer = new ArrayBuffer(2); + new DataView(buffer).setInt16(0, 256, true); + return new Int16Array(buffer)[0] === 256; + })(); + + var controlChar = null; + var packetChar = null; + var versionChar = null; + var server = null; + + // Hack to see debug info + var resultsEl = document.getElementById("results"); + function logFn(message) { + console.log(message); + resultsEl.innerText += message + "\n"; + } + + function findDevice(filters) { + return bluetooth.requestDevice({ + filters: [ filters ], + optionalServices: [serviceUUID] + }); + } + + function writeMode(device) { + return new Promise(function(resolve, reject) { + + // Disconnect event currently not implemented +/* + device.addEventListener("gattserverdisconnected", () => { + logFn("modeData written"); + resolve(); + }); +*/ + connect(device) + .then(() => { + logFn("writing modeData..."); + controlChar.writeValue(new Uint8Array([1])); + return server.disconnect(); + }) + .then(() => { + logFn("modeData written"); + resolve(device); + }).catch(error => { + error = "writeMode error: " + error; + logFn(error); + reject(error); + }); + }); + } + + function provision(device, arrayBuffer, imageType) { + return new Promise(function(resolve, reject) { + imageType = imageType || ImageType.Application; + + connect(device) + .then(() => { + if (versionChar) { + versionChar.readValue() + .then(data => { + var view = new DataView(data); + var major = view.getUint8(0); + var minor = view.getUint8(1); + return transfer(arrayBuffer, imageType, major, minor); + }); + } else { + // Default to version 6.0 + return transfer(arrayBuffer, imageType, 6, 0); + } + }) + .then(() => { + resolve(); + }) + .catch(error => { + logFn(error); + reject(error); + }); + }); + } + + function connect(device) { + return new Promise(function(resolve, reject) { + var service = null; + // Disconnect event currently not implemented +/* + device.addEventListener("gattserverdisconnected", () => { + logFn("device disconnected"); + service = null; + controlChar = null; + packetChar = null; + versionChar = null; + server = null; + }); +*/ + device.connectGATT() + .then(gattServer => { + // Connected + server = gattServer; + logFn("connected to device"); + return server.getPrimaryService(serviceUUID); + }) + .then(primaryService => { + logFn("found DFU service"); + service = primaryService; + return service.getCharacteristic(controlUUID); + }) + .then(characteristic => { + logFn("found control characteristic"); + controlChar = characteristic; + return service.getCharacteristic(packetUUID); + }) + .then(characteristic => { + logFn("found packet characteristic"); + packetChar = characteristic; + service.getCharacteristic(versionUUID) + .then(() => { + logFn("found version characteristic"); + versionChar = characteristic; + resolve(); + }) + .catch(error => { + resolve(); + }); + }) + .catch(error => { + error = "connect error: " + error; + logFn(error); + reject(error); + }); + }); + }; + + var interval; + var offset; + function transfer(arrayBuffer, imageType, majorVersion, minorVersion) { + return new Promise(function(resolve, reject) { + logFn('using dfu version ' + majorVersion + "." + minorVersion); + + // Set up receipts + interval = Math.floor(arrayBuffer.byteLength / (packetSize * notifySteps)); + offset = 0; + + controlChar.addEventListener('characteristicvaluechanged', data => { + var view = new DataView(data); + var opCode = view.getUint8(0); + + if (opCode === 16) { // response + var resp_code = view.getUint8(2); + if (resp_code !== 1) { + var error = "error from control: " + resp_code; + logFn(error); + return reject(error); + } + + var req_opcode = view.getUint8(1); + if (req_opcode === 1 && majorVersion > 6) { + logFn('write null init packet'); + + 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; + logFn(error); + reject(error); + }); + + } else if (req_opcode === 1 || req_opcode === 2) { + logFn('complete, send packet count'); + + var buffer = new ArrayBuffer(3); + var view = new DataView(buffer); + view.setUint8(0, 8); + view.setUint16(1, interval, littleEndian); + + controlChar.writeValue(view) + .then(() => { + logFn("sent packet count: " + interval); + return controlChar.writeValue(new Uint8Array([3])); + }) + .then(() => { + logFn("sent receive"); + return writePacket(arrayBuffer, 0); + }) + .catch(error => { + error = "error sending packet count: " + error; + logFn(error); + reject(error); + }); + + } else if (req_opcode === 3) { + logFn('complete, check length'); + + controlChar.writeValue(new Uint8Array([7])) + .catch(error => { + error = "error checking length: " + error; + logFn(error); + reject(error); + }); + + } else if (req_opcode === 7) { + var bytecount = view.getUint32(3, littleEndian); + logFn('length: ' + bytecount); + logFn('complete, validate...'); + + controlChar.writeValue(new Uint8Array([4])) + .catch(error => { + error = "error validating: " + error; + logFn(error); + reject(error); + }); + + } else if (req_opcode === 4) { + logFn('complete, reset...'); + + controlChar.writeValue(new Uint8Array([5])) + .then(() => { + resolve(); + }) + .catch(error => { + error = "error resetting: " + error; + logFn(error); + reject(error); + }); + } + + } else if (opCode === 17) { + var bytecount = view.getUint32(1, littleEndian); + logFn('transferred: ' + bytecount); + writePacket(arrayBuffer, 0); + } + }); + + if (!controlChar.properties.notify) { + var error = "controlChar missing notify property"; + logFn(error); + return reject(error); + } + + logFn("enabling notifications"); + controlChar.startNotifications() + .then(() => { + logFn("sending imagetype: " + imageType); + return controlChar.writeValue(new Uint8Array([1, imageType])) + }) + .then(() => { + logFn("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, littleEndian); + view.setUint32(4, bootLength, littleEndian); + view.setUint32(8, appLength, littleEndian); + + // Set firmware length + packetChar.writeValue(view) + .then(() => { + logFn("sent buffer size: " + arrayBuffer.byteLength); + }) + .catch(error => { + error = "firmware length error: " + error; + logFn(error); + reject(error); + }); + }) + .catch(error => { + error = "start error: " + error; + logFn(error); + reject(error); + }); + }); + } + + function writePacket(arrayBuffer, offset, 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(() => { + count ++; + offset += packetSize; + if (count < interval && offset < arrayBuffer.byteLength) { + writePacket(arrayBuffer, count); + } + }) + .catch(error => { + error = "writePacket error: " + error; + logFn(error); + }); + } + + return { + ImageType: ImageType, + findDevice: findDevice, + writeMode: writeMode, + provision: provision + }; +})); \ No newline at end of file diff --git a/dist/hex2bin.js b/dist/hex2bin.js new file mode 100644 index 0000000..1b868ff --- /dev/null +++ b/dist/hex2bin.js @@ -0,0 +1,45 @@ +// 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'; + + return function(hex) { + var hexLines = hex.split("\n"); + var size = 0; + + hexLines.forEach(function(line) { + if (line.substr(7, 2) === "00") { // type == data + size += parseInt(line.substr(1, 2), 16); + } + }); + + var buffer = new ArrayBuffer(size); + var view = new Uint8Array(buffer); + var pointer = 0; + + hexLines.forEach(function(line) { + if (line.substr(7, 2) === "00") { // type == data + var length = parseInt(line.substr(1, 2), 16); + var data = line.substr(9, length * 2); + for (var i = 0; i < length * 2; i += 2) { + view[pointer] = parseInt(data.substr(i, 2), 16); + pointer++; + } + } + }); + + return buffer; + }; +})); \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..0657483 --- /dev/null +++ b/index.html @@ -0,0 +1,84 @@ + + + + web-bluetooth-dfu + + + + +
+ + + + + + +