mirror of
https://github.com/thegecko/web-bluetooth-dfu.git
synced 2025-12-13 04:28:13 +08:00
444 lines
17 KiB
JavaScript
444 lines
17 KiB
JavaScript
/* @license
|
|
*
|
|
* Device firmware update with Web Bluetooth
|
|
* Version: 0.0.1
|
|
*
|
|
* Protocol from:
|
|
* http://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v8.x.x/doc/8.1.0/s110/html/a00103.html
|
|
*
|
|
* 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 (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 LITTLE_ENDIAN = true;
|
|
|
|
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
|
|
};
|
|
|
|
// 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) {
|
|
if (typeof loggerFn === "function") {
|
|
loggers.push(loggerFn);
|
|
}
|
|
}
|
|
function log(message) {
|
|
loggers.forEach(logger => {
|
|
logger(message);
|
|
});
|
|
}
|
|
|
|
function findDevice(filters) {
|
|
return bluetooth.requestDevice({
|
|
filters: [ filters ],
|
|
optionalServices: [serviceUUID]
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
var controlChar = null;
|
|
/*
|
|
// Disconnect event currently not implemented...
|
|
device.addEventListener("gattserverdisconnected", () => {
|
|
log("DFU Target issued GAP Disconnect and reset into Bootloader/DFU mode.");
|
|
resolve(device);
|
|
});
|
|
|
|
*/
|
|
connect(device)
|
|
.then(chars => {
|
|
log("enabling notifications");
|
|
controlChar = chars.controlChar;
|
|
return controlChar.startNotifications()
|
|
.then(() => {
|
|
controlChar.addEventListener('characteristicvaluechanged', handleNotifications);
|
|
});
|
|
})
|
|
.then(() => {
|
|
log("writing modeData");
|
|
return controlChar.writeValue(new Uint8Array([1, 4]))
|
|
.then(() => {
|
|
log("modeData written");
|
|
resolve(device); // TODO: once disconnect event is implemented we should resolve in its callback...
|
|
});
|
|
})
|
|
.catch(error => {
|
|
error = "writeMode error: " + error;
|
|
log(error);
|
|
reject(error);
|
|
});
|
|
|
|
function handleNotifications(event) {
|
|
log('received notification on control characteristic - ERROR this should not happen');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 => {
|
|
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 (mbed).
|
|
return transfer(chars, arrayBuffer, imageType, 6, 0);
|
|
}
|
|
})
|
|
.then(() => {
|
|
resolve();
|
|
})
|
|
.catch(error => {
|
|
log(error);
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
function connect(device) {
|
|
return new Promise(function(resolve, reject) {
|
|
var server = null;
|
|
var service = null;
|
|
var controlChar = null;
|
|
var packetChar = null;
|
|
var versionChar = null;
|
|
|
|
function complete() {
|
|
resolve({
|
|
server: server,
|
|
controlChar: controlChar,
|
|
packetChar: packetChar,
|
|
versionChar: versionChar
|
|
});
|
|
}
|
|
|
|
device.connectGATT()
|
|
.then(gattServer => {
|
|
// Connected
|
|
server = gattServer;
|
|
log("connected to device");
|
|
return server.getPrimaryService(serviceUUID);
|
|
})
|
|
.then(primaryService => {
|
|
log("found DFU service");
|
|
service = primaryService;
|
|
return service.getCharacteristic(controlUUID);
|
|
})
|
|
.then(characteristic => {
|
|
log("found control characteristic");
|
|
controlChar = characteristic;
|
|
return service.getCharacteristic(packetUUID);
|
|
})
|
|
.then(characteristic => {
|
|
log("found packet characteristic");
|
|
packetChar = characteristic;
|
|
service.getCharacteristic(versionUUID)
|
|
.then(characteristic => { // Older DFU implementations (from older Nordic SDKs) have no DFU Version characteristic. So this may fail.
|
|
log("found version characteristic");
|
|
versionChar = characteristic;
|
|
complete();
|
|
})
|
|
.catch(error => {
|
|
error += ' no version charactersitic found';
|
|
log(error);
|
|
complete();
|
|
});
|
|
})
|
|
.catch(error => {
|
|
error = "connect error: " + error;
|
|
log(error);
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
var interval;
|
|
var offset;
|
|
function transfer(chars, arrayBuffer, imageType, majorVersion, minorVersion) {
|
|
return new Promise(function(resolve, reject) {
|
|
var controlChar = chars.controlChar;
|
|
var packetChar = chars.packetChar;
|
|
log('using dfu version ' + majorVersion + "." + minorVersion);
|
|
|
|
// Set up receipts
|
|
interval = Math.floor(arrayBuffer.byteLength / (packetSize * notifySteps));
|
|
offset = 0;
|
|
|
|
if (!controlChar.properties.notify) {
|
|
var error = "controlChar missing notify property";
|
|
log(error);
|
|
return reject(error);
|
|
}
|
|
|
|
log("enabling notifications");
|
|
controlChar.startNotifications()
|
|
.then(() => {
|
|
controlChar.addEventListener('characteristicvaluechanged', handleControl);
|
|
log("sending imagetype: " + imageType);
|
|
return controlChar.writeValue(new Uint8Array([1, imageType]));
|
|
})
|
|
.then(() => {
|
|
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);
|
|
|
|
// Set firmware length
|
|
return packetChar.writeValue(view);
|
|
})
|
|
.then(() => {
|
|
log("sent buffer size: " + arrayBuffer.byteLength);
|
|
})
|
|
.catch(error => {
|
|
error = "start error: " + error;
|
|
log(error);
|
|
reject(error);
|
|
});
|
|
|
|
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 Code.
|
|
if (resp_code !== 1) {
|
|
var error = "error from control: " + resp_code;
|
|
log(error);
|
|
return reject(error);
|
|
}
|
|
|
|
if (req_opcode === OPCODE.START_DFU && majorVersion > 6) {
|
|
log('write init packet');
|
|
|
|
controlChar.writeValue(new Uint8Array([2,0]))
|
|
.then(() => {
|
|
return packetChar.writeValue(generateInitPacket());
|
|
})
|
|
.then(() => {
|
|
return controlChar.writeValue(new Uint8Array([2,1]));
|
|
})
|
|
.catch(error => {
|
|
error = "error writing init: " + error;
|
|
log(error);
|
|
reject(error);
|
|
});
|
|
|
|
} else if (req_opcode === OPCODE.START_DFU || req_opcode === OPCODE.INITIALIZE_DFU_PARAMETERS) {
|
|
log('complete, send packet count');
|
|
|
|
var buffer = new ArrayBuffer(3);
|
|
view = new DataView(buffer);
|
|
view.setUint8(0, 8);
|
|
view.setUint16(1, interval, LITTLE_ENDIAN);
|
|
|
|
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 === OPCODE.RECEIVE_FIRMWARE_IMAGE) {
|
|
log('complete, check length');
|
|
|
|
controlChar.writeValue(new Uint8Array([7]))
|
|
.catch(error => {
|
|
error = "error checking length: " + error;
|
|
log(error);
|
|
reject(error);
|
|
});
|
|
|
|
} else if (req_opcode === OPCODE.REPORT_RECEIVED_IMAGE_SIZE) {
|
|
var byteCount = view.getUint32(3, LITTLE_ENDIAN);
|
|
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 === OPCODE.VALIDATE_FIRMWARE) {
|
|
log('complete, reset...');
|
|
/*
|
|
// Disconnect event currently not implemented
|
|
controlChar.service.device.addEventListener("gattserverdisconnected", () => {
|
|
resolve();
|
|
});
|
|
*/
|
|
controlChar.writeValue(new Uint8Array([5]))
|
|
.then(() => {
|
|
log('image activated and dfu target reset');
|
|
resolve(); // TODO: Resolve in disconnect event handler when implemented in Web Bluetooth API.
|
|
})
|
|
.catch(error => {
|
|
error = "error resetting: " + error;
|
|
log(error);
|
|
reject(error);
|
|
});
|
|
}
|
|
|
|
} else if (opCode === OPCODE.PACKET_RECEIPT_NOTIFICATION) { // 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(() => {
|
|
count ++;
|
|
offset += packetSize;
|
|
if (count < interval && offset < arrayBuffer.byteLength) {
|
|
writePacket(packetChar, arrayBuffer, count);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
error = "writePacket error: " + error;
|
|
log(error);
|
|
});
|
|
}
|
|
|
|
return {
|
|
addLogger: addLogger,
|
|
ImageType: ImageType,
|
|
findDevice: findDevice,
|
|
writeMode: writeMode,
|
|
provision: provision
|
|
};
|
|
})); |