diff --git a/dist/secure-dfu.js b/dist/secure-dfu.js index a397bf7..1a340c0 100644 --- a/dist/secure-dfu.js +++ b/dist/secure-dfu.js @@ -46,11 +46,13 @@ const SERVICE_UUID = 0xFE59; const CONTROL_UUID = "8ec90001-f315-4f60-9fb8-838830daea50"; const PACKET_UUID = "8ec90002-f315-4f60-9fb8-838830daea50"; + const BUTTON_UUID = "8ec90003-f315-4f60-9fb8-838830daea50"; const LITTLE_ENDIAN = true; const PACKET_SIZE = 20; const OPERATIONS = { + BUTTON_COMMAND: [0x01], CREATE_COMMAND: [0x01, 0x01], CREATE_DATA: [0x01, 0x02], RECEIPT_NOTIFICATIONS: [0x02], @@ -58,7 +60,7 @@ EXECUTE: [0x04], SELECT_COMMAND: [0x06, 0x01], SELECT_DATA: [0x06, 0x02], - RESPONSE: [0x60] + RESPONSE: [0x60, 0x20] }; const RESPONSE = { @@ -95,10 +97,8 @@ this.crc32 = crc32; this.events = {}; this.notifyFns = {}; - this.connected = false; this.controlChar = null; this.packetChar = null; - this.buffer = null; } function createListenerFn(eventTypes) { @@ -129,65 +129,163 @@ }); }; - secureDfu.prototype.requestDevice = function(filters) { - if (!filters) { - filters = [{ - services: [SERVICE_UUID] - }]; + secureDfu.prototype.progress = function(bytes) { + this.dispatchEvent({ + type: "progress", + object: "unknown", + totalBytes: 0, + currentBytes: bytes + }); + } + + secureDfu.prototype.requestDevice = function(hiddenDfu, filters) { + if (!hiddenDfu && !filters) { + filters = [{ services: [SERVICE_UUID] }]; } return bluetooth.requestDevice({ filters: filters, + acceptAllDevices: !filters, optionalServices: [SERVICE_UUID] + }) + .then(device => { + if (hiddenDfu) { + return this.setDfuMode(device); + } + return device; }); }; - secureDfu.prototype.connect = function(device) { - let service = null; + secureDfu.prototype.setDfuMode = function(device) { + return this.gattConnect(device) + .then(characteristics => { + this.log(`found ${characteristics.length} characteristic(s)`); - device.addEventListener("gattserverdisconnected", event => { - this.connected = false; - this.controlChar = null; - this.packetChar = null; - this.buffer = null; - this.log("disconnected"); - }); + let controlChar = characteristics.find(characteristic => { + return (characteristic.uuid === CONTROL_UUID); + }); + let packetChar = characteristics.find(characteristic => { + return (characteristic.uuid === PACKET_UUID); + }); - return device.gatt.connect() - .then(gattServer => { - this.log("connected to gatt server"); - return gattServer.getPrimaryService(SERVICE_UUID); - }) - .then(primaryService => { - this.log("found DFU service"); - service = primaryService; - return service.getCharacteristic(CONTROL_UUID); - }) - .then(characteristic => { - this.log("found control characteristic"); - if (!characteristic.properties.notify) { - throw new Error("control characterisitc does not allow notifications"); + if (controlChar && packetChar) { + return device; } - this.controlChar = characteristic; - return characteristic.startNotifications(); + + let buttonChar = characteristics.find(characteristic => { + return (characteristic.uuid === BUTTON_UUID); + }); + + if (!buttonChar) { + throw new Error("Unsupported device"); + } + + // Support buttonless devices + this.log("found buttonless characteristic"); + if (!buttonChar.properties.notify && !buttonChar.properties.indicate) { + throw new Error("Buttonless characteristic does not allow notifications"); + } + + return buttonChar.startNotifications() + .then(() => { + this.log("enabled buttonless notifications"); + buttonChar.addEventListener("characteristicvaluechanged", this.handleNotification.bind(this)); + return this.sendOperation(buttonChar, OPERATIONS.BUTTON_COMMAND); + }) + .then(() => { + this.log("sent dfu mode"); + return new Promise((resolve, reject) => { + device.addEventListener("gattserverdisconnected", event => { + resolve(); + }); + }); + }); + }); + } + + secureDfu.prototype.update = function(device, init, firmware) { + if (!device) throw new Error("Device not specified"); + if (!init) throw new Error("Init not specified"); + if (!firmware) throw new Error("Firmware not specified"); + + return this.connect(device) + .then(() => { + this.log("transferring init"); + return this.transferInit(init); }) .then(() => { - this.log("enabled control notifications"); - this.controlChar.addEventListener("characteristicvaluechanged", this.handleNotification.bind(this)); - return service.getCharacteristic(PACKET_UUID); + this.log("transferring firmware"); + return this.transferFirmware(firmware); }) - .then(characteristic => { + .then(() => { + this.log("complete, disconnecting..."); + return new Promise((resolve, reject) => { + device.addEventListener("gattserverdisconnected", event => { + this.log("disconnected"); + resolve(); + }); + }); + }); + } + + secureDfu.prototype.connect = function(device) { + device.addEventListener("gattserverdisconnected", event => { + this.controlChar = null; + this.packetChar = null; + }); + + return this.gattConnect(device) + .then(characteristics => { + this.log(`found ${characteristics.length} characteristic(s)`); + + this.packetChar = characteristics.find(characteristic => { + return (characteristic.uuid === PACKET_UUID); + }); + if (!this.packetChar) throw new Error("Unable to find packet characteristic"); this.log("found packet characteristic"); - this.packetChar = characteristic; - this.connected = true; + + this.controlChar = characteristics.find(characteristic => { + return (characteristic.uuid === CONTROL_UUID); + }); + if (!this.controlChar) throw new Error("Unable to find control characteristic"); + this.log("found control characteristic"); + + if (!this.controlChar.properties.notify && !this.controlChar.properties.indicate) { + throw new Error("Control characteristic does not allow notifications"); + } + return this.controlChar.startNotifications(); + }) + .then(() => { + this.controlChar.addEventListener("characteristicvaluechanged", this.handleNotification.bind(this)); + this.log("enabled control notifications"); + return device; }); }; + secureDfu.prototype.gattConnect = function(device) { + return Promise.resolve() + .then(() => { + if (device.gatt.connected) return device.gatt; + return device.gatt.connect(); + }) + .then(server => { + this.log("connected to gatt server"); + return server.getPrimaryService(SERVICE_UUID) + .catch(error => { + throw new Error("Unable to find DFU service"); + }); + }) + .then(service => { + this.log("found DFU service"); + return service.getCharacteristics(); + }); + } + secureDfu.prototype.handleNotification = function(event) { let view = event.target.value; - if (view.getUint8(0) !== OPERATIONS.RESPONSE[0]) { - throw new Error("unrecognised control characteristic response notification"); + if (OPERATIONS.RESPONSE.indexOf(view.getUint8(0)) < 0) { + throw new Error("Unrecognised control characteristic response notification"); } let operation = view.getUint8(1); @@ -213,12 +311,8 @@ } }; - secureDfu.prototype.sendOperation = function(operation, buffer) { + secureDfu.prototype.sendOperation = function(characteristic, operation, buffer) { return new Promise((resolve, reject) => { - if (!this.connected) throw new Error("device not connected"); - if (!this.controlChar) throw new Error("control characteristic not found"); - if (!this.packetChar) throw new Error("packet characteristic not found"); - let size = operation.length; if (buffer) size += buffer.byteLength; @@ -234,72 +328,80 @@ reject: reject }; - this.controlChar.writeValue(value); + characteristic.writeValue(value); }); } + secureDfu.prototype.sendControl = function(operation, buffer) { + return this.sendOperation(this.controlChar, operation, buffer); + } + secureDfu.prototype.transferInit = function(buffer) { - return this.sendOperation(OPERATIONS.SELECT_COMMAND) + return this.transfer(buffer, "init", OPERATIONS.SELECT_COMMAND, OPERATIONS.CREATE_COMMAND); + } + + secureDfu.prototype.transferFirmware = function(buffer) { + return this.transfer(buffer, "firmware", OPERATIONS.SELECT_DATA, OPERATIONS.CREATE_DATA); + } + + secureDfu.prototype.transfer = function(buffer, type, selectType, createType) { + return this.sendControl(selectType) .then(response => { let maxSize = response.getUint32(0, LITTLE_ENDIAN); let offset = response.getUint32(4, LITTLE_ENDIAN); let crc = response.getInt32(8, LITTLE_ENDIAN); - if (offset === buffer.byteLength && this.checkCrc(buffer, crc)) { + if (type === "init" && offset === buffer.byteLength && this.checkCrc(buffer, crc)) { this.log("init packet already available, skipping transfer"); return; } - this.buffer = buffer; - return this.transferObject(OPERATIONS.CREATE_COMMAND, maxSize, offset); + this.progress = function(bytes) { + this.dispatchEvent({ + type: "progress", + object: type, + totalBytes: buffer.byteLength, + currentBytes: bytes + }); + } + this.progress(0); + + return this.transferObject(buffer, createType, maxSize, offset); }); } - secureDfu.prototype.transferFirmware = function(buffer) { - return this.sendOperation(OPERATIONS.SELECT_DATA) - .then(response => { - - let maxSize = response.getUint32(0, LITTLE_ENDIAN); - let offset = response.getUint32(4, LITTLE_ENDIAN); - let crc = response.getInt32(8, LITTLE_ENDIAN); - - this.buffer = buffer; - return this.transferObject(OPERATIONS.CREATE_DATA, maxSize, offset); - }); - } - - secureDfu.prototype.transferObject = function(createType, maxSize, offset) { + secureDfu.prototype.transferObject = function(buffer, createType, maxSize, offset) { let start = offset - offset % maxSize; - let end = Math.min(start + maxSize, this.buffer.byteLength); + let end = Math.min(start + maxSize, buffer.byteLength); let view = new DataView(new ArrayBuffer(4)); view.setUint32(0, end - start, LITTLE_ENDIAN); - return this.sendOperation(createType, view.buffer) + return this.sendControl(createType, view.buffer) .then(response => { - let data = this.buffer.slice(start, end); + let data = buffer.slice(start, end); return this.transferData(data, start); }) .then(() => { - return this.sendOperation(OPERATIONS.CACULATE_CHECKSUM); + return this.sendControl(OPERATIONS.CACULATE_CHECKSUM); }) .then(response => { let crc = response.getInt32(4, LITTLE_ENDIAN); let transferred = response.getUint32(0, LITTLE_ENDIAN); - let data = this.buffer.slice(0, transferred); + let data = buffer.slice(0, transferred); if (this.checkCrc(data, crc)) { this.log(`written ${transferred} bytes`); offset = transferred; - return this.sendOperation(OPERATIONS.EXECUTE); + return this.sendControl(OPERATIONS.EXECUTE); } else { this.log("object failed to validate"); } }) .then(() => { - if (end < this.buffer.byteLength) { - return this.transferObject(createType, maxSize, offset); + if (end < buffer.byteLength) { + return this.transferObject(buffer, createType, maxSize, offset); } else { this.log("transfer complete"); } @@ -313,11 +415,7 @@ return this.packetChar.writeValue(packet) .then(() => { - this.dispatchEvent({ - type: "progress", - currentBytes: offset + end, - totalBytes: this.buffer.byteLength - }); + this.progress(offset + end); if (end < data.byteLength) { return this.transferData(data, offset, end); diff --git a/examples/secure_dfu_web.html b/examples/secure_dfu_web.html index e793f72..7b924e6 100644 --- a/examples/secure_dfu_web.html +++ b/examples/secure_dfu_web.html @@ -19,11 +19,11 @@ font-weight: 600; } #drop { - margin: 40px auto; - max-width: 640px; - background-color: rgba(255, 255, 255, 0.10); position: relative; - padding: 100px 0px 70px; + margin: 40px auto; + max-width: 680px; + background-color: rgba(255, 255, 255, 0.10); + padding: 120px 0px 100px; outline: 2px dashed #072b44; outline-offset: -10px; } @@ -33,50 +33,49 @@ } #icon { width: 100%; - height: 40px; fill: #d7ecfb; - display: block; - margin-bottom: 40px; + margin-bottom: 30px; + } + #github { + fill: #d7ecfb; + } + #github:hover { + fill: #8bb5ba; } #file { width: 0.1px; height: 0.1px; opacity: 0; - overflow: hidden; - position: absolute; z-index: -1; } #label { cursor: pointer; - display: block; } #label:hover strong { color: #8bb5ba; } - #update { + #select { position: absolute; width: 100%; - z-index: 1; visibility: hidden; } #button { font-size: 12px; - color: #d5e8f1; + color: inherit; margin: 20px auto; border: 0px; background-color: #1b679c; - outline: none; height: 30px; padding: 0 30px; border-radius: 4px; text-transform: uppercase; cursor: pointer; + outline: none; } #button:hover { background: #2674ab; } #status { - position: relative; margin: 20px auto; border: 1px solid #d7ecfb; width: 400px; @@ -89,9 +88,9 @@ height: 100%; } #transfer { - position: absolute; - line-height: 24px; width: 100%; + line-height: 24px; + margin-top: -24px; } @@ -109,16 +108,22 @@ or drag it here -
- +
+
-
-
+
+
+ + + + + + @@ -126,7 +131,8 @@