diff --git a/dist/secure-dfu.js b/dist/secure-dfu.js new file mode 100644 index 0000000..4a1bdd3 --- /dev/null +++ b/dist/secure-dfu.js @@ -0,0 +1,2 @@ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).SecureDfu=e()}}(function(){return function o(s,a,c){function u(t,e){if(!a[t]){if(!s[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(f)return f(t,!0);var r=new Error("Cannot find module '"+t+"'");throw r.code="MODULE_NOT_FOUND",r}var i=a[t]={exports:{}};s[t][0].call(i.exports,function(e){return u(s[t][1][e]||e)},i,i.exports,o,s,a,c)}return a[t].exports}for(var f="function"==typeof require&&require,e=0;ei){s.warned=!0;var a=new Error("Possible EventEmitter memory leak detected. "+s.length+' "'+String(t)+'" listeners added. Use emitter.setMaxListeners() to increase limit.');a.name="MaxListenersExceededWarning",a.emitter=e,a.type=t,a.count=s.length,"object"==typeof console&&console.warn&&console.warn("%s: %s",a.name,a.message)}}else s=o[t]=n,++e._eventsCount;return e}function l(){if(!this.fired)switch(this.target.removeListener(this.type,this.wrapFn),this.fired=!0,arguments.length){case 0:return this.listener.call(this.target);case 1:return this.listener.call(this.target,arguments[0]);case 2:return this.listener.call(this.target,arguments[0],arguments[1]);case 3:return this.listener.call(this.target,arguments[0],arguments[1],arguments[2]);default:for(var e=new Array(arguments.length),t=0;t 1)\n er = arguments[1];\n if (er instanceof Error) {\n throw er; // Unhandled 'error' event\n } else {\n // At least give some kind of context to the user\n var err = new Error('Unhandled \"error\" event. (' + er + ')');\n err.context = er;\n throw err;\n }\n return false;\n }\n\n handler = events[type];\n\n if (!handler)\n return false;\n\n var isFn = typeof handler === 'function';\n len = arguments.length;\n switch (len) {\n // fast cases\n case 1:\n emitNone(handler, isFn, this);\n break;\n case 2:\n emitOne(handler, isFn, this, arguments[1]);\n break;\n case 3:\n emitTwo(handler, isFn, this, arguments[1], arguments[2]);\n break;\n case 4:\n emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]);\n break;\n // slower\n default:\n args = new Array(len - 1);\n for (i = 1; i < len; i++)\n args[i - 1] = arguments[i];\n emitMany(handler, isFn, this, args);\n }\n\n return true;\n};\n\nfunction _addListener(target, type, listener, prepend) {\n var m;\n var events;\n var existing;\n\n if (typeof listener !== 'function')\n throw new TypeError('\"listener\" argument must be a function');\n\n events = target._events;\n if (!events) {\n events = target._events = objectCreate(null);\n target._eventsCount = 0;\n } else {\n // To avoid recursion in the case that type === \"newListener\"! Before\n // adding it to the listeners, first emit \"newListener\".\n if (events.newListener) {\n target.emit('newListener', type,\n listener.listener ? listener.listener : listener);\n\n // Re-assign `events` because a newListener handler could have caused the\n // this._events to be assigned to a new object\n events = target._events;\n }\n existing = events[type];\n }\n\n if (!existing) {\n // Optimize the case of one listener. Don't need the extra array object.\n existing = events[type] = listener;\n ++target._eventsCount;\n } else {\n if (typeof existing === 'function') {\n // Adding the second element, need to change to array.\n existing = events[type] =\n prepend ? [listener, existing] : [existing, listener];\n } else {\n // If we've already got an array, just append.\n if (prepend) {\n existing.unshift(listener);\n } else {\n existing.push(listener);\n }\n }\n\n // Check for listener leak\n if (!existing.warned) {\n m = $getMaxListeners(target);\n if (m && m > 0 && existing.length > m) {\n existing.warned = true;\n var w = new Error('Possible EventEmitter memory leak detected. ' +\n existing.length + ' \"' + String(type) + '\" listeners ' +\n 'added. Use emitter.setMaxListeners() to ' +\n 'increase limit.');\n w.name = 'MaxListenersExceededWarning';\n w.emitter = target;\n w.type = type;\n w.count = existing.length;\n if (typeof console === 'object' && console.warn) {\n console.warn('%s: %s', w.name, w.message);\n }\n }\n }\n }\n\n return target;\n}\n\nEventEmitter.prototype.addListener = function addListener(type, listener) {\n return _addListener(this, type, listener, false);\n};\n\nEventEmitter.prototype.on = EventEmitter.prototype.addListener;\n\nEventEmitter.prototype.prependListener =\n function prependListener(type, listener) {\n return _addListener(this, type, listener, true);\n };\n\nfunction onceWrapper() {\n if (!this.fired) {\n this.target.removeListener(this.type, this.wrapFn);\n this.fired = true;\n switch (arguments.length) {\n case 0:\n return this.listener.call(this.target);\n case 1:\n return this.listener.call(this.target, arguments[0]);\n case 2:\n return this.listener.call(this.target, arguments[0], arguments[1]);\n case 3:\n return this.listener.call(this.target, arguments[0], arguments[1],\n arguments[2]);\n default:\n var args = new Array(arguments.length);\n for (var i = 0; i < args.length; ++i)\n args[i] = arguments[i];\n this.listener.apply(this.target, args);\n }\n }\n}\n\nfunction _onceWrap(target, type, listener) {\n var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };\n var wrapped = bind.call(onceWrapper, state);\n wrapped.listener = listener;\n state.wrapFn = wrapped;\n return wrapped;\n}\n\nEventEmitter.prototype.once = function once(type, listener) {\n if (typeof listener !== 'function')\n throw new TypeError('\"listener\" argument must be a function');\n this.on(type, _onceWrap(this, type, listener));\n return this;\n};\n\nEventEmitter.prototype.prependOnceListener =\n function prependOnceListener(type, listener) {\n if (typeof listener !== 'function')\n throw new TypeError('\"listener\" argument must be a function');\n this.prependListener(type, _onceWrap(this, type, listener));\n return this;\n };\n\n// Emits a 'removeListener' event if and only if the listener was removed.\nEventEmitter.prototype.removeListener =\n function removeListener(type, listener) {\n var list, events, position, i, originalListener;\n\n if (typeof listener !== 'function')\n throw new TypeError('\"listener\" argument must be a function');\n\n events = this._events;\n if (!events)\n return this;\n\n list = events[type];\n if (!list)\n return this;\n\n if (list === listener || list.listener === listener) {\n if (--this._eventsCount === 0)\n this._events = objectCreate(null);\n else {\n delete events[type];\n if (events.removeListener)\n this.emit('removeListener', type, list.listener || listener);\n }\n } else if (typeof list !== 'function') {\n position = -1;\n\n for (i = list.length - 1; i >= 0; i--) {\n if (list[i] === listener || list[i].listener === listener) {\n originalListener = list[i].listener;\n position = i;\n break;\n }\n }\n\n if (position < 0)\n return this;\n\n if (position === 0)\n list.shift();\n else\n spliceOne(list, position);\n\n if (list.length === 1)\n events[type] = list[0];\n\n if (events.removeListener)\n this.emit('removeListener', type, originalListener || listener);\n }\n\n return this;\n };\n\nEventEmitter.prototype.removeAllListeners =\n function removeAllListeners(type) {\n var listeners, events, i;\n\n events = this._events;\n if (!events)\n return this;\n\n // not listening for removeListener, no need to emit\n if (!events.removeListener) {\n if (arguments.length === 0) {\n this._events = objectCreate(null);\n this._eventsCount = 0;\n } else if (events[type]) {\n if (--this._eventsCount === 0)\n this._events = objectCreate(null);\n else\n delete events[type];\n }\n return this;\n }\n\n // emit removeListener for all listeners on all events\n if (arguments.length === 0) {\n var keys = objectKeys(events);\n var key;\n for (i = 0; i < keys.length; ++i) {\n key = keys[i];\n if (key === 'removeListener') continue;\n this.removeAllListeners(key);\n }\n this.removeAllListeners('removeListener');\n this._events = objectCreate(null);\n this._eventsCount = 0;\n return this;\n }\n\n listeners = events[type];\n\n if (typeof listeners === 'function') {\n this.removeListener(type, listeners);\n } else if (listeners) {\n // LIFO order\n for (i = listeners.length - 1; i >= 0; i--) {\n this.removeListener(type, listeners[i]);\n }\n }\n\n return this;\n };\n\nfunction _listeners(target, type, unwrap) {\n var events = target._events;\n\n if (!events)\n return [];\n\n var evlistener = events[type];\n if (!evlistener)\n return [];\n\n if (typeof evlistener === 'function')\n return unwrap ? [evlistener.listener || evlistener] : [evlistener];\n\n return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);\n}\n\nEventEmitter.prototype.listeners = function listeners(type) {\n return _listeners(this, type, true);\n};\n\nEventEmitter.prototype.rawListeners = function rawListeners(type) {\n return _listeners(this, type, false);\n};\n\nEventEmitter.listenerCount = function(emitter, type) {\n if (typeof emitter.listenerCount === 'function') {\n return emitter.listenerCount(type);\n } else {\n return listenerCount.call(emitter, type);\n }\n};\n\nEventEmitter.prototype.listenerCount = listenerCount;\nfunction listenerCount(type) {\n var events = this._events;\n\n if (events) {\n var evlistener = events[type];\n\n if (typeof evlistener === 'function') {\n return 1;\n } else if (evlistener) {\n return evlistener.length;\n }\n }\n\n return 0;\n}\n\nEventEmitter.prototype.eventNames = function eventNames() {\n return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : [];\n};\n\n// About 1.5x faster than the two-arg version of Array#splice().\nfunction spliceOne(list, index) {\n for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1)\n list[i] = list[k];\n list.pop();\n}\n\nfunction arrayClone(arr, n) {\n var copy = new Array(n);\n for (var i = 0; i < n; ++i)\n copy[i] = arr[i];\n return copy;\n}\n\nfunction unwrapListeners(arr) {\n var ret = new Array(arr.length);\n for (var i = 0; i < ret.length; ++i) {\n ret[i] = arr[i].listener || arr[i];\n }\n return ret;\n}\n\nfunction objectCreatePolyfill(proto) {\n var F = function() {};\n F.prototype = proto;\n return new F;\n}\nfunction objectKeysPolyfill(obj) {\n var keys = [];\n for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) {\n keys.push(k);\n }\n return k;\n}\nfunction functionBindPolyfill(context) {\n var fn = this;\n return function () {\n return fn.apply(context, arguments);\n };\n}\n\n},{}]},{},[3])(3)\n});\n"],"sourceRoot":"../lib"} \ No newline at end of file diff --git a/lib/dispatcher.js b/lib/dispatcher.js new file mode 100644 index 0000000..a59a827 --- /dev/null +++ b/lib/dispatcher.js @@ -0,0 +1,64 @@ +"use strict"; +/* +* Web Bluetooth DFU +* Copyright (c) 2018 Rob Moran +* +* The MIT License (MIT) +* +* 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. +*/ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +var events_1 = require("events"); +/** + * @hidden + */ +var EventDispatcher = /** @class */ (function (_super) { + __extends(EventDispatcher, _super); + function EventDispatcher() { + return _super !== null && _super.apply(this, arguments) || this; + } + // tslint:disable-next-line:array-type + EventDispatcher.prototype.addEventListener = function (event, listener) { + return _super.prototype.addListener.call(this, event, listener); + }; + // tslint:disable-next-line:array-type + EventDispatcher.prototype.removeEventListener = function (event, listener) { + return _super.prototype.removeListener.call(this, event, listener); + }; + EventDispatcher.prototype.dispatchEvent = function (eventType, event) { + return _super.prototype.emit.call(this, eventType, event); + }; + return EventDispatcher; +}(events_1.EventEmitter)); +exports.EventDispatcher = EventDispatcher; + +//# sourceMappingURL=dispatcher.js.map diff --git a/lib/dispatcher.js.map b/lib/dispatcher.js.map new file mode 100644 index 0000000..b0188c9 --- /dev/null +++ b/lib/dispatcher.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["dispatcher.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;EAuBE;;;;;;;;;;;;;;;AAEF,iCAAsC;AAEtC;;GAEG;AACH;IAAqC,mCAAY;IAAjD;;IAeA,CAAC;IAbG,sCAAsC;IAC/B,0CAAgB,GAAvB,UAAwB,KAAsB,EAAE,QAAkC;QAC9E,OAAO,iBAAM,WAAW,YAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED,sCAAsC;IAC/B,6CAAmB,GAA1B,UAA2B,KAAsB,EAAE,QAAkC;QACjF,OAAO,iBAAM,cAAc,YAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACjD,CAAC;IAEM,uCAAa,GAApB,UAAqB,SAA0B,EAAE,KAAW;QACxD,OAAO,iBAAM,IAAI,YAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IACxC,CAAC;IACL,sBAAC;AAAD,CAfA,AAeC,CAfoC,qBAAY,GAehD;AAfY,0CAAe","file":"dispatcher.js","sourcesContent":["/*\n* Web Bluetooth DFU\n* Copyright (c) 2018 Rob Moran\n*\n* The MIT License (MIT)\n*\n* Permission is hereby granted, free of charge, to any person obtaining a copy\n* of this software and associated documentation files (the \"Software\"), to deal\n* in the Software without restriction, including without limitation the rights\n* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n* copies of the Software, and to permit persons to whom the Software is\n* furnished to do so, subject to the following conditions:\n*\n* The above copyright notice and this permission notice shall be included in all\n* copies or substantial portions of the Software.\n*\n* THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n* SOFTWARE.\n*/\n\nimport { EventEmitter } from \"events\";\n\n/**\n * @hidden\n */\nexport class EventDispatcher extends EventEmitter {\n\n // tslint:disable-next-line:array-type\n public addEventListener(event: string | symbol, listener: (...args: any[]) => void) {\n return super.addListener(event, listener);\n }\n\n // tslint:disable-next-line:array-type\n public removeEventListener(event: string | symbol, listener: (...args: any[]) => void) {\n return super.removeListener(event, listener);\n }\n\n public dispatchEvent(eventType: string | symbol, event?: any) {\n return super.emit(eventType, event);\n }\n}\n"],"sourceRoot":"../src"} \ No newline at end of file diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..b215a8a --- /dev/null +++ b/lib/index.js @@ -0,0 +1,29 @@ +"use strict"; +/* +* Web Bluetooth DFU +* Copyright (c) 2018 Rob Moran +* +* The MIT License (MIT) +* +* 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. +*/ +var secure_dfu_1 = require("./secure-dfu"); +module.exports = secure_dfu_1.SecureDfu; + +//# sourceMappingURL=index.js.map diff --git a/lib/index.js.map b/lib/index.js.map new file mode 100644 index 0000000..40e5f8b --- /dev/null +++ b/lib/index.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;EAuBE;AAEF,2CAAyC;AACzC,iBAAS,sBAAS,CAAC","file":"index.js","sourcesContent":["/*\n* Web Bluetooth DFU\n* Copyright (c) 2018 Rob Moran\n*\n* The MIT License (MIT)\n*\n* Permission is hereby granted, free of charge, to any person obtaining a copy\n* of this software and associated documentation files (the \"Software\"), to deal\n* in the Software without restriction, including without limitation the rights\n* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n* copies of the Software, and to permit persons to whom the Software is\n* furnished to do so, subject to the following conditions:\n*\n* The above copyright notice and this permission notice shall be included in all\n* copies or substantial portions of the Software.\n*\n* THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n* SOFTWARE.\n*/\n\nimport { SecureDfu } from \"./secure-dfu\";\nexport = SecureDfu;\n"],"sourceRoot":"../src"} \ No newline at end of file diff --git a/lib/secure-dfu.js b/lib/secure-dfu.js new file mode 100644 index 0000000..8c9ac9a --- /dev/null +++ b/lib/secure-dfu.js @@ -0,0 +1,504 @@ +"use strict"; +/* +* Web Bluetooth DFU +* Copyright (c) 2018 Rob Moran +* +* The MIT License (MIT) +* +* 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. +*/ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var dispatcher_1 = require("./dispatcher"); +var CONTROL_UUID = "8ec90001-f315-4f60-9fb8-838830daea50"; +var PACKET_UUID = "8ec90002-f315-4f60-9fb8-838830daea50"; +var BUTTON_UUID = "8ec90003-f315-4f60-9fb8-838830daea50"; +var LITTLE_ENDIAN = true; +var PACKET_SIZE = 20; +var OPERATIONS = { + BUTTON_COMMAND: [0x01], + CREATE_COMMAND: [0x01, 0x01], + CREATE_DATA: [0x01, 0x02], + RECEIPT_NOTIFICATIONS: [0x02], + CACULATE_CHECKSUM: [0x03], + EXECUTE: [0x04], + SELECT_COMMAND: [0x06, 0x01], + SELECT_DATA: [0x06, 0x02], + RESPONSE: [0x60, 0x20] +}; +var RESPONSE = { + // Invalid code + 0x00: "Invalid opcode", + // Success + 0x01: "Operation successful", + // Opcode not supported + 0x02: "Opcode not supported", + // Invalid parameter + 0x03: "Missing or invalid parameter value", + // Insufficient resources + 0x04: "Not enough memory for the data object", + // Invalid object + 0x05: "Data object does not match the firmware and hardware requirements, the signature is wrong, or parsing the command failed", + // Unsupported type + 0x07: "Not a valid object type for a Create request", + // Operation not permitted + 0x08: "The state of the DFU process does not allow this operation", + // Operation failed + 0x0A: "Operation failed", + // Extended error + 0x0B: "Extended error" +}; +var EXTENDED_ERROR = { + // No error + 0x00: "No extended error code has been set. This error indicates an implementation problem", + // Invalid error code + 0x01: "Invalid error code. This error code should never be used outside of development", + // Wrong command format + 0x02: "The format of the command was incorrect", + // Unknown command + 0x03: "The command was successfully parsed, but it is not supported or unknown", + // Init command invalid + 0x04: "The init command is invalid. The init packet either has an invalid update type or it is missing required fields for the update type", + // Firmware version failure + 0x05: "The firmware version is too low. For an application, the version must be greater than the current application. For a bootloader, it must be greater than or equal to the current version", + // Hardware version failure + 0x06: "The hardware version of the device does not match the required hardware version for the update", + // Softdevice version failure + 0x07: "The array of supported SoftDevices for the update does not contain the FWID of the current SoftDevice", + // Signature missing + 0x08: "The init packet does not contain a signature", + // Wrong hash type + 0x09: "The hash type that is specified by the init packet is not supported by the DFU bootloader", + // Hash failed + 0x0A: "The hash of the firmware image cannot be calculated", + // Wrong signature type + 0x0B: "The type of the signature is unknown or not supported by the DFU bootloader", + // Verification failed + 0x0C: "The hash of the received firmware image does not match the hash in the init packet", + // Insufficient space + 0x0D: "The available space on the device is insufficient to hold the firmware" +}; +/** + * Secure Device Firmware Update class + */ +var SecureDfu = /** @class */ (function (_super) { + __extends(SecureDfu, _super); + /** + * Characteristic constructor + * @param bluetooth A bluetooth instance + * @param crc32 A CRC32 function + * @param delay Milliseconds of delay between packets + */ + function SecureDfu(crc32, bluetooth, delay) { + if (delay === void 0) { delay = 0; } + var _this = _super.call(this) || this; + _this.crc32 = crc32; + _this.bluetooth = bluetooth; + _this.delay = delay; + _this.DEFAULT_UUIDS = { + service: SecureDfu.SERVICE_UUID, + button: BUTTON_UUID, + control: CONTROL_UUID, + packet: PACKET_UUID + }; + _this.notifyFns = {}; + _this.controlChar = null; + _this.packetChar = null; + if (!_this.bluetooth && window && window.navigator && window.navigator.bluetooth) { + _this.bluetooth = navigator.bluetooth; + } + return _this; + } + SecureDfu.prototype.log = function (message) { + this.dispatchEvent(SecureDfu.EVENT_LOG, { + message: message + }); + }; + SecureDfu.prototype.progress = function (bytes) { + this.dispatchEvent(SecureDfu.EVENT_PROGRESS, { + object: "unknown", + totalBytes: 0, + currentBytes: bytes + }); + }; + SecureDfu.prototype.connect = function (device) { + var _this = this; + device.addEventListener("gattserverdisconnected", function () { + _this.notifyFns = {}; + _this.controlChar = null; + _this.packetChar = null; + }); + return this.gattConnect(device) + .then(function (characteristics) { + _this.log("found " + characteristics.length + " characteristic(s)"); + _this.packetChar = characteristics.find(function (characteristic) { + return (characteristic.uuid === PACKET_UUID); + }); + if (!_this.packetChar) + throw new Error("Unable to find packet characteristic"); + _this.log("found packet characteristic"); + _this.controlChar = characteristics.find(function (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(function () { + _this.controlChar.addEventListener("characteristicvaluechanged", _this.handleNotification.bind(_this)); + _this.log("enabled control notifications"); + return device; + }); + }; + SecureDfu.prototype.gattConnect = function (device, serviceUUID) { + var _this = this; + if (serviceUUID === void 0) { serviceUUID = SecureDfu.SERVICE_UUID; } + return Promise.resolve() + .then(function () { + if (device.gatt.connected) + return device.gatt; + return device.gatt.connect(); + }) + .then(function (server) { + _this.log("connected to gatt server"); + return server.getPrimaryService(serviceUUID) + .catch(function () { + throw new Error("Unable to find DFU service"); + }); + }) + .then(function (service) { + _this.log("found DFU service"); + return service.getCharacteristics(); + }); + }; + SecureDfu.prototype.handleNotification = function (event) { + var view = event.target.value; + if (OPERATIONS.RESPONSE.indexOf(view.getUint8(0)) < 0) { + throw new Error("Unrecognised control characteristic response notification"); + } + var operation = view.getUint8(1); + if (this.notifyFns[operation]) { + var result = view.getUint8(2); + var error = null; + if (result === 0x01) { + var data = new DataView(view.buffer, 3); + this.notifyFns[operation].resolve(data); + } + else if (result === 0x0B) { + var code = view.getUint8(3); + error = "Error: " + EXTENDED_ERROR[code]; + } + else { + error = "Error: " + RESPONSE[result]; + } + if (error) { + this.log("notify: " + error); + this.notifyFns[operation].reject(error); + } + delete this.notifyFns[operation]; + } + }; + SecureDfu.prototype.sendOperation = function (characteristic, operation, buffer) { + var _this = this; + return new Promise(function (resolve, reject) { + var size = operation.length; + if (buffer) + size += buffer.byteLength; + var value = new Uint8Array(size); + value.set(operation); + if (buffer) { + var data = new Uint8Array(buffer); + value.set(data, operation.length); + } + _this.notifyFns[operation[0]] = { + resolve: resolve, + reject: reject + }; + characteristic.writeValue(value) + .catch(function (e) { + _this.log(e); + return Promise.resolve() + .then(function () { return _this.delayPromise(500); }) + // Retry once + .then(function () { return characteristic.writeValue(value); }); + }); + }); + }; + SecureDfu.prototype.sendControl = function (operation, buffer) { + var _this = this; + return new Promise(function (resolve) { + _this.sendOperation(_this.controlChar, operation, buffer) + .then(function (resp) { + setTimeout(function () { return resolve(resp); }, _this.delay); + }); + }); + }; + SecureDfu.prototype.transferInit = function (buffer) { + 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) { + var _this = this; + return this.sendControl(selectType) + .then(function (response) { + var maxSize = response.getUint32(0, LITTLE_ENDIAN); + var offset = response.getUint32(4, LITTLE_ENDIAN); + var crc = response.getInt32(8, LITTLE_ENDIAN); + if (type === "init" && offset === buffer.byteLength && _this.checkCrc(buffer, crc)) { + _this.log("init packet already available, skipping transfer"); + return; + } + _this.progress = function (bytes) { + _this.dispatchEvent(SecureDfu.EVENT_PROGRESS, { + object: type, + totalBytes: buffer.byteLength, + currentBytes: bytes + }); + }; + _this.progress(0); + return _this.transferObject(buffer, createType, maxSize, offset); + }); + }; + SecureDfu.prototype.transferObject = function (buffer, createType, maxSize, offset) { + var _this = this; + var start = offset - offset % maxSize; + var end = Math.min(start + maxSize, buffer.byteLength); + var view = new DataView(new ArrayBuffer(4)); + view.setUint32(0, end - start, LITTLE_ENDIAN); + return this.sendControl(createType, view.buffer) + .then(function () { + var data = buffer.slice(start, end); + return _this.transferData(data, start); + }) + .then(function () { + return _this.sendControl(OPERATIONS.CACULATE_CHECKSUM); + }) + .then(function (response) { + var crc = response.getInt32(4, LITTLE_ENDIAN); + var transferred = response.getUint32(0, LITTLE_ENDIAN); + var data = buffer.slice(0, transferred); + if (_this.checkCrc(data, crc)) { + _this.log("written " + transferred + " bytes"); + offset = transferred; + return _this.sendControl(OPERATIONS.EXECUTE); + } + else { + _this.log("object failed to validate"); + } + }) + .then(function () { + if (end < buffer.byteLength) { + return _this.transferObject(buffer, createType, maxSize, offset); + } + else { + _this.log("transfer complete"); + } + }); + }; + SecureDfu.prototype.transferData = function (data, offset, start) { + var _this = this; + start = start || 0; + var end = Math.min(start + PACKET_SIZE, data.byteLength); + var packet = data.slice(start, end); + return this.packetChar.writeValue(packet) + .then(function () { return _this.delayPromise(_this.delay); }) + .then(function () { + _this.progress(offset + end); + if (end < data.byteLength) { + return _this.transferData(data, offset, end); + } + }); + }; + SecureDfu.prototype.checkCrc = function (buffer, crc) { + if (!this.crc32) { + this.log("crc32 not found, skipping CRC check"); + return true; + } + return crc === this.crc32(new Uint8Array(buffer)); + }; + SecureDfu.prototype.delayPromise = function (delay) { + return new Promise(function (resolve) { + setTimeout(resolve, delay); + }); + }; + /** + * Scans for a device to update + * @param buttonLess Scans for all devices and will automatically call `setDfuMode` + * @param filters Alternative filters to use when scanning + * @param uuids Optional alternative uuids for service, control, packet or button + * @returns Promise containing the device + */ + SecureDfu.prototype.requestDevice = function (buttonLess, filters, uuids) { + var _this = this; + if (uuids === void 0) { uuids = this.DEFAULT_UUIDS; } + uuids = __assign(__assign({}, this.DEFAULT_UUIDS), uuids); + if (!buttonLess && !filters) { + filters = [{ services: [uuids.service] }]; + } + var options = { + optionalServices: [uuids.service] + }; + if (filters) + options.filters = filters; + else + options.acceptAllDevices = true; + return this.bluetooth.requestDevice(options) + .then(function (device) { + if (buttonLess) { + return _this.setDfuMode(device, uuids); + } + return device; + }); + }; + /** + * Sets the DFU mode of a device, preparing it for update + * @param device The device to switch mode + * @param uuids Optional alternative uuids for control, packet or button + * @returns Promise containing the device if it is still on a valid state + */ + SecureDfu.prototype.setDfuMode = function (device, uuids) { + var _this = this; + if (uuids === void 0) { uuids = this.DEFAULT_UUIDS; } + uuids = __assign(__assign({}, this.DEFAULT_UUIDS), uuids); + return this.gattConnect(device, uuids.service) + .then(function (characteristics) { + _this.log("found " + characteristics.length + " characteristic(s)"); + var controlChar = characteristics.find(function (characteristic) { + return (characteristic.uuid === uuids.control); + }); + var packetChar = characteristics.find(function (characteristic) { + return (characteristic.uuid === uuids.packet); + }); + if (controlChar && packetChar) { + return device; + } + var buttonChar = characteristics.find(function (characteristic) { + return (characteristic.uuid === uuids.button); + }); + 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 new Promise(function (resolve, _reject) { + function complete() { + this.notifyFns = {}; + // Resolve with null device as it needs reconnecting + resolve(null); + } + buttonChar.startNotifications() + .then(function () { + _this.log("enabled buttonless notifications"); + device.addEventListener("gattserverdisconnected", complete.bind(_this)); + buttonChar.addEventListener("characteristicvaluechanged", _this.handleNotification.bind(_this)); + return _this.sendOperation(buttonChar, OPERATIONS.BUTTON_COMMAND); + }) + .then(function () { + _this.log("sent DFU mode"); + complete(); + }); + }); + }); + }; + /** + * Updates a device + * @param device The device to switch mode + * @param init The initialisation packet to send + * @param firmware The firmware to update + * @returns Promise containing the device + */ + SecureDfu.prototype.update = function (device, init, firmware) { + var _this = this; + return new Promise(function (resolve, reject) { + if (!device) + return reject("Device not specified"); + if (!init) + return reject("Init not specified"); + if (!firmware) + return reject("Firmware not specified"); + _this.connect(device) + .then(function () { + _this.log("transferring init"); + return _this.transferInit(init); + }) + .then(function () { + _this.log("transferring firmware"); + return _this.transferFirmware(firmware); + }) + .then(function () { + _this.log("complete, disconnecting..."); + device.addEventListener("gattserverdisconnected", function () { + _this.log("disconnected"); + resolve(device); + }); + }) + .catch(function (error) { return reject(error); }); + }); + }; + /** + * DFU Service unique identifier + */ + SecureDfu.SERVICE_UUID = 0xFE59; + /** + * Log event + * @event + */ + SecureDfu.EVENT_LOG = "log"; + /** + * Progress event + * @event + */ + SecureDfu.EVENT_PROGRESS = "progress"; + return SecureDfu; +}(dispatcher_1.EventDispatcher)); +exports.SecureDfu = SecureDfu; + +//# sourceMappingURL=secure-dfu.js.map diff --git a/lib/secure-dfu.js.map b/lib/secure-dfu.js.map new file mode 100644 index 0000000..5983a0d --- /dev/null +++ b/lib/secure-dfu.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["secure-dfu.ts"],"names":[],"mappings":"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","file":"secure-dfu.js","sourcesContent":["/*\n* Web Bluetooth DFU\n* Copyright (c) 2018 Rob Moran\n*\n* The MIT License (MIT)\n*\n* Permission is hereby granted, free of charge, to any person obtaining a copy\n* of this software and associated documentation files (the \"Software\"), to deal\n* in the Software without restriction, including without limitation the rights\n* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n* copies of the Software, and to permit persons to whom the Software is\n* furnished to do so, subject to the following conditions:\n*\n* The above copyright notice and this permission notice shall be included in all\n* copies or substantial portions of the Software.\n*\n* THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n* SOFTWARE.\n*/\n\nimport { EventDispatcher } from \"./dispatcher\";\n\nconst CONTROL_UUID = \"8ec90001-f315-4f60-9fb8-838830daea50\";\nconst PACKET_UUID = \"8ec90002-f315-4f60-9fb8-838830daea50\";\nconst BUTTON_UUID = \"8ec90003-f315-4f60-9fb8-838830daea50\";\n\nconst LITTLE_ENDIAN = true;\nconst PACKET_SIZE = 20;\n\nconst OPERATIONS = {\n BUTTON_COMMAND: [ 0x01 ],\n CREATE_COMMAND: [ 0x01, 0x01 ],\n CREATE_DATA: [ 0x01, 0x02 ],\n RECEIPT_NOTIFICATIONS: [ 0x02 ],\n CACULATE_CHECKSUM: [ 0x03 ],\n EXECUTE: [ 0x04 ],\n SELECT_COMMAND: [ 0x06, 0x01 ],\n SELECT_DATA: [ 0x06, 0x02 ],\n RESPONSE: [ 0x60, 0x20 ]\n};\n\nconst RESPONSE = {\n // Invalid code\n 0x00: \"Invalid opcode\",\n // Success\n 0x01: \"Operation successful\",\n // Opcode not supported\n 0x02: \"Opcode not supported\",\n // Invalid parameter\n 0x03: \"Missing or invalid parameter value\",\n // Insufficient resources\n 0x04: \"Not enough memory for the data object\",\n // Invalid object\n 0x05: \"Data object does not match the firmware and hardware requirements, the signature is wrong, or parsing the command failed\",\n // Unsupported type\n 0x07: \"Not a valid object type for a Create request\",\n // Operation not permitted\n 0x08: \"The state of the DFU process does not allow this operation\",\n // Operation failed\n 0x0A: \"Operation failed\",\n // Extended error\n 0x0B: \"Extended error\"\n};\n\nconst EXTENDED_ERROR = {\n // No error\n 0x00: \"No extended error code has been set. This error indicates an implementation problem\",\n // Invalid error code\n 0x01: \"Invalid error code. This error code should never be used outside of development\",\n // Wrong command format\n 0x02: \"The format of the command was incorrect\",\n // Unknown command\n 0x03: \"The command was successfully parsed, but it is not supported or unknown\",\n // Init command invalid\n 0x04: \"The init command is invalid. The init packet either has an invalid update type or it is missing required fields for the update type\",\n // Firmware version failure\n 0x05: \"The firmware version is too low. For an application, the version must be greater than the current application. For a bootloader, it must be greater than or equal to the current version\",\n // Hardware version failure\n 0x06: \"The hardware version of the device does not match the required hardware version for the update\",\n // Softdevice version failure\n 0x07: \"The array of supported SoftDevices for the update does not contain the FWID of the current SoftDevice\",\n // Signature missing\n 0x08: \"The init packet does not contain a signature\",\n // Wrong hash type\n 0x09: \"The hash type that is specified by the init packet is not supported by the DFU bootloader\",\n // Hash failed\n 0x0A: \"The hash of the firmware image cannot be calculated\",\n // Wrong signature type\n 0x0B: \"The type of the signature is unknown or not supported by the DFU bootloader\",\n // Verification failed\n 0x0C: \"The hash of the received firmware image does not match the hash in the init packet\",\n // Insufficient space\n 0x0D: \"The available space on the device is insufficient to hold the firmware\"\n};\n\n/**\n * BluetoothLE Scan Filter Init interface\n */\nexport interface BluetoothLEScanFilterInit {\n /**\n * An array of service UUIDs to filter on\n */\n services?: Array;\n\n /**\n * The device name to filter on\n */\n name?: string;\n\n /**\n * The device name prefix to filter on\n */\n namePrefix?: string;\n}\n\nexport interface UuidOptions {\n service?: number | string;\n button?: number | string;\n control?: number | string;\n packet?: number | string;\n}\n\n/**\n * Secure Device Firmware Update class\n */\nexport class SecureDfu extends EventDispatcher {\n\n /**\n * DFU Service unique identifier\n */\n public static SERVICE_UUID: number = 0xFE59;\n\n /**\n * Log event\n * @event\n */\n public static EVENT_LOG: string = \"log\";\n\n /**\n * Progress event\n * @event\n */\n public static EVENT_PROGRESS: string = \"progress\";\n\n private DEFAULT_UUIDS: UuidOptions = {\n service: SecureDfu.SERVICE_UUID,\n button: BUTTON_UUID,\n control: CONTROL_UUID,\n packet: PACKET_UUID\n };\n\n private notifyFns: {} = {};\n private controlChar: BluetoothRemoteGATTCharacteristic = null;\n private packetChar: BluetoothRemoteGATTCharacteristic = null;\n\n /**\n * Characteristic constructor\n * @param bluetooth A bluetooth instance\n * @param crc32 A CRC32 function\n * @param delay Milliseconds of delay between packets\n */\n constructor(private crc32: (data: Array | Uint8Array, seed?: number) => number,\n private bluetooth?: Bluetooth,\n private delay: number = 0) {\n super();\n\n if (!this.bluetooth && window && window.navigator && window.navigator.bluetooth) {\n this.bluetooth = navigator.bluetooth;\n }\n }\n\n private log(message: string) {\n this.dispatchEvent(SecureDfu.EVENT_LOG, {\n message: message\n });\n }\n\n private progress(bytes: number) {\n this.dispatchEvent(SecureDfu.EVENT_PROGRESS, {\n object: \"unknown\",\n totalBytes: 0,\n currentBytes: bytes\n });\n }\n\n private connect(device: BluetoothDevice): Promise {\n device.addEventListener(\"gattserverdisconnected\", () => {\n this.notifyFns = {};\n this.controlChar = null;\n this.packetChar = null;\n });\n\n return this.gattConnect(device)\n .then(characteristics => {\n this.log(`found ${characteristics.length} characteristic(s)`);\n\n this.packetChar = characteristics.find(characteristic => {\n return (characteristic.uuid === PACKET_UUID);\n });\n\n if (!this.packetChar) throw new Error(\"Unable to find packet characteristic\");\n this.log(\"found packet characteristic\");\n\n this.controlChar = characteristics.find(characteristic => {\n return (characteristic.uuid === CONTROL_UUID);\n });\n\n if (!this.controlChar) throw new Error(\"Unable to find control characteristic\");\n this.log(\"found control characteristic\");\n\n if (!this.controlChar.properties.notify && !this.controlChar.properties.indicate) {\n throw new Error(\"Control characteristic does not allow notifications\");\n }\n\n return this.controlChar.startNotifications();\n })\n .then(() => {\n this.controlChar.addEventListener(\"characteristicvaluechanged\", this.handleNotification.bind(this));\n this.log(\"enabled control notifications\");\n return device;\n });\n }\n\n private gattConnect(device: BluetoothDevice, serviceUUID: number | string = SecureDfu.SERVICE_UUID): Promise> {\n return Promise.resolve()\n .then(() => {\n if (device.gatt.connected) return device.gatt;\n return device.gatt.connect();\n })\n .then(server => {\n this.log(\"connected to gatt server\");\n return server.getPrimaryService(serviceUUID)\n .catch(() => {\n throw new Error(\"Unable to find DFU service\");\n });\n })\n .then(service => {\n this.log(\"found DFU service\");\n return service.getCharacteristics();\n });\n }\n\n private handleNotification(event: any) {\n const view = event.target.value;\n\n if (OPERATIONS.RESPONSE.indexOf(view.getUint8(0)) < 0) {\n throw new Error(\"Unrecognised control characteristic response notification\");\n }\n\n const operation = view.getUint8(1);\n if (this.notifyFns[operation]) {\n const result = view.getUint8(2);\n let error = null;\n\n if (result === 0x01) {\n const data = new DataView(view.buffer, 3);\n this.notifyFns[operation].resolve(data);\n } else if (result === 0x0B) {\n const code = view.getUint8(3);\n error = `Error: ${EXTENDED_ERROR[code]}`;\n } else {\n error = `Error: ${RESPONSE[result]}`;\n }\n\n if (error) {\n this.log(`notify: ${error}`);\n this.notifyFns[operation].reject(error);\n }\n\n delete this.notifyFns[operation];\n }\n }\n\n private sendOperation(characteristic: BluetoothRemoteGATTCharacteristic, operation: Array, buffer?: ArrayBuffer): Promise {\n return new Promise((resolve, reject) => {\n let size = operation.length;\n if (buffer) size += buffer.byteLength;\n\n const value = new Uint8Array(size);\n value.set(operation);\n\n if (buffer) {\n const data = new Uint8Array(buffer);\n value.set(data, operation.length);\n }\n\n this.notifyFns[operation[0]] = {\n resolve: resolve,\n reject: reject\n };\n\n characteristic.writeValue(value)\n .catch(e => {\n this.log(e);\n return Promise.resolve()\n .then(() => this.delayPromise(500))\n // Retry once\n .then(() => characteristic.writeValue(value));\n });\n });\n }\n\n private sendControl(operation: Array, buffer?: ArrayBuffer): Promise {\n return new Promise(resolve => {\n this.sendOperation(this.controlChar, operation, buffer)\n .then(resp => {\n setTimeout(() => resolve(resp), this.delay);\n });\n });\n }\n\n private transferInit(buffer: ArrayBuffer): Promise {\n return this.transfer(buffer, \"init\", OPERATIONS.SELECT_COMMAND, OPERATIONS.CREATE_COMMAND);\n }\n\n private transferFirmware(buffer: ArrayBuffer): Promise {\n return this.transfer(buffer, \"firmware\", OPERATIONS.SELECT_DATA, OPERATIONS.CREATE_DATA);\n }\n\n private transfer(buffer: ArrayBuffer, type: string, selectType: Array, createType: Array): Promise {\n return this.sendControl(selectType)\n .then(response => {\n\n const maxSize = response.getUint32(0, LITTLE_ENDIAN);\n const offset = response.getUint32(4, LITTLE_ENDIAN);\n const crc = response.getInt32(8, LITTLE_ENDIAN);\n\n if (type === \"init\" && offset === buffer.byteLength && this.checkCrc(buffer, crc)) {\n this.log(\"init packet already available, skipping transfer\");\n return;\n }\n\n this.progress = bytes => {\n this.dispatchEvent(SecureDfu.EVENT_PROGRESS, {\n object: type,\n totalBytes: buffer.byteLength,\n currentBytes: bytes\n });\n };\n this.progress(0);\n\n return this.transferObject(buffer, createType, maxSize, offset);\n });\n }\n\n private transferObject(buffer: ArrayBuffer, createType: Array, maxSize: number, offset: number): Promise {\n const start = offset - offset % maxSize;\n const end = Math.min(start + maxSize, buffer.byteLength);\n\n const view = new DataView(new ArrayBuffer(4));\n view.setUint32(0, end - start, LITTLE_ENDIAN);\n\n return this.sendControl(createType, view.buffer)\n .then(() => {\n const data = buffer.slice(start, end);\n return this.transferData(data, start);\n })\n .then(() => {\n return this.sendControl(OPERATIONS.CACULATE_CHECKSUM);\n })\n .then(response => {\n const crc = response.getInt32(4, LITTLE_ENDIAN);\n const transferred = response.getUint32(0, LITTLE_ENDIAN);\n const data = buffer.slice(0, transferred);\n\n if (this.checkCrc(data, crc)) {\n this.log(`written ${transferred} bytes`);\n offset = transferred;\n return this.sendControl(OPERATIONS.EXECUTE);\n } else {\n this.log(\"object failed to validate\");\n }\n })\n .then(() => {\n if (end < buffer.byteLength) {\n return this.transferObject(buffer, createType, maxSize, offset);\n } else {\n this.log(\"transfer complete\");\n }\n });\n }\n\n private transferData(data: ArrayBuffer, offset: number, start?: number) {\n start = start || 0;\n const end = Math.min(start + PACKET_SIZE, data.byteLength);\n const packet = data.slice(start, end);\n\n return this.packetChar.writeValue(packet)\n .then(() => this.delayPromise(this.delay))\n .then(() => {\n this.progress(offset + end);\n\n if (end < data.byteLength) {\n return this.transferData(data, offset, end);\n }\n });\n }\n\n private checkCrc(buffer: ArrayBuffer, crc: number): boolean {\n if (!this.crc32) {\n this.log(\"crc32 not found, skipping CRC check\");\n return true;\n }\n\n return crc === this.crc32(new Uint8Array(buffer));\n }\n\n private delayPromise(delay: number) {\n return new Promise(resolve => {\n setTimeout(resolve, delay);\n });\n }\n\n /**\n * Scans for a device to update\n * @param buttonLess Scans for all devices and will automatically call `setDfuMode`\n * @param filters Alternative filters to use when scanning\n * @param uuids Optional alternative uuids for service, control, packet or button\n * @returns Promise containing the device\n */\n public requestDevice(buttonLess: boolean, filters: Array, uuids: UuidOptions = this.DEFAULT_UUIDS): Promise {\n uuids = {\n ...this.DEFAULT_UUIDS,\n ...uuids\n };\n\n if (!buttonLess && !filters) {\n filters = [ { services: [ uuids.service ] } ];\n }\n\n const options: any = {\n optionalServices: [ uuids.service ]\n };\n\n if (filters) options.filters = filters;\n else options.acceptAllDevices = true;\n\n return this.bluetooth.requestDevice(options)\n .then(device => {\n if (buttonLess) {\n return this.setDfuMode(device, uuids);\n }\n return device;\n });\n }\n\n /**\n * Sets the DFU mode of a device, preparing it for update\n * @param device The device to switch mode\n * @param uuids Optional alternative uuids for control, packet or button\n * @returns Promise containing the device if it is still on a valid state\n */\n public setDfuMode(device: BluetoothDevice, uuids: UuidOptions = this.DEFAULT_UUIDS): Promise {\n uuids = {\n ...this.DEFAULT_UUIDS,\n ...uuids\n };\n\n return this.gattConnect(device, uuids.service)\n .then(characteristics => {\n this.log(`found ${characteristics.length} characteristic(s)`);\n\n const controlChar = characteristics.find(characteristic => {\n return (characteristic.uuid === uuids.control);\n });\n\n const packetChar = characteristics.find(characteristic => {\n return (characteristic.uuid === uuids.packet);\n });\n\n if (controlChar && packetChar) {\n return device;\n }\n\n const buttonChar = characteristics.find(characteristic => {\n return (characteristic.uuid === uuids.button);\n });\n\n if (!buttonChar) {\n throw new Error(\"Unsupported device\");\n }\n\n // Support buttonless devices\n this.log(\"found buttonless characteristic\");\n if (!buttonChar.properties.notify && !buttonChar.properties.indicate) {\n throw new Error(\"Buttonless characteristic does not allow notifications\");\n }\n\n return new Promise((resolve, _reject) => {\n\n function complete() {\n this.notifyFns = {};\n // Resolve with null device as it needs reconnecting\n resolve(null);\n }\n\n buttonChar.startNotifications()\n .then(() => {\n this.log(\"enabled buttonless notifications\");\n\n device.addEventListener(\"gattserverdisconnected\", complete.bind(this));\n buttonChar.addEventListener(\"characteristicvaluechanged\", this.handleNotification.bind(this));\n\n return this.sendOperation(buttonChar, OPERATIONS.BUTTON_COMMAND);\n })\n .then(() => {\n this.log(\"sent DFU mode\");\n complete();\n });\n });\n });\n }\n\n /**\n * Updates a device\n * @param device The device to switch mode\n * @param init The initialisation packet to send\n * @param firmware The firmware to update\n * @returns Promise containing the device\n */\n public update(device: BluetoothDevice, init: ArrayBuffer, firmware: ArrayBuffer): Promise {\n return new Promise((resolve, reject) => {\n if (!device) return reject(\"Device not specified\");\n if (!init) return reject(\"Init not specified\");\n if (!firmware) return reject(\"Firmware not specified\");\n\n this.connect(device)\n .then(() => {\n this.log(\"transferring init\");\n return this.transferInit(init);\n })\n .then(() => {\n this.log(\"transferring firmware\");\n return this.transferFirmware(firmware);\n })\n .then(() => {\n this.log(\"complete, disconnecting...\");\n\n device.addEventListener(\"gattserverdisconnected\", () => {\n this.log(\"disconnected\");\n resolve(device);\n });\n })\n .catch(error => reject(error));\n });\n }\n}\n"],"sourceRoot":"../src"} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6792241..7b8d0bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "web-bluetooth-dfu", - "version": "1.2.0", + "version": "1.2.1-next.b285922.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1191,8 +1191,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "constants-browserify": { "version": "1.0.0", @@ -3365,7 +3364,6 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3610,8 +3608,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "dev": true, - "optional": true + "dev": true }, "npm-packlist": { "version": "1.4.8", @@ -5571,8 +5568,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "optional": true + "dev": true }, "yargs": { "version": "7.1.0", diff --git a/package.json b/package.json index 31631e5..2c47bc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-bluetooth-dfu", - "version": "1.2.0", + "version": "1.2.1-next.b285922.0", "description": "Device firmware update with Web Bluetooth", "homepage": "https://thegecko.github.io/web-bluetooth-dfu/", "author": "Rob Moran ", diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts new file mode 100644 index 0000000..1218aad --- /dev/null +++ b/types/dispatcher.d.ts @@ -0,0 +1,10 @@ +/// +import { EventEmitter } from "events"; +/** + * @hidden + */ +export declare class EventDispatcher extends EventEmitter { + addEventListener(event: string | symbol, listener: (...args: any[]) => void): this; + removeEventListener(event: string | symbol, listener: (...args: any[]) => void): this; + dispatchEvent(eventType: string | symbol, event?: any): boolean; +} diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..d97df27 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,2 @@ +import { SecureDfu } from "./secure-dfu"; +export = SecureDfu; diff --git a/types/secure-dfu.d.ts b/types/secure-dfu.d.ts new file mode 100644 index 0000000..8fd4929 --- /dev/null +++ b/types/secure-dfu.d.ts @@ -0,0 +1,95 @@ +/// +import { EventDispatcher } from "./dispatcher"; +/** + * BluetoothLE Scan Filter Init interface + */ +export interface BluetoothLEScanFilterInit { + /** + * An array of service UUIDs to filter on + */ + services?: Array; + /** + * The device name to filter on + */ + name?: string; + /** + * The device name prefix to filter on + */ + namePrefix?: string; +} +export interface UuidOptions { + service?: number | string; + button?: number | string; + control?: number | string; + packet?: number | string; +} +/** + * Secure Device Firmware Update class + */ +export declare class SecureDfu extends EventDispatcher { + private crc32; + private bluetooth?; + private delay; + /** + * DFU Service unique identifier + */ + static SERVICE_UUID: number; + /** + * Log event + * @event + */ + static EVENT_LOG: string; + /** + * Progress event + * @event + */ + static EVENT_PROGRESS: string; + private DEFAULT_UUIDS; + private notifyFns; + private controlChar; + private packetChar; + /** + * Characteristic constructor + * @param bluetooth A bluetooth instance + * @param crc32 A CRC32 function + * @param delay Milliseconds of delay between packets + */ + constructor(crc32: (data: Array | Uint8Array, seed?: number) => number, bluetooth?: Bluetooth, delay?: number); + private log; + private progress; + private connect; + private gattConnect; + private handleNotification; + private sendOperation; + private sendControl; + private transferInit; + private transferFirmware; + private transfer; + private transferObject; + private transferData; + private checkCrc; + private delayPromise; + /** + * Scans for a device to update + * @param buttonLess Scans for all devices and will automatically call `setDfuMode` + * @param filters Alternative filters to use when scanning + * @param uuids Optional alternative uuids for service, control, packet or button + * @returns Promise containing the device + */ + requestDevice(buttonLess: boolean, filters: Array, uuids?: UuidOptions): Promise; + /** + * Sets the DFU mode of a device, preparing it for update + * @param device The device to switch mode + * @param uuids Optional alternative uuids for control, packet or button + * @returns Promise containing the device if it is still on a valid state + */ + setDfuMode(device: BluetoothDevice, uuids?: UuidOptions): Promise; + /** + * Updates a device + * @param device The device to switch mode + * @param init The initialisation packet to send + * @param firmware The firmware to update + * @returns Promise containing the device + */ + update(device: BluetoothDevice, init: ArrayBuffer, firmware: ArrayBuffer): Promise; +}