First site

This commit is contained in:
Rob Moran
2016-02-04 10:40:31 -06:00
parent 0360585b80
commit 1434b14406
5 changed files with 476 additions and 0 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
.DS_Store
node_modules

View File

@@ -1,2 +1,4 @@
# web-bluetooth-dfu
Device firmware update with Web Bluetooth
[Website](https://thegecko.github.io/web-bluetooth-dfu)

344
dist/dfu.js vendored Normal file
View File

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

45
dist/hex2bin.js vendored Normal file
View File

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

84
index.html Normal file
View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html>
<head>
<title>web-bluetooth-dfu</title>
</head>
<body>
<button onclick="setMode()" style="font-size: 42px;">Set Mode and Transfer</button>
<button onclick="findDFU()" style="font-size: 42px;">Transfer Only</button>
<div id="results"></div>
<script src="dist/dfu.js"></script>
<script src="dist/hex2bin.js"></script>
<script>
var names = ["DFU_Test", "Hi_Rob", "Bye_Rob"];
var urlMask = "//thegecko.github.io/web-bluetooth-dfu/firmware/NRF51822_{0}_Rob_OTA.hex";
var resultsEl = document.getElementById("results");
function log(message) {
console.log(message);
resultsEl.innerText += message + "\n";
}
function download(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.addEventListener("load", function() {
resolve(this.responseText);
});
xhr.open("GET", url);
xhr.send();
});
}
function transfer(device) {
return new Promise(function(resolve, reject) {
var url = urlMask.replace("{0}", device.name === "Hi_Rob" ? "Bye" : "Hi");
download(url)
.then(hex => {
var buffer = hex2bin(hex);
log("downloaded length: " + buffer.byteLength);
return dfu.provision(device, buffer);
})
.then(() => {
log('dfu complete');
resolve();
})
.catch(error => {
log(error);
reject(error);
});
});
}
function setMode() {
dfu.findDevice({ services: [0x180D] })
.then(device => {
log('Found device: ' + device.name);
return dfu.writeMode(device);
})
.then(device => {
log('mode written');
transfer(device);
})
.catch(error => {
log(error);
});
}
function findDFU() {
dfu.findDevice({ name: "DfuTarg" })
.then(device => {
transfer(device);
})
.catch(error => {
log(error);
});
}
</script>
</body>
</html>