├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── n0100 ├── FileSaver.js ├── dfu-util.js ├── dfu.js ├── dfuse.js ├── index.html └── sakura.css ├── n0110 ├── FileSaver.js ├── dfu-util.js ├── dfu.js ├── dfuse.js ├── index.html └── sakura.css └── sakura.css /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vs/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Devan Lai 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | --- 16 | 17 | index.html + dfu-util.js parts modified by Adriweb - (c) 2018 18 | (no license change) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webdfu_numworks 2 | 3 | This is a fork from the [WebDFU](https://devanlai.github.io/webdfu/dfu-util/) demo (WebUSB DFU), but tailored for dumping/flashing the [NumWorks](https://numworks.com) calculator. 4 | 5 | 6 | ### Note from the original Readme: 7 | 8 | WebUSB is currently only supported by Chromium / Google Chrome. 9 | 10 | For Chrome to communicate with a USB device, it must have permission to access the device and the operating system must be able to load a generic driver that libusb can talk to. 11 | 12 | On Windows, that means that an appropriate WinUSB/libusb driver must first be installed. This can be done manually with programs such as [Zadig](http://zadig.akeo.ie/) or automatically (sometimes...) with [WCID](https://github.com/pbatard/libwdi/wiki/WCID-Devices) 13 | 14 | On Linux, that means that the current user must have permission to access the device. 15 | 16 | On macOS, it should work directly. 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebUSB DFU for NumWorks 6 | 21 | 22 | 23 |
24 |

WebDFU TI-Planet For Numworks

25 | Choose your NumWorks model:

N0100
N0110/N0115/N0120 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /n0100/FileSaver.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | if (typeof define === "function" && define.amd) { 3 | define([], factory); 4 | } else if (typeof exports !== "undefined") { 5 | factory(); 6 | } else { 7 | var mod = { 8 | exports: {} 9 | }; 10 | factory(); 11 | global.FileSaver = mod.exports; 12 | } 13 | })(this, function () { 14 | "use strict"; 15 | 16 | /* 17 | * FileSaver.js 18 | * A saveAs() FileSaver implementation. 19 | * 20 | * By Eli Grey, http://eligrey.com 21 | * 22 | * License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT) 23 | * source : http://purl.eligrey.com/github/FileSaver.js 24 | */ 25 | // The one and only way of getting global scope in all environments 26 | // https://stackoverflow.com/q/3277182/1008999 27 | var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : void 0; 28 | 29 | function bom(blob, opts) { 30 | if (typeof opts === 'undefined') opts = { 31 | autoBom: false 32 | };else if (typeof opts !== 'object') { 33 | console.warn('Depricated: Expected third argument to be a object'); 34 | opts = { 35 | autoBom: !opts 36 | }; 37 | } // prepend BOM for UTF-8 XML and text/* types (including HTML) 38 | // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF 39 | 40 | if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { 41 | return new Blob([String.fromCharCode(0xFEFF), blob], { 42 | type: blob.type 43 | }); 44 | } 45 | 46 | return blob; 47 | } 48 | 49 | function download(url, name, opts) { 50 | var xhr = new XMLHttpRequest(); 51 | xhr.open('GET', url); 52 | xhr.responseType = 'blob'; 53 | 54 | xhr.onload = function () { 55 | saveAs(xhr.response, name, opts); 56 | }; 57 | 58 | xhr.onerror = function () { 59 | console.error('could not download file'); 60 | }; 61 | 62 | xhr.send(); 63 | } 64 | 65 | function corsEnabled(url) { 66 | var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker 67 | 68 | xhr.open('HEAD', url, false); 69 | xhr.send(); 70 | return xhr.status >= 200 && xhr.status <= 299; 71 | } // `a.click()` doesn't work for all browsers (#465) 72 | 73 | 74 | function click(node) { 75 | try { 76 | node.dispatchEvent(new MouseEvent('click')); 77 | } catch (e) { 78 | var evt = document.createEvent('MouseEvents'); 79 | evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); 80 | node.dispatchEvent(evt); 81 | } 82 | } 83 | 84 | var saveAs = _global.saveAs || // probably in some web worker 85 | typeof window !== 'object' || window !== _global ? function saveAs() {} 86 | /* noop */ 87 | // Use download attribute first if possible (#193 Lumia mobile) 88 | : 'download' in HTMLAnchorElement.prototype ? function saveAs(blob, name, opts) { 89 | var URL = _global.URL || _global.webkitURL; 90 | var a = document.createElement('a'); 91 | name = name || blob.name || 'download'; 92 | a.download = name; 93 | a.rel = 'noopener'; // tabnabbing 94 | // TODO: detect chrome extensions & packaged apps 95 | // a.target = '_blank' 96 | 97 | if (typeof blob === 'string') { 98 | // Support regular links 99 | a.href = blob; 100 | 101 | if (a.origin !== location.origin) { 102 | corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank'); 103 | } else { 104 | click(a); 105 | } 106 | } else { 107 | // Support blobs 108 | a.href = URL.createObjectURL(blob); 109 | setTimeout(function () { 110 | URL.revokeObjectURL(a.href); 111 | }, 4E4); // 40s 112 | 113 | setTimeout(function () { 114 | click(a); 115 | }, 0); 116 | } 117 | } // Use msSaveOrOpenBlob as a second approach 118 | : 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) { 119 | name = name || blob.name || 'download'; 120 | 121 | if (typeof blob === 'string') { 122 | if (corsEnabled(blob)) { 123 | download(blob, name, opts); 124 | } else { 125 | var a = document.createElement('a'); 126 | a.href = blob; 127 | a.target = '_blank'; 128 | setTimeout(function () { 129 | click(a); 130 | }); 131 | } 132 | } else { 133 | navigator.msSaveOrOpenBlob(bom(blob, opts), name); 134 | } 135 | } // Fallback to using FileReader and a popup 136 | : function saveAs(blob, name, opts, popup) { 137 | // Open a popup immediately do go around popup blocker 138 | // Mostly only avalible on user interaction and the fileReader is async so... 139 | popup = popup || open('', '_blank'); 140 | 141 | if (popup) { 142 | popup.document.title = popup.document.body.innerText = 'downloading...'; 143 | } 144 | 145 | if (typeof blob === 'string') return download(blob, name, opts); 146 | var force = blob.type === 'application/octet-stream'; 147 | 148 | var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari; 149 | 150 | var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent); 151 | 152 | if ((isChromeIOS || force && isSafari) && typeof FileReader === 'object') { 153 | // Safari doesn't allow downloading of blob urls 154 | var reader = new FileReader(); 155 | 156 | reader.onloadend = function () { 157 | var url = reader.result; 158 | url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;'); 159 | if (popup) popup.location.href = url;else location = url; 160 | popup = null; // reverse-tabnabbing #460 161 | }; 162 | 163 | reader.readAsDataURL(blob); 164 | } else { 165 | var URL = _global.URL || _global.webkitURL; 166 | var url = URL.createObjectURL(blob); 167 | if (popup) popup.location = url;else location.href = url; 168 | popup = null; // reverse-tabnabbing #460 169 | 170 | setTimeout(function () { 171 | URL.revokeObjectURL(url); 172 | }, 4E4); // 40s 173 | } 174 | }; 175 | _global.saveAs = saveAs.saveAs = saveAs; 176 | 177 | if (typeof module !== 'undefined') { 178 | module.exports = saveAs; 179 | } 180 | }); -------------------------------------------------------------------------------- /n0100/dfu-util.js: -------------------------------------------------------------------------------- 1 | var device = null; 2 | (function() { 3 | 'use strict'; 4 | 5 | function hex4(n) { 6 | let s = n.toString(16) 7 | while (s.length < 4) { 8 | s = '0' + s; 9 | } 10 | return s; 11 | } 12 | 13 | function hexAddr8(n) { 14 | let s = n.toString(16) 15 | while (s.length < 8) { 16 | s = '0' + s; 17 | } 18 | return "0x" + s; 19 | } 20 | 21 | function niceSize(n) { 22 | const gigabyte = 1024 * 1024 * 1024; 23 | const megabyte = 1024 * 1024; 24 | const kilobyte = 1024; 25 | if (n >= gigabyte) { 26 | return n / gigabyte + "GiB"; 27 | } else if (n >= megabyte) { 28 | return n / megabyte + "MiB"; 29 | } else if (n >= kilobyte) { 30 | return n / kilobyte + "KiB"; 31 | } else { 32 | return n + "B"; 33 | } 34 | } 35 | 36 | function formatDFUSummary(device) { 37 | const vid = hex4(device.device_.vendorId); 38 | const pid = hex4(device.device_.productId); 39 | const name = device.device_.productName; 40 | 41 | let mode = "Unknown" 42 | if (device.settings.alternate.interfaceProtocol == 0x01) { 43 | mode = "Runtime"; 44 | } else if (device.settings.alternate.interfaceProtocol == 0x02) { 45 | mode = "DFU"; 46 | } 47 | 48 | const cfg = device.settings.configuration.configurationValue; 49 | const intf = device.settings["interface"].interfaceNumber; 50 | const alt = device.settings.alternate.alternateSetting; 51 | const serial = device.device_.serialNumber; 52 | let info = `${mode}: [${vid}:${pid}] cfg=${cfg}, intf=${intf}, alt=${alt}, name="${name}" serial="${serial}"`; 53 | return info; 54 | } 55 | 56 | function formatDFUInterfaceAlternate(settings) { 57 | let mode = "Unknown" 58 | if (settings.alternate.interfaceProtocol == 0x01) { 59 | mode = "Runtime"; 60 | } else if (settings.alternate.interfaceProtocol == 0x02) { 61 | mode = "DFU"; 62 | } 63 | 64 | const cfg = settings.configuration.configurationValue; 65 | const intf = settings["interface"].interfaceNumber; 66 | const alt = settings.alternate.alternateSetting; 67 | const name = (settings.name) ? settings.name : "UNKNOWN"; 68 | 69 | return `${mode}: cfg=${cfg}, intf=${intf}, alt=${alt}, name="${name}"`; 70 | } 71 | 72 | async function fixInterfaceNames(device_, interfaces) { 73 | // Check if any interface names were not read correctly 74 | if (interfaces.some(intf => (intf.name == null))) { 75 | // Manually retrieve the interface name string descriptors 76 | let tempDevice = new dfu.Device(device_, interfaces[0]); 77 | await tempDevice.device_.open(); 78 | await tempDevice.device_.selectConfiguration(1); 79 | let mapping = await tempDevice.readInterfaceNames(); 80 | await tempDevice.close(); 81 | 82 | for (let intf of interfaces) { 83 | if (intf.name === null) { 84 | let configIndex = intf.configuration.configurationValue; 85 | let intfNumber = intf["interface"].interfaceNumber; 86 | let alt = intf.alternate.alternateSetting; 87 | intf.name = mapping[configIndex][intfNumber][alt]; 88 | } 89 | } 90 | } 91 | } 92 | 93 | function populateInterfaceList(form, device_, interfaces) { 94 | let old_choices = Array.from(form.getElementsByTagName("div")); 95 | for (let radio_div of old_choices) { 96 | form.removeChild(radio_div); 97 | } 98 | 99 | let button = form.getElementsByTagName("button")[0]; 100 | 101 | for (let i=0; i < interfaces.length; i++) { 102 | let radio = document.createElement("input"); 103 | radio.type = "radio"; 104 | radio.name = "interfaceIndex"; 105 | radio.value = i; 106 | radio.id = "interface" + i; 107 | radio.required = true; 108 | 109 | let label = document.createElement("label"); 110 | label.textContent = formatDFUInterfaceAlternate(interfaces[i]); 111 | label.className = "radio" 112 | label.setAttribute("for", "interface" + i); 113 | 114 | let div = document.createElement("div"); 115 | div.appendChild(radio); 116 | div.appendChild(label); 117 | form.insertBefore(div, button); 118 | } 119 | } 120 | 121 | function getDFUDescriptorProperties(device) { 122 | // Attempt to read the DFU functional descriptor 123 | // TODO: read the selected configuration's descriptor 124 | return device.readConfigurationDescriptor(0).then( 125 | data => { 126 | let configDesc = dfu.parseConfigurationDescriptor(data); 127 | let funcDesc = null; 128 | let configValue = device.settings.configuration.configurationValue; 129 | if (configDesc.bConfigurationValue == configValue) { 130 | for (let desc of configDesc.descriptors) { 131 | if (desc.bDescriptorType == 0x21 && desc.hasOwnProperty("bcdDFUVersion")) { 132 | funcDesc = desc; 133 | break; 134 | } 135 | } 136 | } 137 | 138 | if (funcDesc) { 139 | return { 140 | WillDetach: ((funcDesc.bmAttributes & 0x08) != 0), 141 | ManifestationTolerant: ((funcDesc.bmAttributes & 0x04) != 0), 142 | CanUpload: ((funcDesc.bmAttributes & 0x02) != 0), 143 | CanDnload: ((funcDesc.bmAttributes & 0x01) != 0), 144 | TransferSize: funcDesc.wTransferSize, 145 | DetachTimeOut: funcDesc.wDetachTimeOut, 146 | DFUVersion: funcDesc.bcdDFUVersion 147 | }; 148 | } else { 149 | return {}; 150 | } 151 | }, 152 | error => {} 153 | ); 154 | } 155 | 156 | // Current log div element to append to 157 | let logContext = null; 158 | 159 | function setLogContext(div) { 160 | logContext = div; 161 | }; 162 | 163 | function clearLog(context) { 164 | if (typeof context === 'undefined') { 165 | context = logContext; 166 | } 167 | if (context) { 168 | context.innerHTML = ""; 169 | } 170 | } 171 | 172 | function logDebug(msg) { 173 | console.log(msg); 174 | } 175 | 176 | function logInfo(msg) { 177 | if (logContext) { 178 | let info = document.createElement("p"); 179 | info.className = "info"; 180 | info.textContent = msg; 181 | logContext.appendChild(info); 182 | } 183 | } 184 | 185 | function logWarning(msg) { 186 | if (logContext) { 187 | let warning = document.createElement("p"); 188 | warning.className = "warning"; 189 | warning.textContent = msg; 190 | logContext.appendChild(warning); 191 | } 192 | } 193 | 194 | function logError(msg) { 195 | if (logContext) { 196 | let error = document.createElement("p"); 197 | error.className = "error"; 198 | error.textContent = msg; 199 | logContext.appendChild(error); 200 | } 201 | } 202 | 203 | function logProgress(done, total) { 204 | if (logContext) { 205 | let progressBar; 206 | if (logContext.lastChild.tagName.toLowerCase() == "progress") { 207 | progressBar = logContext.lastChild; 208 | } 209 | if (!progressBar) { 210 | progressBar = document.createElement("progress"); 211 | logContext.appendChild(progressBar); 212 | } 213 | progressBar.value = done; 214 | if (typeof total !== 'undefined') { 215 | progressBar.max = total; 216 | } 217 | } 218 | } 219 | 220 | document.addEventListener('DOMContentLoaded', event => { 221 | let connectButton = document.querySelector("#connect"); 222 | let detachButton = document.querySelector("#detach"); 223 | let downloadButton = document.querySelector("#download"); 224 | let uploadButton = document.querySelector("#upload"); 225 | let statusDisplay = document.querySelector("#status"); 226 | let infoDisplay = document.querySelector("#usbInfo"); 227 | let dfuDisplay = document.querySelector("#dfuInfo"); 228 | let vidField = document.querySelector("#vid"); 229 | let pidField = document.querySelector("#pid"); 230 | let interfaceDialog = document.querySelector("#interfaceDialog"); 231 | let interfaceForm = document.querySelector("#interfaceForm"); 232 | let interfaceSelectButton = document.querySelector("#selectInterface"); 233 | 234 | let searchParams = new URLSearchParams(window.location.search); 235 | let doAutoConnect = false; 236 | let vid = 0; 237 | let pid = 0; 238 | 239 | // Set the vendor ID from the landing page URL 240 | if (searchParams.has("vid")) { 241 | const vidString = searchParams.get("vid"); 242 | try { 243 | if (vidString.toLowerCase().startsWith("0x")) { 244 | vid = parseInt(vidString, 16); 245 | } else { 246 | vid = parseInt(vidString, 10); 247 | } 248 | vidField.value = "0x" + hex4(vid).toUpperCase(); 249 | doAutoConnect = true; 250 | } catch (error) { 251 | console.log("Bad VID " + vidString + ":" + error); 252 | } 253 | } else { 254 | // NumWorks specialization 255 | vid = 0x0483; // ST 256 | } 257 | 258 | // Set the product ID from the landing page URL 259 | if (searchParams.has("pid")) { 260 | const pidString = searchParams.get("pid"); 261 | try { 262 | if (pidString.toLowerCase().startsWith("0x")) { 263 | pid = parseInt(pidString, 16); 264 | } else { 265 | pid = parseInt(pidString, 10); 266 | } 267 | pidField.value = "0x" + hex4(pid).toUpperCase(); 268 | doAutoConnect = true; 269 | } catch (error) { 270 | console.log("Bad PID " + pidString + ":" + error); 271 | } 272 | } else { 273 | // NumWorks specialization 274 | pid = 0xDF11; // STM Device in DFU Mode 275 | } 276 | 277 | // Grab the serial number from the landing page 278 | let serial = ""; 279 | if (searchParams.has("serial")) { 280 | serial = searchParams.get("serial"); 281 | // Workaround for Chromium issue 339054 282 | if (window.location.search.endsWith("/") && serial.endsWith("/")) { 283 | serial = serial.substring(0, serial.length-1); 284 | } 285 | doAutoConnect = true; 286 | } 287 | 288 | const isNumWorks = (vid === 0x0483 && pid === 0xDF11); 289 | if (isNumWorks) { 290 | doAutoConnect = true; 291 | } 292 | console.log(`isNumWorks = ${isNumWorks} (VID = ${vid}, PID = ${pid})`); 293 | 294 | let configForm = document.querySelector("#configForm"); 295 | 296 | let transferSizeField = document.querySelector("#transferSize"); 297 | let transferSize = parseInt(transferSizeField.value); 298 | 299 | let dfuseStartAddressField = document.querySelector("#dfuseStartAddress"); 300 | let dfuseUploadSizeField = document.querySelector("#dfuseUploadSize"); 301 | 302 | let firmwareFileField = document.querySelector("#firmwareFile"); 303 | let firmwareFile = null; 304 | 305 | let downloadLog = document.querySelector("#downloadLog"); 306 | let uploadLog = document.querySelector("#uploadLog"); 307 | 308 | let manifestationTolerant = true; 309 | 310 | //let device; 311 | 312 | function onDisconnect(reason) { 313 | if (reason) { 314 | statusDisplay.textContent = reason; 315 | } 316 | 317 | connectButton.textContent = "Connect"; 318 | infoDisplay.textContent = ""; 319 | dfuDisplay.textContent = ""; 320 | detachButton.disabled = true; 321 | uploadButton.disabled = true; 322 | downloadButton.disabled = true; 323 | firmwareFileField.disabled = true; 324 | } 325 | 326 | function onUnexpectedDisconnect(event) { 327 | if (device !== null && device.device_ !== null) { 328 | if (device.device_ === event.device) { 329 | device.disconnected = true; 330 | onDisconnect("Device disconnected"); 331 | device = null; 332 | } 333 | } 334 | } 335 | 336 | async function connect(device) { 337 | try { 338 | await device.open(); 339 | } catch (error) { 340 | onDisconnect(error); 341 | throw error; 342 | } 343 | 344 | // Attempt to parse the DFU functional descriptor 345 | let desc = {}; 346 | try { 347 | desc = await getDFUDescriptorProperties(device); 348 | } catch (error) { 349 | onDisconnect(error); 350 | throw error; 351 | } 352 | 353 | let memorySummary = ""; 354 | if (desc && Object.keys(desc).length > 0) { 355 | device.properties = desc; 356 | let info = `WillDetach=${desc.WillDetach}, ManifestationTolerant=${desc.ManifestationTolerant}, CanUpload=${desc.CanUpload}, CanDnload=${desc.CanDnload}, TransferSize=${desc.TransferSize}, DetachTimeOut=${desc.DetachTimeOut}, Version=${hex4(desc.DFUVersion)}`; 357 | dfuDisplay.textContent += "\n" + info; 358 | transferSizeField.value = desc.TransferSize; 359 | transferSize = desc.TransferSize; 360 | if (desc.CanDnload) { 361 | manifestationTolerant = desc.ManifestationTolerant; 362 | } 363 | 364 | if (device.settings.alternate.interfaceProtocol == 0x02) { 365 | if (!desc.CanUpload) { 366 | uploadButton.disabled = true; 367 | dfuseUploadSizeField.disabled = true; 368 | } 369 | if (!desc.CanDnload) { 370 | downloadButton.disabled = true; 371 | } 372 | } 373 | 374 | if (desc.DFUVersion == 0x011a && device.settings.alternate.interfaceProtocol == 0x02) { 375 | device = new dfuse.Device(device.device_, device.settings); 376 | if (device.memoryInfo) { 377 | let totalSize = 0; 378 | for (let segment of device.memoryInfo.segments) { 379 | totalSize += segment.end - segment.start; 380 | } 381 | memorySummary = `Selected memory region: ${device.memoryInfo.name} (${niceSize(totalSize)})`; 382 | for (let segment of device.memoryInfo.segments) { 383 | let properties = []; 384 | if (segment.readable) { 385 | properties.push("readable"); 386 | } 387 | if (segment.erasable) { 388 | properties.push("erasable"); 389 | } 390 | if (segment.writable) { 391 | properties.push("writable"); 392 | } 393 | let propertySummary = properties.join(", "); 394 | if (!propertySummary) { 395 | propertySummary = "inaccessible"; 396 | } 397 | 398 | memorySummary += `\n${hexAddr8(segment.start)}-${hexAddr8(segment.end-1)} (${propertySummary})`; 399 | } 400 | } 401 | } 402 | } 403 | 404 | // Bind logging methods 405 | device.logDebug = logDebug; 406 | device.logInfo = logInfo; 407 | device.logWarning = logWarning; 408 | device.logError = logError; 409 | device.logProgress = logProgress; 410 | 411 | // Clear logs 412 | clearLog(uploadLog); 413 | clearLog(downloadLog); 414 | 415 | // Display basic USB information 416 | statusDisplay.textContent = ''; 417 | connectButton.textContent = 'Disconnect'; 418 | infoDisplay.textContent = ( 419 | "Name: " + device.device_.productName + "\n" + 420 | "MFG: " + device.device_.manufacturerName + "\n" + 421 | "Serial: " + device.device_.serialNumber + "\n" 422 | ); 423 | 424 | // Display basic dfu-util style info 425 | dfuDisplay.textContent = formatDFUSummary(device) + "\n" + memorySummary; 426 | 427 | // Update buttons based on capabilities 428 | if (device.settings.alternate.interfaceProtocol == 0x01) { 429 | // Runtime 430 | detachButton.disabled = false; 431 | uploadButton.disabled = true; 432 | downloadButton.disabled = true; 433 | firmwareFileField.disabled = true; 434 | } else { 435 | // DFU 436 | detachButton.disabled = true; 437 | uploadButton.disabled = false; 438 | downloadButton.disabled = false; 439 | firmwareFileField.disabled = false; 440 | } 441 | 442 | if (device.memoryInfo) { 443 | let dfuseFieldsDiv = document.querySelector("#dfuseFields") 444 | dfuseFieldsDiv.hidden = false; 445 | dfuseStartAddressField.disabled = false; 446 | dfuseUploadSizeField.disabled = false; 447 | let segment = device.getFirstWritableSegment(); 448 | if (segment) { 449 | device.startAddress = segment.start; 450 | dfuseStartAddressField.value = "0x" + segment.start.toString(16); 451 | const maxReadSize = device.getMaxReadSize(segment.start); 452 | dfuseUploadSizeField.value = maxReadSize; 453 | dfuseUploadSizeField.max = maxReadSize; 454 | } 455 | } else { 456 | let dfuseFieldsDiv = document.querySelector("#dfuseFields") 457 | dfuseFieldsDiv.hidden = true; 458 | dfuseStartAddressField.disabled = true; 459 | dfuseUploadSizeField.disabled = true; 460 | } 461 | 462 | return device; 463 | } 464 | 465 | function autoConnect(vid, pid, serial) { 466 | dfu.findAllDfuInterfaces().then( 467 | async dfu_devices => { 468 | let matching_devices = []; 469 | for (let dfu_device of dfu_devices) { 470 | if (serial) { 471 | if (dfu_device.device_.serialNumber == serial) { 472 | matching_devices.push(dfu_device); 473 | } 474 | } else { 475 | if ( 476 | (!pid && vid > 0 && dfu_device.device_.vendorId == vid) || 477 | (!vid && pid > 0 && dfu_device.device_.productId == pid) || 478 | (vid > 0 && pid > 0 && dfu_device.device_.vendorId == vid && dfu_device.device_.productId == pid) 479 | ) 480 | { 481 | matching_devices.push(dfu_device); 482 | } 483 | } 484 | } 485 | 486 | if (matching_devices.length == 0) { 487 | statusDisplay.textContent = 'No device found.'; 488 | } else { 489 | if (matching_devices.length == 1 || isNumWorks) { // For NumWorks, we want interface 0 ("Internal Flash") 490 | statusDisplay.textContent = 'Connecting...'; 491 | device = matching_devices[0]; 492 | console.log("Autoconnecting to device:", device); 493 | device = await connect(device); 494 | } else { 495 | statusDisplay.textContent = "Multiple DFU interfaces found."; 496 | } 497 | vidField.value = "0x" + hex4(matching_devices[0].device_.vendorId).toUpperCase(); 498 | vid = matching_devices[0].device_.vendorId; 499 | } 500 | } 501 | ); 502 | } 503 | 504 | vidField.addEventListener("change", function() { 505 | vid = parseInt(vidField.value, 16); 506 | }); 507 | 508 | transferSizeField.addEventListener("change", function() { 509 | transferSize = parseInt(transferSizeField.value); 510 | }); 511 | 512 | dfuseStartAddressField.addEventListener("change", function(event) { 513 | const field = event.target; 514 | let address = parseInt(field.value, 16); 515 | if (isNaN(address)) { 516 | field.setCustomValidity("Invalid hexadecimal start address"); 517 | } else if (device && device.memoryInfo) { 518 | if (device.getSegment(address) !== null) { 519 | device.startAddress = address; 520 | field.setCustomValidity(""); 521 | dfuseUploadSizeField.max = device.getMaxReadSize(address); 522 | } else { 523 | field.setCustomValidity("Address outside of memory map"); 524 | } 525 | } else { 526 | field.setCustomValidity(""); 527 | } 528 | }); 529 | 530 | connectButton.addEventListener('click', function() { 531 | if (device) { 532 | device.close().then(onDisconnect); 533 | device = null; 534 | } else { 535 | let filters = []; 536 | if (serial) { 537 | filters.push({ 'serialNumber': serial }); 538 | } else { 539 | if (vid) { 540 | filters.push({'vendorId': vid}); 541 | } 542 | if (vid && pid) { 543 | filters.push({'productId': pid, 'vendorId': vid}); 544 | } 545 | } 546 | navigator.usb.requestDevice({ 'filters': filters }).then( 547 | async selectedDevice => { 548 | let interfaces = dfu.findDeviceDfuInterfaces(selectedDevice); 549 | if (interfaces.length == 0) { 550 | console.log(selectedDevice); 551 | statusDisplay.textContent = "The selected device does not have any USB DFU interfaces."; 552 | } else if (interfaces.length == 1 || isNumWorks) { // For NumWorks, we want interface 0 ("Internal Flash") 553 | await fixInterfaceNames(selectedDevice, interfaces); 554 | device = await connect(new dfu.Device(selectedDevice, interfaces[0])); 555 | } else { 556 | await fixInterfaceNames(selectedDevice, interfaces); 557 | populateInterfaceList(interfaceForm, selectedDevice, interfaces); 558 | async function connectToSelectedInterface() { 559 | interfaceForm.removeEventListener('submit', this); 560 | const index = interfaceForm.elements["interfaceIndex"].value; 561 | device = await connect(new dfu.Device(selectedDevice, interfaces[index])); 562 | } 563 | 564 | interfaceForm.addEventListener('submit', connectToSelectedInterface); 565 | 566 | interfaceDialog.addEventListener('cancel', function () { 567 | interfaceDialog.removeEventListener('cancel', this); 568 | interfaceForm.removeEventListener('submit', connectToSelectedInterface); 569 | }); 570 | 571 | interfaceDialog.showModal(); 572 | } 573 | } 574 | ).catch(error => { 575 | statusDisplay.textContent = error; 576 | }); 577 | } 578 | }); 579 | 580 | detachButton.addEventListener('click', function() { 581 | if (device) { 582 | device.detach().then( 583 | async len => { 584 | let detached = false; 585 | try { 586 | await device.close(); 587 | await device.waitDisconnected(5000); 588 | detached = true; 589 | } catch (err) { 590 | console.log("Detach failed: " + err); 591 | } 592 | 593 | onDisconnect(); 594 | device = null; 595 | if (detached) { 596 | // Wait a few seconds and try reconnecting 597 | setTimeout(autoConnect, 5000); 598 | } 599 | }, 600 | async error => { 601 | await device.close(); 602 | onDisconnect(error); 603 | device = null; 604 | } 605 | ); 606 | } 607 | }); 608 | 609 | uploadButton.addEventListener('click', async function(event) { 610 | event.preventDefault(); 611 | event.stopPropagation(); 612 | if (!configForm.checkValidity()) { 613 | configForm.reportValidity(); 614 | return false; 615 | } 616 | 617 | if (!device || !device.device_.opened) { 618 | onDisconnect(); 619 | device = null; 620 | } else { 621 | setLogContext(uploadLog); 622 | clearLog(uploadLog); 623 | try { 624 | let status = await device.getStatus(); 625 | if (status.state == dfu.dfuERROR) { 626 | await device.clearStatus(); 627 | } 628 | } catch (error) { 629 | device.logWarning("Failed to clear status"); 630 | } 631 | 632 | let maxSize = Infinity; 633 | if (!dfuseUploadSizeField.disabled) { 634 | maxSize = parseInt(dfuseUploadSizeField.value); 635 | } 636 | 637 | try { 638 | const blob = await device.do_upload(transferSize, maxSize); 639 | saveAs(blob, "firmware.bin"); 640 | } catch (error) { 641 | logError(error); 642 | } 643 | 644 | setLogContext(null); 645 | } 646 | 647 | return false; 648 | }); 649 | 650 | firmwareFileField.addEventListener("change", function() { 651 | firmwareFile = null; 652 | if (firmwareFileField.files.length > 0) { 653 | let file = firmwareFileField.files[0]; 654 | let reader = new FileReader(); 655 | reader.onload = function() { 656 | firmwareFile = reader.result; 657 | }; 658 | reader.readAsArrayBuffer(file); 659 | } 660 | }); 661 | 662 | downloadButton.addEventListener('click', async function(event) { 663 | event.preventDefault(); 664 | event.stopPropagation(); 665 | if (!configForm.checkValidity()) { 666 | configForm.reportValidity(); 667 | return false; 668 | } 669 | 670 | if (device && firmwareFile != null) { 671 | setLogContext(downloadLog); 672 | clearLog(downloadLog); 673 | try { 674 | let status = await device.getStatus(); 675 | if (status.state == dfu.dfuERROR) { 676 | await device.clearStatus(); 677 | } 678 | } catch (error) { 679 | device.logWarning("Failed to clear status"); 680 | } 681 | await device.do_download(transferSize, firmwareFile, manifestationTolerant).then( 682 | () => { 683 | logInfo("Done!"); 684 | setLogContext(null); 685 | if (!manifestationTolerant) { 686 | device.waitDisconnected(5000).then( 687 | dev => { 688 | onDisconnect(); 689 | device = null; 690 | }, 691 | error => { 692 | // It didn't reset and disconnect for some reason... 693 | console.log("Device unexpectedly tolerated manifestation."); 694 | } 695 | ); 696 | } 697 | }, 698 | error => { 699 | logError(error); 700 | setLogContext(null); 701 | } 702 | ) 703 | } 704 | 705 | //return false; 706 | }); 707 | 708 | // Check if WebUSB is available 709 | if (typeof navigator.usb !== 'undefined') { 710 | navigator.usb.addEventListener("disconnect", onUnexpectedDisconnect); 711 | // Try connecting automatically 712 | if (doAutoConnect) { 713 | autoConnect(vid, pid, serial); 714 | } 715 | } else { 716 | statusDisplay.textContent = 'WebUSB not available.' 717 | connectButton.disabled = true; 718 | } 719 | }); 720 | })(); 721 | -------------------------------------------------------------------------------- /n0100/dfu.js: -------------------------------------------------------------------------------- 1 | var dfu = {}; 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | dfu.DETACH = 0x00; 7 | dfu.DNLOAD = 0x01; 8 | dfu.UPLOAD = 0x02; 9 | dfu.GETSTATUS = 0x03; 10 | dfu.CLRSTATUS = 0x04; 11 | dfu.GETSTATE = 0x05; 12 | dfu.ABORT = 6; 13 | 14 | dfu.appIDLE = 0; 15 | dfu.appDETACH = 1; 16 | dfu.dfuIDLE = 2; 17 | dfu.dfuDNLOAD_SYNC = 3; 18 | dfu.dfuDNBUSY = 4; 19 | dfu.dfuDNLOAD_IDLE = 5; 20 | dfu.dfuMANIFEST_SYNC = 6; 21 | dfu.dfuMANIFEST = 7; 22 | dfu.dfuMANIFEST_WAIT_RESET = 8; 23 | dfu.dfuUPLOAD_IDLE = 9; 24 | dfu.dfuERROR = 10; 25 | 26 | dfu.STATUS_OK = 0x0; 27 | 28 | dfu.Device = function(device, settings) { 29 | this.device_ = device; 30 | this.settings = settings; 31 | this.intfNumber = settings["interface"].interfaceNumber; 32 | }; 33 | 34 | dfu.findDeviceDfuInterfaces = function(device) { 35 | let interfaces = []; 36 | for (let conf of device.configurations) { 37 | for (let intf of conf.interfaces) { 38 | for (let alt of intf.alternates) { 39 | if (alt.interfaceClass == 0xFE && 40 | alt.interfaceSubclass == 0x01 && 41 | (alt.interfaceProtocol == 0x01 || alt.interfaceProtocol == 0x02)) { 42 | let settings = { 43 | "configuration": conf, 44 | "interface": intf, 45 | "alternate": alt, 46 | "name": alt.interfaceName 47 | }; 48 | interfaces.push(settings); 49 | } 50 | } 51 | } 52 | } 53 | 54 | return interfaces; 55 | } 56 | 57 | dfu.findAllDfuInterfaces = function() { 58 | return navigator.usb.getDevices().then( 59 | devices => { 60 | let matches = []; 61 | for (let device of devices) { 62 | let interfaces = dfu.findDeviceDfuInterfaces(device); 63 | for (let interface_ of interfaces) { 64 | matches.push(new dfu.Device(device, interface_)) 65 | } 66 | } 67 | return matches; 68 | } 69 | ) 70 | }; 71 | 72 | dfu.Device.prototype.logDebug = function(msg) { 73 | 74 | }; 75 | 76 | dfu.Device.prototype.logInfo = function(msg) { 77 | console.log(msg); 78 | }; 79 | 80 | dfu.Device.prototype.logWarning = function(msg) { 81 | console.log(msg); 82 | }; 83 | 84 | dfu.Device.prototype.logError = function(msg) { 85 | console.log(msg); 86 | }; 87 | 88 | dfu.Device.prototype.logProgress = function(done, total) { 89 | if (typeof total === 'undefined') { 90 | console.log(done) 91 | } else { 92 | console.log(done + '/' + total); 93 | } 94 | }; 95 | 96 | dfu.Device.prototype.open = async function() { 97 | await this.device_.open(); 98 | const confValue = this.settings.configuration.configurationValue; 99 | if (this.device_.configuration === null || 100 | this.device_.configuration.configurationValue != confValue) { 101 | await this.device_.selectConfiguration(confValue); 102 | } 103 | 104 | const intfNumber = this.settings["interface"].interfaceNumber; 105 | if (!this.device_.configuration.interfaces[intfNumber].claimed) { 106 | await this.device_.claimInterface(intfNumber); 107 | } 108 | 109 | const altSetting = this.settings.alternate.alternateSetting; 110 | let intf = this.device_.configuration.interfaces[intfNumber]; 111 | if (intf.alternate === null || 112 | intf.alternate.alternateSetting != altSetting) { 113 | await this.device_.selectAlternateInterface(intfNumber, altSetting); 114 | } 115 | } 116 | 117 | dfu.Device.prototype.close = async function() { 118 | try { 119 | await this.device_.close(); 120 | } catch (error) { 121 | console.log(error); 122 | } 123 | }; 124 | 125 | dfu.Device.prototype.readDeviceDescriptor = function() { 126 | const GET_DESCRIPTOR = 0x06; 127 | const DT_DEVICE = 0x01; 128 | const wValue = (DT_DEVICE << 8); 129 | 130 | return this.device_.controlTransferIn({ 131 | "requestType": "standard", 132 | "recipient": "device", 133 | "request": GET_DESCRIPTOR, 134 | "value": wValue, 135 | "index": 0 136 | }, 18).then( 137 | result => { 138 | if (result.status == "ok") { 139 | return Promise.resolve(result.data); 140 | } else { 141 | return Promise.reject(result.status); 142 | } 143 | } 144 | ); 145 | }; 146 | 147 | dfu.Device.prototype.readStringDescriptor = async function(index, langID) { 148 | if (typeof langID === 'undefined') { 149 | langID = 0; 150 | } 151 | 152 | const GET_DESCRIPTOR = 0x06; 153 | const DT_STRING = 0x03; 154 | const wValue = (DT_STRING << 8) | index; 155 | 156 | const request_setup = { 157 | "requestType": "standard", 158 | "recipient": "device", 159 | "request": GET_DESCRIPTOR, 160 | "value": wValue, 161 | "index": langID 162 | } 163 | 164 | // Read enough for bLength 165 | var result = await this.device_.controlTransferIn(request_setup, 1); 166 | 167 | if (result.status == "ok") { 168 | // Retrieve the full descriptor 169 | const bLength = result.data.getUint8(0); 170 | result = await this.device_.controlTransferIn(request_setup, bLength); 171 | if (result.status == "ok") { 172 | const len = (bLength-2) / 2; 173 | let u16_words = []; 174 | for (let i=0; i < len; i++) { 175 | u16_words.push(result.data.getUint16(2+i*2, true)); 176 | } 177 | if (langID == 0) { 178 | // Return the langID array 179 | return u16_words; 180 | } else { 181 | // Decode from UCS-2 into a string 182 | return String.fromCharCode.apply(String, u16_words); 183 | } 184 | } 185 | } 186 | 187 | throw `Failed to read string descriptor ${index}: ${result.status}`; 188 | }; 189 | 190 | dfu.Device.prototype.readInterfaceNames = async function() { 191 | const DT_INTERFACE = 4; 192 | 193 | let configs = {}; 194 | let allStringIndices = new Set(); 195 | for (let configIndex=0; configIndex < this.device_.configurations.length; configIndex++) { 196 | const rawConfig = await this.readConfigurationDescriptor(configIndex); 197 | let configDesc = dfu.parseConfigurationDescriptor(rawConfig); 198 | let configValue = configDesc.bConfigurationValue; 199 | configs[configValue] = {}; 200 | 201 | // Retrieve string indices for interface names 202 | for (let desc of configDesc.descriptors) { 203 | if (desc.bDescriptorType == DT_INTERFACE) { 204 | if (!(desc.bInterfaceNumber in configs[configValue])) { 205 | configs[configValue][desc.bInterfaceNumber] = {}; 206 | } 207 | configs[configValue][desc.bInterfaceNumber][desc.bAlternateSetting] = desc.iInterface; 208 | if (desc.iInterface > 0) { 209 | allStringIndices.add(desc.iInterface); 210 | } 211 | } 212 | } 213 | } 214 | 215 | let strings = {}; 216 | // Retrieve interface name strings 217 | for (let index of allStringIndices) { 218 | try { 219 | strings[index] = await this.readStringDescriptor(index, 0x0409); 220 | } catch (error) { 221 | console.log(error); 222 | strings[index] = null; 223 | } 224 | } 225 | 226 | for (let configValue in configs) { 227 | for (let intfNumber in configs[configValue]) { 228 | for (let alt in configs[configValue][intfNumber]) { 229 | const iIndex = configs[configValue][intfNumber][alt]; 230 | configs[configValue][intfNumber][alt] = strings[iIndex]; 231 | } 232 | } 233 | } 234 | 235 | return configs; 236 | }; 237 | 238 | dfu.parseDeviceDescriptor = function(data) { 239 | return { 240 | bLength: data.getUint8(0), 241 | bDescriptorType: data.getUint8(1), 242 | bcdUSB: data.getUint16(2, true), 243 | bDeviceClass: data.getUint8(4), 244 | bDeviceSubClass: data.getUint8(5), 245 | bDeviceProtocol: data.getUint8(6), 246 | bMaxPacketSize: data.getUint8(7), 247 | idVendor: data.getUint16(8, true), 248 | idProduct: data.getUint16(10, true), 249 | bcdDevice: data.getUint16(12, true), 250 | iManufacturer: data.getUint8(14), 251 | iProduct: data.getUint8(15), 252 | iSerialNumber: data.getUint8(16), 253 | bNumConfigurations: data.getUint8(17), 254 | }; 255 | }; 256 | 257 | dfu.parseConfigurationDescriptor = function(data) { 258 | let descriptorData = new DataView(data.buffer.slice(9)); 259 | let descriptors = dfu.parseSubDescriptors(descriptorData); 260 | return { 261 | bLength: data.getUint8(0), 262 | bDescriptorType: data.getUint8(1), 263 | wTotalLength: data.getUint16(2, true), 264 | bNumInterfaces: data.getUint8(4), 265 | bConfigurationValue:data.getUint8(5), 266 | iConfiguration: data.getUint8(6), 267 | bmAttributes: data.getUint8(7), 268 | bMaxPower: data.getUint8(8), 269 | descriptors: descriptors 270 | }; 271 | }; 272 | 273 | dfu.parseInterfaceDescriptor = function(data) { 274 | return { 275 | bLength: data.getUint8(0), 276 | bDescriptorType: data.getUint8(1), 277 | bInterfaceNumber: data.getUint8(2), 278 | bAlternateSetting: data.getUint8(3), 279 | bNumEndpoints: data.getUint8(4), 280 | bInterfaceClass: data.getUint8(5), 281 | bInterfaceSubClass: data.getUint8(6), 282 | bInterfaceProtocol: data.getUint8(7), 283 | iInterface: data.getUint8(8), 284 | descriptors: [] 285 | }; 286 | }; 287 | 288 | dfu.parseFunctionalDescriptor = function(data) { 289 | return { 290 | bLength: data.getUint8(0), 291 | bDescriptorType: data.getUint8(1), 292 | bmAttributes: data.getUint8(2), 293 | wDetachTimeOut: data.getUint16(3, true), 294 | wTransferSize: data.getUint16(5, true), 295 | bcdDFUVersion: data.getUint16(7, true) 296 | }; 297 | }; 298 | 299 | dfu.parseSubDescriptors = function(descriptorData) { 300 | const DT_INTERFACE = 4; 301 | const DT_ENDPOINT = 5; 302 | const DT_DFU_FUNCTIONAL = 0x21; 303 | const USB_CLASS_APP_SPECIFIC = 0xFE; 304 | const USB_SUBCLASS_DFU = 0x01; 305 | let remainingData = descriptorData; 306 | let descriptors = []; 307 | let currIntf; 308 | let inDfuIntf = false; 309 | while (remainingData.byteLength > 2) { 310 | let bLength = remainingData.getUint8(0); 311 | let bDescriptorType = remainingData.getUint8(1); 312 | let descData = new DataView(remainingData.buffer.slice(0, bLength)); 313 | if (bDescriptorType == DT_INTERFACE) { 314 | currIntf = dfu.parseInterfaceDescriptor(descData); 315 | if (currIntf.bInterfaceClass == USB_CLASS_APP_SPECIFIC && 316 | currIntf.bInterfaceSubClass == USB_SUBCLASS_DFU) { 317 | inDfuIntf = true; 318 | } else { 319 | inDfuIntf = false; 320 | } 321 | descriptors.push(currIntf); 322 | } else if (inDfuIntf && bDescriptorType == DT_DFU_FUNCTIONAL) { 323 | let funcDesc = dfu.parseFunctionalDescriptor(descData) 324 | descriptors.push(funcDesc); 325 | currIntf.descriptors.push(funcDesc); 326 | } else { 327 | let desc = { 328 | bLength: bLength, 329 | bDescriptorType: bDescriptorType, 330 | data: descData 331 | }; 332 | descriptors.push(desc); 333 | if (currIntf) { 334 | currIntf.descriptors.push(desc); 335 | } 336 | } 337 | remainingData = new DataView(remainingData.buffer.slice(bLength)); 338 | } 339 | 340 | return descriptors; 341 | }; 342 | 343 | dfu.Device.prototype.readConfigurationDescriptor = function(index) { 344 | const GET_DESCRIPTOR = 0x06; 345 | const DT_CONFIGURATION = 0x02; 346 | const wValue = ((DT_CONFIGURATION << 8) | index); 347 | 348 | return this.device_.controlTransferIn({ 349 | "requestType": "standard", 350 | "recipient": "device", 351 | "request": GET_DESCRIPTOR, 352 | "value": wValue, 353 | "index": 0 354 | }, 4).then( 355 | result => { 356 | if (result.status == "ok") { 357 | // Read out length of the configuration descriptor 358 | let wLength = result.data.getUint16(2, true); 359 | return this.device_.controlTransferIn({ 360 | "requestType": "standard", 361 | "recipient": "device", 362 | "request": GET_DESCRIPTOR, 363 | "value": wValue, 364 | "index": 0 365 | }, wLength); 366 | } else { 367 | return Promise.reject(result.status); 368 | } 369 | } 370 | ).then( 371 | result => { 372 | if (result.status == "ok") { 373 | return Promise.resolve(result.data); 374 | } else { 375 | return Promise.reject(result.status); 376 | } 377 | } 378 | ); 379 | }; 380 | 381 | dfu.Device.prototype.requestOut = function(bRequest, data, wValue=0) { 382 | return this.device_.controlTransferOut({ 383 | "requestType": "class", 384 | "recipient": "interface", 385 | "request": bRequest, 386 | "value": wValue, 387 | "index": this.intfNumber 388 | }, data).then( 389 | result => { 390 | if (result.status == "ok") { 391 | return Promise.resolve(result.bytesWritten); 392 | } else { 393 | return Promise.reject(result.status); 394 | } 395 | }, 396 | error => { 397 | return Promise.reject("ControlTransferOut failed: " + error); 398 | } 399 | ); 400 | }; 401 | 402 | dfu.Device.prototype.requestIn = function(bRequest, wLength, wValue=0) { 403 | return this.device_.controlTransferIn({ 404 | "requestType": "class", 405 | "recipient": "interface", 406 | "request": bRequest, 407 | "value": wValue, 408 | "index": this.intfNumber 409 | }, wLength).then( 410 | result => { 411 | if (result.status == "ok") { 412 | return Promise.resolve(result.data); 413 | } else { 414 | return Promise.reject(result.status); 415 | } 416 | }, 417 | error => { 418 | return Promise.reject("ControlTransferIn failed: " + error); 419 | } 420 | ); 421 | }; 422 | 423 | dfu.Device.prototype.detach = function() { 424 | return this.requestOut(dfu.DETACH, undefined, 1000); 425 | } 426 | 427 | dfu.Device.prototype.waitDisconnected = async function(timeout) { 428 | let device = this; 429 | let usbDevice = this.device_; 430 | return new Promise(function(resolve, reject) { 431 | let timeoutID; 432 | if (timeout > 0) { 433 | function onTimeout() { 434 | navigator.usb.removeEventListener("disconnect", onDisconnect); 435 | if (device.disconnected !== true) { 436 | reject("Disconnect timeout expired"); 437 | } 438 | } 439 | timeoutID = setTimeout(reject, timeout); 440 | } 441 | 442 | function onDisconnect(event) { 443 | if (event.device === usbDevice) { 444 | if (timeout > 0) { 445 | clearTimeout(timeoutID); 446 | } 447 | device.disconnected = true; 448 | navigator.usb.removeEventListener("disconnect", onDisconnect); 449 | event.stopPropagation(); 450 | resolve(device); 451 | } 452 | } 453 | 454 | navigator.usb.addEventListener("disconnect", onDisconnect); 455 | }); 456 | }; 457 | 458 | dfu.Device.prototype.download = function(data, blockNum) { 459 | return this.requestOut(dfu.DNLOAD, data, blockNum); 460 | }; 461 | 462 | dfu.Device.prototype.dnload = dfu.Device.prototype.download; 463 | 464 | dfu.Device.prototype.upload = function(length, blockNum) { 465 | return this.requestIn(dfu.UPLOAD, length, blockNum) 466 | }; 467 | 468 | dfu.Device.prototype.clearStatus = function() { 469 | return this.requestOut(dfu.CLRSTATUS); 470 | }; 471 | 472 | dfu.Device.prototype.clrStatus = dfu.Device.prototype.clearStatus; 473 | 474 | dfu.Device.prototype.getStatus = function() { 475 | return this.requestIn(dfu.GETSTATUS, 6).then( 476 | data => 477 | Promise.resolve({ 478 | "status": data.getUint8(0), 479 | "pollTimeout": data.getUint32(1, true) & 0xFFFFFF, 480 | "state": data.getUint8(4) 481 | }), 482 | error => 483 | Promise.reject("DFU GETSTATUS failed: " + error) 484 | ); 485 | }; 486 | 487 | dfu.Device.prototype.getState = function() { 488 | return this.requestIn(dfu.GETSTATE, 1).then( 489 | data => Promise.resolve(data.getUint8(0)), 490 | error => Promise.reject("DFU GETSTATE failed: " + error) 491 | ); 492 | }; 493 | 494 | dfu.Device.prototype.abort = function() { 495 | return this.requestOut(dfu.ABORT); 496 | }; 497 | 498 | dfu.Device.prototype.abortToIdle = async function() { 499 | await this.abort(); 500 | let state = await this.getState(); 501 | if (state == dfu.dfuERROR) { 502 | await this.clearStatus(); 503 | state = await this.getState(); 504 | } 505 | if (state != dfu.dfuIDLE) { 506 | throw "Failed to return to idle state after abort: state " + state.state; 507 | } 508 | }; 509 | 510 | dfu.Device.prototype.do_upload = async function(xfer_size, max_size=Infinity, first_block=0) { 511 | let transaction = first_block; 512 | let blocks = []; 513 | let bytes_read = 0; 514 | 515 | this.logInfo("Copying data from DFU device to browser"); 516 | // Initialize progress to 0 517 | this.logProgress(0); 518 | 519 | let result; 520 | let bytes_to_read; 521 | do { 522 | bytes_to_read = Math.min(xfer_size, max_size - bytes_read); 523 | result = await this.upload(bytes_to_read, transaction++); 524 | this.logDebug("Read " + result.byteLength + " bytes"); 525 | if (result.byteLength > 0) { 526 | blocks.push(result); 527 | bytes_read += result.byteLength; 528 | } 529 | if (Number.isFinite(max_size)) { 530 | this.logProgress(bytes_read, max_size); 531 | } else { 532 | this.logProgress(bytes_read); 533 | } 534 | } while ((bytes_read < max_size) && (result.byteLength == bytes_to_read)); 535 | 536 | if (bytes_read == max_size) { 537 | await this.abortToIdle(); 538 | } 539 | 540 | this.logInfo(`Read ${bytes_read} bytes`); 541 | 542 | return new Blob(blocks, { type: "application/octet-stream" }); 543 | }; 544 | 545 | dfu.Device.prototype.poll_until = async function(state_predicate) { 546 | let dfu_status = await this.getStatus(); 547 | 548 | let device = this; 549 | function async_sleep(duration_ms) { 550 | return new Promise(function(resolve, reject) { 551 | device.logDebug("Sleeping for " + duration_ms + "ms"); 552 | setTimeout(resolve, duration_ms); 553 | }); 554 | } 555 | 556 | while (!state_predicate(dfu_status.state) && dfu_status.state != dfu.dfuERROR) { 557 | await async_sleep(dfu_status.pollTimeout); 558 | dfu_status = await this.getStatus(); 559 | } 560 | 561 | return dfu_status; 562 | }; 563 | 564 | dfu.Device.prototype.poll_until_idle = function(idle_state) { 565 | return this.poll_until(state => (state == idle_state)); 566 | }; 567 | 568 | dfu.Device.prototype.do_download = async function(xfer_size, data, manifestationTolerant) { 569 | let bytes_sent = 0; 570 | let expected_size = data.byteLength; 571 | let transaction = 0; 572 | 573 | this.logInfo("Copying data from browser to DFU device"); 574 | 575 | // Initialize progress to 0 576 | this.logProgress(bytes_sent, expected_size); 577 | 578 | while (bytes_sent < expected_size) { 579 | const bytes_left = expected_size - bytes_sent; 580 | const chunk_size = Math.min(bytes_left, xfer_size); 581 | 582 | let bytes_written = 0; 583 | let dfu_status; 584 | try { 585 | bytes_written = await this.download(data.slice(bytes_sent, bytes_sent+chunk_size), transaction++); 586 | this.logDebug("Sent " + bytes_written + " bytes"); 587 | dfu_status = await this.poll_until_idle(dfu.dfuDNLOAD_IDLE); 588 | } catch (error) { 589 | throw "Error during DFU download: " + error; 590 | } 591 | 592 | if (dfu_status.status != dfu.STATUS_OK) { 593 | throw `DFU DOWNLOAD failed state=${dfu_status.state}, status=${dfu_status.status}`; 594 | } 595 | 596 | this.logDebug("Wrote " + bytes_written + " bytes"); 597 | bytes_sent += bytes_written; 598 | 599 | this.logProgress(bytes_sent, expected_size); 600 | } 601 | 602 | this.logDebug("Sending empty block"); 603 | try { 604 | await this.download(new ArrayBuffer([]), transaction++); 605 | } catch (error) { 606 | throw "Error during final DFU download: " + error; 607 | } 608 | 609 | this.logInfo("Wrote " + bytes_sent + " bytes"); 610 | this.logInfo("Manifesting new firmware"); 611 | 612 | if (manifestationTolerant) { 613 | // Transition to MANIFEST_SYNC state 614 | let dfu_status; 615 | try { 616 | // Wait until it returns to idle. 617 | // If it's not really manifestation tolerant, it might transition to MANIFEST_WAIT_RESET 618 | dfu_status = await this.poll_until(state => (state == dfu.dfuIDLE || state == dfu.dfuMANIFEST_WAIT_RESET)); 619 | if (dfu_status.state == dfu.dfuMANIFEST_WAIT_RESET) { 620 | this.logDebug("Device transitioned to MANIFEST_WAIT_RESET even though it is manifestation tolerant"); 621 | } 622 | if (dfu_status.status != dfu.STATUS_OK) { 623 | throw `DFU MANIFEST failed state=${dfu_status.state}, status=${dfu_status.status}`; 624 | } 625 | } catch (error) { 626 | if (error.endsWith("ControlTransferIn failed: NotFoundError: Device unavailable.") || 627 | error.endsWith("ControlTransferIn failed: NotFoundError: The device was disconnected.")) { 628 | this.logWarning("Unable to poll final manifestation status"); 629 | } else { 630 | throw "Error during DFU manifest: " + error; 631 | } 632 | } 633 | } else { 634 | // Try polling once to initiate manifestation 635 | try { 636 | let final_status = await this.getStatus(); 637 | this.logDebug(`Final DFU status: state=${final_status.state}, status=${final_status.status}`); 638 | } catch (error) { 639 | this.logDebug("Manifest GET_STATUS poll error: " + error); 640 | } 641 | } 642 | 643 | // Reset to exit MANIFEST_WAIT_RESET 644 | try { 645 | await this.device_.reset(); 646 | } catch (error) { 647 | if (error == "NetworkError: Unable to reset the device." || 648 | error == "NotFoundError: Device unavailable." || 649 | error == "NotFoundError: The device was disconnected.") { 650 | this.logDebug("Ignored reset error"); 651 | } else { 652 | throw "Error during reset for manifestation: " + error; 653 | } 654 | } 655 | 656 | return; 657 | }; 658 | 659 | })(); 660 | -------------------------------------------------------------------------------- /n0100/dfuse.js: -------------------------------------------------------------------------------- 1 | /* dfu.js must be included before dfuse.js */ 2 | 3 | var dfuse = {}; 4 | 5 | (function() { 6 | 'use strict'; 7 | 8 | dfuse.GET_COMMANDS = 0x00; 9 | dfuse.SET_ADDRESS = 0x21; 10 | dfuse.ERASE_SECTOR = 0x41; 11 | 12 | dfuse.Device = function(device, settings) { 13 | dfu.Device.call(this, device, settings); 14 | this.memoryInfo = null; 15 | this.startAddress = NaN; 16 | if (settings.name) { 17 | this.memoryInfo = dfuse.parseMemoryDescriptor(settings.name); 18 | } 19 | } 20 | 21 | dfuse.Device.prototype = Object.create(dfu.Device.prototype); 22 | dfuse.Device.prototype.constructor = dfuse.Device; 23 | 24 | dfuse.parseMemoryDescriptor = function(desc) { 25 | const nameEndIndex = desc.indexOf("/"); 26 | if (!desc.startsWith("@") || nameEndIndex == -1) { 27 | throw `Not a DfuSe memory descriptor: "${desc}"`; 28 | } 29 | 30 | const name = desc.substring(1, nameEndIndex).trim(); 31 | const segmentString = desc.substring(nameEndIndex); 32 | 33 | let segments = []; 34 | 35 | const sectorMultipliers = { 36 | ' ': 1, 37 | 'B': 1, 38 | 'K': 1024, 39 | 'M': 1048576 40 | }; 41 | 42 | let contiguousSegmentRegex = /\/\s*(0x[0-9a-fA-F]{1,8})\s*\/(\s*[0-9]+\s*\*\s*[0-9]+\s?[ BKM]\s*[abcdefg]\s*,?\s*)+/g; 43 | let contiguousSegmentMatch; 44 | while (contiguousSegmentMatch = contiguousSegmentRegex.exec(segmentString)) { 45 | let segmentRegex = /([0-9]+)\s*\*\s*([0-9]+)\s?([ BKM])\s*([abcdefg])\s*,?\s*/g; 46 | let startAddress = parseInt(contiguousSegmentMatch[1], 16); 47 | let segmentMatch; 48 | while (segmentMatch = segmentRegex.exec(contiguousSegmentMatch[0])) { 49 | let segment = {} 50 | let sectorCount = parseInt(segmentMatch[1], 10); 51 | let sectorSize = parseInt(segmentMatch[2]) * sectorMultipliers[segmentMatch[3]]; 52 | let properties = segmentMatch[4].charCodeAt(0) - 'a'.charCodeAt(0) + 1; 53 | segment.start = startAddress; 54 | segment.sectorSize = sectorSize; 55 | segment.end = startAddress + sectorSize * sectorCount; 56 | segment.readable = (properties & 0x1) != 0; 57 | segment.erasable = (properties & 0x2) != 0; 58 | segment.writable = (properties & 0x4) != 0; 59 | segments.push(segment); 60 | 61 | startAddress += sectorSize * sectorCount; 62 | } 63 | } 64 | 65 | return {"name": name, "segments": segments}; 66 | }; 67 | 68 | dfuse.Device.prototype.dfuseCommand = async function(command, param, len) { 69 | if (typeof param === 'undefined' && typeof len === 'undefined') { 70 | param = 0x00; 71 | len = 1; 72 | } 73 | 74 | const commandNames = { 75 | 0x00: "GET_COMMANDS", 76 | 0x21: "SET_ADDRESS", 77 | 0x41: "ERASE_SECTOR" 78 | }; 79 | 80 | let payload = new ArrayBuffer(len + 1); 81 | let view = new DataView(payload); 82 | view.setUint8(0, command); 83 | if (len == 1) { 84 | view.setUint8(1, param); 85 | } else if (len == 4) { 86 | view.setUint32(1, param, true); 87 | } else { 88 | throw "Don't know how to handle data of len " + len; 89 | } 90 | 91 | try { 92 | await this.download(payload, 0); 93 | } catch (error) { 94 | throw "Error during special DfuSe command " + commandNames[command] + ":" + error; 95 | } 96 | 97 | let status = await this.poll_until(state => (state != dfu.dfuDNBUSY)); 98 | if (status.status != dfu.STATUS_OK) { 99 | throw "Special DfuSe command " + commandName + " failed"; 100 | } 101 | }; 102 | 103 | dfuse.Device.prototype.getSegment = function(addr) { 104 | if (!this.memoryInfo || ! this.memoryInfo.segments) { 105 | throw "No memory map information available"; 106 | } 107 | 108 | for (let segment of this.memoryInfo.segments) { 109 | if (segment.start <= addr && addr < segment.end) { 110 | return segment; 111 | } 112 | } 113 | 114 | return null; 115 | }; 116 | 117 | dfuse.Device.prototype.getSectorStart = function(addr, segment) { 118 | if (typeof segment === 'undefined') { 119 | segment = this.getSegment(addr); 120 | } 121 | 122 | if (!segment) { 123 | throw `Address ${addr.toString(16)} outside of memory map`; 124 | } 125 | 126 | const sectorIndex = Math.floor((addr - segment.start)/segment.sectorSize); 127 | return segment.start + sectorIndex * segment.sectorSize; 128 | }; 129 | 130 | dfuse.Device.prototype.getSectorEnd = function(addr, segment) { 131 | if (typeof segment === 'undefined') { 132 | segment = this.getSegment(addr); 133 | } 134 | 135 | if (!segment) { 136 | throw `Address ${addr.toString(16)} outside of memory map`; 137 | } 138 | 139 | const sectorIndex = Math.floor((addr - segment.start)/segment.sectorSize); 140 | return segment.start + (sectorIndex + 1) * segment.sectorSize; 141 | }; 142 | 143 | dfuse.Device.prototype.getFirstWritableSegment = function() { 144 | if (!this.memoryInfo || ! this.memoryInfo.segments) { 145 | throw "No memory map information available"; 146 | } 147 | 148 | for (let segment of this.memoryInfo.segments) { 149 | if (segment.writable) { 150 | return segment; 151 | } 152 | } 153 | 154 | return null; 155 | }; 156 | 157 | dfuse.Device.prototype.getMaxReadSize = function(startAddr) { 158 | if (!this.memoryInfo || ! this.memoryInfo.segments) { 159 | throw "No memory map information available"; 160 | } 161 | 162 | let numBytes = 0; 163 | for (let segment of this.memoryInfo.segments) { 164 | if (segment.start <= startAddr && startAddr < segment.end) { 165 | // Found the first segment the read starts in 166 | if (segment.readable) { 167 | numBytes += segment.end - startAddr; 168 | } else { 169 | return 0; 170 | } 171 | } else if (segment.start == startAddr + numBytes) { 172 | // Include a contiguous segment 173 | if (segment.readable) { 174 | numBytes += (segment.end - segment.start); 175 | } else { 176 | break; 177 | } 178 | } 179 | } 180 | 181 | return numBytes; 182 | }; 183 | 184 | dfuse.Device.prototype.erase = async function(startAddr, length) { 185 | let segment = this.getSegment(startAddr); 186 | let addr = this.getSectorStart(startAddr, segment); 187 | const endAddr = this.getSectorEnd(startAddr + length - 1); 188 | 189 | let bytesErased = 0; 190 | const bytesToErase = endAddr - addr; 191 | if (bytesToErase > 0) { 192 | this.logProgress(bytesErased, bytesToErase); 193 | } 194 | 195 | while (addr < endAddr) { 196 | if (segment.end <= addr) { 197 | segment = this.getSegment(addr); 198 | } 199 | if (!segment.erasable) { 200 | // Skip over the non-erasable section 201 | bytesErased = Math.min(bytesErased + segment.end - addr, bytesToErase); 202 | addr = segment.end; 203 | this.logProgress(bytesErased, bytesToErase); 204 | continue; 205 | } 206 | const sectorIndex = Math.floor((addr - segment.start)/segment.sectorSize); 207 | const sectorAddr = segment.start + sectorIndex * segment.sectorSize; 208 | this.logDebug(`Erasing ${segment.sectorSize}B at 0x${sectorAddr.toString(16)}`); 209 | await this.dfuseCommand(dfuse.ERASE_SECTOR, sectorAddr, 4); 210 | addr = sectorAddr + segment.sectorSize; 211 | bytesErased += segment.sectorSize; 212 | this.logProgress(bytesErased, bytesToErase); 213 | } 214 | }; 215 | 216 | dfuse.Device.prototype.do_download = async function(xfer_size, data, manifestationTolerant) { 217 | if (!this.memoryInfo || ! this.memoryInfo.segments) { 218 | throw "No memory map available"; 219 | } 220 | 221 | this.logInfo("Erasing DFU device memory"); 222 | 223 | let bytes_sent = 0; 224 | let expected_size = data.byteLength; 225 | 226 | let startAddress = this.startAddress; 227 | if (isNaN(startAddress)) { 228 | startAddress = this.memoryInfo.segments[0].start; 229 | this.logWarning("Using inferred start address 0x" + startAddress.toString(16)); 230 | } else if (this.getSegment(startAddress) === null) { 231 | this.logError(`Start address 0x${startAddress.toString(16)} outside of memory map bounds`); 232 | } 233 | await this.erase(startAddress, expected_size); 234 | 235 | this.logInfo("Copying data from browser to DFU device"); 236 | 237 | let address = startAddress; 238 | while (bytes_sent < expected_size) { 239 | const bytes_left = expected_size - bytes_sent; 240 | const chunk_size = Math.min(bytes_left, xfer_size); 241 | 242 | let bytes_written = 0; 243 | let dfu_status; 244 | try { 245 | await this.dfuseCommand(dfuse.SET_ADDRESS, address, 4); 246 | this.logDebug(`Set address to 0x${address.toString(16)}`); 247 | bytes_written = await this.download(data.slice(bytes_sent, bytes_sent+chunk_size), 2); 248 | this.logDebug("Sent " + bytes_written + " bytes"); 249 | dfu_status = await this.poll_until_idle(dfu.dfuDNLOAD_IDLE); 250 | address += chunk_size; 251 | } catch (error) { 252 | throw "Error during DfuSe download: " + error; 253 | } 254 | 255 | if (dfu_status.status != dfu.STATUS_OK) { 256 | throw `DFU DOWNLOAD failed state=${dfu_status.state}, status=${dfu_status.status}`; 257 | } 258 | 259 | this.logDebug("Wrote " + bytes_written + " bytes"); 260 | bytes_sent += bytes_written; 261 | 262 | this.logProgress(bytes_sent, expected_size); 263 | } 264 | this.logInfo(`Wrote ${bytes_sent} bytes`); 265 | 266 | this.logInfo("Manifesting new firmware"); 267 | try { 268 | await this.dfuseCommand(dfuse.SET_ADDRESS, startAddress, 4); 269 | await this.download(new ArrayBuffer(), 0); 270 | } catch (error) { 271 | throw "Error during DfuSe manifestation: " + error; 272 | } 273 | 274 | try { 275 | await this.poll_until(state => (state == dfu.dfuMANIFEST)); 276 | } catch (error) { 277 | this.logError(error); 278 | } 279 | } 280 | 281 | dfuse.Device.prototype.do_upload = async function(xfer_size, max_size) { 282 | let startAddress = this.startAddress; 283 | if (isNaN(startAddress)) { 284 | startAddress = this.memoryInfo.segments[0].start; 285 | this.logWarning("Using inferred start address 0x" + startAddress.toString(16)); 286 | } else if (this.getSegment(startAddress) === null) { 287 | this.logWarning(`Start address 0x${startAddress.toString(16)} outside of memory map bounds`); 288 | } 289 | 290 | this.logInfo(`Reading up to 0x${max_size.toString(16)} bytes starting at 0x${startAddress.toString(16)}`); 291 | let state = await this.getState(); 292 | if (state != dfu.dfuIDLE) { 293 | await this.abortToIdle(); 294 | } 295 | await this.dfuseCommand(dfuse.SET_ADDRESS, startAddress, 4); 296 | await this.abortToIdle(); 297 | 298 | // DfuSe encodes the read address based on the transfer size, 299 | // the block number - 2, and the SET_ADDRESS pointer. 300 | return await dfu.Device.prototype.do_upload.call(this, xfer_size, max_size, 2); 301 | } 302 | })(); 303 | -------------------------------------------------------------------------------- /n0100/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebUSB DFU for NumWorks 6 | 7 | 8 | 9 | 10 | 25 | 26 | 27 |

28 | 29 |

30 |

31 | 32 | 33 | 34 | 35 |

36 |

37 | Plug in your NumWorks calculator and press its reset button, then click here:
38 | 39 |

40 | 41 | Your device has multiple DFU interfaces. Select one from the list below: 42 |
43 | 44 |
45 |
46 |

47 |

48 |
49 |

50 |
51 | Runtime mode 52 | 53 |
54 |
55 |
56 | 57 | 58 | 64 | 65 | DFU mode 66 |
67 | Flash firmware onto device 68 |

69 | 70 |

71 |

72 | 73 |

74 |
75 |
76 |
77 | Dump flash from device 78 |

79 | 80 |

81 |
82 |
83 |
84 |
85 |
86 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /n0100/sakura.css: -------------------------------------------------------------------------------- 1 | /* Sakura.css v1.0.0 2 | * ================ 3 | * Minimal css theme. 4 | * Project: https://github.com/oxalorg/sakura 5 | */ 6 | /* Body */ 7 | html { 8 | font-size: 62.5%; 9 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 10 | } 11 | 12 | body { 13 | font-size: 1.8rem; 14 | line-height: 1.618; 15 | max-width: 38em; 16 | margin: auto; 17 | color: #4a4a4a; 18 | background-color: #f9f9f9; 19 | padding: 13px; 20 | } 21 | 22 | @media (max-width: 684px) { 23 | body { 24 | font-size: 1.53rem; } } 25 | @media (max-width: 382px) { 26 | body { 27 | font-size: 1.35rem; } } 28 | h1, h2, h3, h4, h5, h6 { 29 | line-height: 1.1; 30 | font-family: Verdana, Geneva, sans-serif; 31 | font-weight: 700; 32 | overflow-wrap: break-word; 33 | word-wrap: break-word; 34 | -ms-word-break: break-all; 35 | word-break: break-word; 36 | -ms-hyphens: auto; 37 | -moz-hyphens: auto; 38 | -webkit-hyphens: auto; 39 | hyphens: auto; } 40 | 41 | h1 { 42 | font-size: 2.35em; } 43 | 44 | h2 { 45 | font-size: 2em; } 46 | 47 | h3 { 48 | font-size: 1.75em; } 49 | 50 | h4 { 51 | font-size: 1.5em; } 52 | 53 | h5 { 54 | font-size: 1.25em; } 55 | 56 | h6 { 57 | font-size: 1em; } 58 | 59 | small, sub, sup { 60 | font-size: 75%; } 61 | 62 | hr { 63 | border-color: #2c8898; } 64 | 65 | a { 66 | text-decoration: none; 67 | color: #2c8898; } 68 | a:hover { 69 | color: #982c61; 70 | border-bottom: 2px solid #4a4a4a; } 71 | 72 | ul { 73 | padding-left: 1.4em; } 74 | 75 | li { 76 | margin-bottom: 0.4em; } 77 | 78 | blockquote { 79 | font-style: italic; 80 | margin-left: 1.5em; 81 | padding-left: 1em; 82 | border-left: 3px solid #2c8898; } 83 | 84 | img { 85 | max-width: 100%; } 86 | 87 | /* Pre and Code */ 88 | pre { 89 | background-color: #f1f1f1; 90 | display: block; 91 | padding: 1em; 92 | overflow-x: auto; } 93 | 94 | code { 95 | font-size: 0.9em; 96 | padding: 0 0.5em; 97 | background-color: #f1f1f1; 98 | white-space: pre-wrap; } 99 | 100 | pre > code { 101 | padding: 0; 102 | background-color: transparent; 103 | white-space: pre; } 104 | 105 | /* Tables */ 106 | table { 107 | text-align: justify; 108 | width: 100%; 109 | border-collapse: collapse; } 110 | 111 | td, th { 112 | padding: 0.5em; 113 | border-bottom: 1px solid #f1f1f1; } 114 | 115 | /* Buttons, forms and input */ 116 | input, textarea { 117 | border: 1px solid #4a4a4a; } 118 | input:focus, textarea:focus { 119 | border: 1px solid #2c8898; } 120 | 121 | textarea { 122 | width: 100%; } 123 | 124 | 125 | .button, button, input[type="submit"], input[type="reset"], input[type="button"] { 126 | margin: 5px; 127 | padding: 10px 20px; 128 | background-color: #faa039; 129 | color: white; 130 | border: none; 131 | font-size: 14px; 132 | font-weight: bold; 133 | border-radius: 7px; 134 | cursor: pointer; 135 | transition: background-color 0.3s ease; 136 | } 137 | .button[disabled], button[disabled], input[type="submit"][disabled], input[type="reset"][disabled], input[type="button"][disabled] { 138 | cursor: default; 139 | opacity: .5; } 140 | .button:focus, .button:hover, button:focus, button:hover, input[type="submit"]:focus, input[type="submit"]:hover, input[type="reset"]:focus, input[type="reset"]:hover, input[type="button"]:focus, input[type="button"]:hover { 141 | background-color: #982c61; 142 | border-color: #982c61; 143 | color: #f9f9f9; 144 | outline: 0; } 145 | 146 | textarea, select, input[type] { 147 | color: #4a4a4a; 148 | padding: 6px 10px; 149 | /* The 6px vertically centers text on FF, ignored by Webkit */ 150 | margin-bottom: 10px; 151 | background-color: #f1f1f1; 152 | border: 1px solid #f1f1f1; 153 | border-radius: 4px; 154 | box-shadow: none; 155 | box-sizing: border-box; } 156 | textarea:focus, select:focus, input[type]:focus { 157 | border: 1px solid #2c8898; 158 | outline: 0; } 159 | 160 | label, legend, fieldset { 161 | display: block; 162 | margin-bottom: .5rem; 163 | font-weight: 600; 164 | } 165 | 166 | fieldset { 167 | border: 1px solid #ccc; 168 | margin: 10px 0; 169 | padding: 15px; 170 | border-radius: 5px; 171 | } 172 | 173 | legend { 174 | font-weight: bold; 175 | color: #333; 176 | } 177 | 178 | /* Style for buttons inside fieldset */ 179 | 180 | fieldset button:disabled { 181 | background-color: #ccc; 182 | cursor: not-allowed; 183 | } 184 | 185 | fieldset button:hover { 186 | background-color: #f08307; 187 | } 188 | 189 | -------------------------------------------------------------------------------- /n0110/FileSaver.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | if (typeof define === "function" && define.amd) { 3 | define([], factory); 4 | } else if (typeof exports !== "undefined") { 5 | factory(); 6 | } else { 7 | var mod = { 8 | exports: {} 9 | }; 10 | factory(); 11 | global.FileSaver = mod.exports; 12 | } 13 | })(this, function () { 14 | "use strict"; 15 | 16 | /* 17 | * FileSaver.js 18 | * A saveAs() FileSaver implementation. 19 | * 20 | * By Eli Grey, http://eligrey.com 21 | * 22 | * License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT) 23 | * source : http://purl.eligrey.com/github/FileSaver.js 24 | */ 25 | // The one and only way of getting global scope in all environments 26 | // https://stackoverflow.com/q/3277182/1008999 27 | var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : void 0; 28 | 29 | function bom(blob, opts) { 30 | if (typeof opts === 'undefined') opts = { 31 | autoBom: false 32 | };else if (typeof opts !== 'object') { 33 | console.warn('Depricated: Expected third argument to be a object'); 34 | opts = { 35 | autoBom: !opts 36 | }; 37 | } // prepend BOM for UTF-8 XML and text/* types (including HTML) 38 | // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF 39 | 40 | if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { 41 | return new Blob([String.fromCharCode(0xFEFF), blob], { 42 | type: blob.type 43 | }); 44 | } 45 | 46 | return blob; 47 | } 48 | 49 | function download(url, name, opts) { 50 | var xhr = new XMLHttpRequest(); 51 | xhr.open('GET', url); 52 | xhr.responseType = 'blob'; 53 | 54 | xhr.onload = function () { 55 | saveAs(xhr.response, name, opts); 56 | }; 57 | 58 | xhr.onerror = function () { 59 | console.error('could not download file'); 60 | }; 61 | 62 | xhr.send(); 63 | } 64 | 65 | function corsEnabled(url) { 66 | var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker 67 | 68 | xhr.open('HEAD', url, false); 69 | xhr.send(); 70 | return xhr.status >= 200 && xhr.status <= 299; 71 | } // `a.click()` doesn't work for all browsers (#465) 72 | 73 | 74 | function click(node) { 75 | try { 76 | node.dispatchEvent(new MouseEvent('click')); 77 | } catch (e) { 78 | var evt = document.createEvent('MouseEvents'); 79 | evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); 80 | node.dispatchEvent(evt); 81 | } 82 | } 83 | 84 | var saveAs = _global.saveAs || // probably in some web worker 85 | typeof window !== 'object' || window !== _global ? function saveAs() {} 86 | /* noop */ 87 | // Use download attribute first if possible (#193 Lumia mobile) 88 | : 'download' in HTMLAnchorElement.prototype ? function saveAs(blob, name, opts) { 89 | var URL = _global.URL || _global.webkitURL; 90 | var a = document.createElement('a'); 91 | name = name || blob.name || 'download'; 92 | a.download = name; 93 | a.rel = 'noopener'; // tabnabbing 94 | // TODO: detect chrome extensions & packaged apps 95 | // a.target = '_blank' 96 | 97 | if (typeof blob === 'string') { 98 | // Support regular links 99 | a.href = blob; 100 | 101 | if (a.origin !== location.origin) { 102 | corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank'); 103 | } else { 104 | click(a); 105 | } 106 | } else { 107 | // Support blobs 108 | a.href = URL.createObjectURL(blob); 109 | setTimeout(function () { 110 | URL.revokeObjectURL(a.href); 111 | }, 4E4); // 40s 112 | 113 | setTimeout(function () { 114 | click(a); 115 | }, 0); 116 | } 117 | } // Use msSaveOrOpenBlob as a second approach 118 | : 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) { 119 | name = name || blob.name || 'download'; 120 | 121 | if (typeof blob === 'string') { 122 | if (corsEnabled(blob)) { 123 | download(blob, name, opts); 124 | } else { 125 | var a = document.createElement('a'); 126 | a.href = blob; 127 | a.target = '_blank'; 128 | setTimeout(function () { 129 | click(a); 130 | }); 131 | } 132 | } else { 133 | navigator.msSaveOrOpenBlob(bom(blob, opts), name); 134 | } 135 | } // Fallback to using FileReader and a popup 136 | : function saveAs(blob, name, opts, popup) { 137 | // Open a popup immediately do go around popup blocker 138 | // Mostly only avalible on user interaction and the fileReader is async so... 139 | popup = popup || open('', '_blank'); 140 | 141 | if (popup) { 142 | popup.document.title = popup.document.body.innerText = 'downloading...'; 143 | } 144 | 145 | if (typeof blob === 'string') return download(blob, name, opts); 146 | var force = blob.type === 'application/octet-stream'; 147 | 148 | var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari; 149 | 150 | var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent); 151 | 152 | if ((isChromeIOS || force && isSafari) && typeof FileReader === 'object') { 153 | // Safari doesn't allow downloading of blob urls 154 | var reader = new FileReader(); 155 | 156 | reader.onloadend = function () { 157 | var url = reader.result; 158 | url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;'); 159 | if (popup) popup.location.href = url;else location = url; 160 | popup = null; // reverse-tabnabbing #460 161 | }; 162 | 163 | reader.readAsDataURL(blob); 164 | } else { 165 | var URL = _global.URL || _global.webkitURL; 166 | var url = URL.createObjectURL(blob); 167 | if (popup) popup.location = url;else location.href = url; 168 | popup = null; // reverse-tabnabbing #460 169 | 170 | setTimeout(function () { 171 | URL.revokeObjectURL(url); 172 | }, 4E4); // 40s 173 | } 174 | }; 175 | _global.saveAs = saveAs.saveAs = saveAs; 176 | 177 | if (typeof module !== 'undefined') { 178 | module.exports = saveAs; 179 | } 180 | }); -------------------------------------------------------------------------------- /n0110/dfu-util.js: -------------------------------------------------------------------------------- 1 | var device = null; 2 | (function() { 3 | 'use strict'; 4 | 5 | function hex4(n) { 6 | let s = n.toString(16) 7 | while (s.length < 4) { 8 | s = '0' + s; 9 | } 10 | return s; 11 | } 12 | 13 | function hexAddr8(n) { 14 | let s = n.toString(16) 15 | while (s.length < 8) { 16 | s = '0' + s; 17 | } 18 | return "0x" + s; 19 | } 20 | 21 | function niceSize(n) { 22 | const gigabyte = 1024 * 1024 * 1024; 23 | const megabyte = 1024 * 1024; 24 | const kilobyte = 1024; 25 | if (n >= gigabyte) { 26 | return n / gigabyte + "GiB"; 27 | } else if (n >= megabyte) { 28 | return n / megabyte + "MiB"; 29 | } else if (n >= kilobyte) { 30 | return n / kilobyte + "KiB"; 31 | } else { 32 | return n + "B"; 33 | } 34 | } 35 | 36 | function formatDFUSummary(device) { 37 | const vid = hex4(device.device_.vendorId); 38 | const pid = hex4(device.device_.productId); 39 | const name = device.device_.productName; 40 | 41 | let mode = "Unknown" 42 | if (device.settings.alternate.interfaceProtocol == 0x01) { 43 | mode = "Runtime"; 44 | } else if (device.settings.alternate.interfaceProtocol == 0x02) { 45 | mode = "DFU"; 46 | } 47 | 48 | const cfg = device.settings.configuration.configurationValue; 49 | const intf = device.settings["interface"].interfaceNumber; 50 | const alt = device.settings.alternate.alternateSetting; 51 | const serial = device.device_.serialNumber; 52 | let info = `${mode}: [${vid}:${pid}] cfg=${cfg}, intf=${intf}, alt=${alt}, name="${name}" serial="${serial}"`; 53 | return info; 54 | } 55 | 56 | function formatDFUInterfaceAlternate(settings) { 57 | let mode = "Unknown" 58 | if (settings.alternate.interfaceProtocol == 0x01) { 59 | mode = "Runtime"; 60 | } else if (settings.alternate.interfaceProtocol == 0x02) { 61 | mode = "DFU"; 62 | } 63 | 64 | const cfg = settings.configuration.configurationValue; 65 | const intf = settings["interface"].interfaceNumber; 66 | const alt = settings.alternate.alternateSetting; 67 | const name = (settings.name) ? settings.name : "UNKNOWN"; 68 | 69 | return `${mode}: cfg=${cfg}, intf=${intf}, alt=${alt}, name="${name}"`; 70 | } 71 | 72 | async function fixInterfaceNames(device_, interfaces) { 73 | // Check if any interface names were not read correctly 74 | if (interfaces.some(intf => (intf.name == null))) { 75 | // Manually retrieve the interface name string descriptors 76 | let tempDevice = new dfu.Device(device_, interfaces[0]); 77 | await tempDevice.device_.open(); 78 | let mapping = await tempDevice.readInterfaceNames(); 79 | await tempDevice.close(); 80 | 81 | for (let intf of interfaces) { 82 | if (intf.name === null) { 83 | let configIndex = intf.configuration.configurationValue; 84 | let intfNumber = intf["interface"].interfaceNumber; 85 | let alt = intf.alternate.alternateSetting; 86 | intf.name = mapping[configIndex][intfNumber][alt]; 87 | } 88 | } 89 | } 90 | } 91 | 92 | function populateInterfaceList(form, device_, interfaces) { 93 | let old_choices = Array.from(form.getElementsByTagName("div")); 94 | for (let radio_div of old_choices) { 95 | form.removeChild(radio_div); 96 | } 97 | 98 | let button = form.getElementsByTagName("button")[0]; 99 | 100 | for (let i=0; i < interfaces.length; i++) { 101 | let radio = document.createElement("input"); 102 | radio.type = "radio"; 103 | radio.name = "interfaceIndex"; 104 | radio.value = i; 105 | radio.id = "interface" + i; 106 | radio.required = true; 107 | 108 | let label = document.createElement("label"); 109 | label.textContent = formatDFUInterfaceAlternate(interfaces[i]); 110 | label.className = "radio" 111 | label.setAttribute("for", "interface" + i); 112 | 113 | let div = document.createElement("div"); 114 | div.appendChild(radio); 115 | div.appendChild(label); 116 | form.insertBefore(div, button); 117 | } 118 | } 119 | 120 | function getDFUDescriptorProperties(device) { 121 | // Attempt to read the DFU functional descriptor 122 | // TODO: read the selected configuration's descriptor 123 | return device.readConfigurationDescriptor(0).then( 124 | data => { 125 | let configDesc = dfu.parseConfigurationDescriptor(data); 126 | let funcDesc = null; 127 | let configValue = device.settings.configuration.configurationValue; 128 | if (configDesc.bConfigurationValue == configValue) { 129 | for (let desc of configDesc.descriptors) { 130 | if (desc.bDescriptorType == 0x21 && desc.hasOwnProperty("bcdDFUVersion")) { 131 | funcDesc = desc; 132 | break; 133 | } 134 | } 135 | } 136 | 137 | if (funcDesc) { 138 | return { 139 | WillDetach: ((funcDesc.bmAttributes & 0x08) != 0), 140 | ManifestationTolerant: ((funcDesc.bmAttributes & 0x04) != 0), 141 | CanUpload: ((funcDesc.bmAttributes & 0x02) != 0), 142 | CanDnload: ((funcDesc.bmAttributes & 0x01) != 0), 143 | TransferSize: funcDesc.wTransferSize, 144 | DetachTimeOut: funcDesc.wDetachTimeOut, 145 | DFUVersion: funcDesc.bcdDFUVersion 146 | }; 147 | } else { 148 | return {}; 149 | } 150 | }, 151 | error => {} 152 | ); 153 | } 154 | 155 | // Current log div element to append to 156 | let logContext = null; 157 | 158 | function setLogContext(div) { 159 | logContext = div; 160 | }; 161 | 162 | function clearLog(context) { 163 | if (typeof context === 'undefined') { 164 | context = logContext; 165 | } 166 | if (context) { 167 | context.innerHTML = ""; 168 | } 169 | } 170 | 171 | function logDebug(msg) { 172 | console.log(msg); 173 | } 174 | 175 | function logInfo(msg) { 176 | if (logContext) { 177 | let info = document.createElement("p"); 178 | info.className = "info"; 179 | info.textContent = msg; 180 | logContext.appendChild(info); 181 | } 182 | } 183 | 184 | function logWarning(msg) { 185 | if (logContext) { 186 | let warning = document.createElement("p"); 187 | warning.className = "warning"; 188 | warning.textContent = msg; 189 | logContext.appendChild(warning); 190 | } 191 | } 192 | 193 | function logError(msg) { 194 | if (logContext) { 195 | let error = document.createElement("p"); 196 | error.className = "error"; 197 | error.textContent = msg; 198 | logContext.appendChild(error); 199 | } 200 | } 201 | 202 | function logProgress(done, total) { 203 | if (logContext) { 204 | let progressBar; 205 | if (logContext.lastChild.tagName.toLowerCase() == "progress") { 206 | progressBar = logContext.lastChild; 207 | } 208 | if (!progressBar) { 209 | progressBar = document.createElement("progress"); 210 | logContext.appendChild(progressBar); 211 | } 212 | progressBar.value = done; 213 | if (typeof total !== 'undefined') { 214 | progressBar.max = total; 215 | } 216 | } 217 | } 218 | 219 | document.addEventListener('DOMContentLoaded', event => { 220 | let connectButton = document.querySelector("#connect"); 221 | let detachButton = document.querySelector("#detach"); 222 | let downloadFlashAddressInput = document.querySelector("#download_address"); 223 | let downloadInternalButton = document.querySelector("#download_internal"); 224 | let downloadExternalButton = document.querySelector("#download_external"); 225 | let downloadSlotAButton = document.querySelector("#download_slota"); 226 | let downloadSlotBButton = document.querySelector("#download_slotb"); 227 | let downloadSlotAUserlandButton = document.querySelector("#download_slota_user"); 228 | let downloadSlotBUserlandButton = document.querySelector("#download_slotb_user"); 229 | let bootSlotAUserlandButton = document.querySelector("#boot_slota_user"); 230 | let bootSlotBUserlandButton = document.querySelector("#boot_slotb_user"); 231 | let uploadInternalButton = document.querySelector("#upload_internal"); 232 | let uploadExternalButton = document.querySelector("#upload_external"); 233 | let uploadSlotAButton = document.querySelector("#upload_slota"); 234 | let uploadSlotBButton = document.querySelector("#upload_slotb"); 235 | let statusDisplay = document.querySelector("#status"); 236 | let infoDisplay = document.querySelector("#usbInfo"); 237 | let dfuDisplay = document.querySelector("#dfuInfo"); 238 | let vidField = document.querySelector("#vid"); 239 | let pidField = document.querySelector("#pid"); 240 | let interfaceDialog = document.querySelector("#interfaceDialog"); 241 | let interfaceForm = document.querySelector("#interfaceForm"); 242 | let interfaceSelectButton = document.querySelector("#selectInterface"); 243 | 244 | let searchParams = new URLSearchParams(window.location.search); 245 | let doAutoConnect = false; 246 | let vid = 0; 247 | let pid = 0; 248 | 249 | // Set the vendor ID from the landing page URL 250 | if (searchParams.has("vid")) { 251 | const vidString = searchParams.get("vid"); 252 | try { 253 | if (vidString.toLowerCase().startsWith("0x")) { 254 | vid = parseInt(vidString, 16); 255 | } else { 256 | vid = parseInt(vidString, 10); 257 | } 258 | vidField.value = "0x" + hex4(vid).toUpperCase(); 259 | doAutoConnect = true; 260 | } catch (error) { 261 | console.log("Bad VID " + vidString + ":" + error); 262 | } 263 | } else { 264 | // NumWorks specialization 265 | vid = 0x0483; // ST 266 | } 267 | 268 | // Set the product ID from the landing page URL 269 | if (searchParams.has("pid")) { 270 | const pidString = searchParams.get("pid"); 271 | try { 272 | if (pidString.toLowerCase().startsWith("0x")) { 273 | pid = parseInt(pidString, 16); 274 | } else { 275 | pid = parseInt(pidString, 10); 276 | } 277 | pidField.value = "0x" + hex4(pid).toUpperCase(); 278 | doAutoConnect = true; 279 | } catch (error) { 280 | console.log("Bad PID " + pidString + ":" + error); 281 | } 282 | } else { 283 | // NumWorks specialization 284 | pid = 0xA291; 285 | } 286 | 287 | // Grab the serial number from the landing page 288 | let serial = ""; 289 | if (searchParams.has("serial")) { 290 | serial = searchParams.get("serial"); 291 | // Workaround for Chromium issue 339054 292 | if (window.location.search.endsWith("/") && serial.endsWith("/")) { 293 | serial = serial.substring(0, serial.length-1); 294 | } 295 | doAutoConnect = true; 296 | } 297 | 298 | const isNumWorks = (vid === 0x0483 && pid === 0xA291); 299 | if (isNumWorks) { 300 | //disable AutoConnect until a proper fix is found 301 | //doAutoConnect = true; 302 | } 303 | console.log(`isNumWorks = ${isNumWorks} (VID = ${vid}, PID = ${pid})`); 304 | 305 | let configForm = document.querySelector("#configForm"); 306 | 307 | let transferSizeField = document.querySelector("#transferSize"); 308 | let transferSize = parseInt(transferSizeField.value); 309 | 310 | let dfuseStartAddressField = document.querySelector("#dfuseStartAddress"); 311 | let dfuseUploadSizeField = document.querySelector("#dfuseUploadSize"); 312 | 313 | let firmwareFileField = document.querySelector("#firmwareFile"); 314 | let firmwareFile = null; 315 | 316 | let downloadLog = document.querySelector("#downloadLog"); 317 | let uploadLog = document.querySelector("#uploadLog"); 318 | 319 | let manifestationTolerant = true; 320 | 321 | //let device; 322 | 323 | function onDisconnect(reason) { 324 | if (reason) { 325 | statusDisplay.textContent = reason; 326 | } 327 | 328 | connectButton.textContent = "Connect"; 329 | infoDisplay.textContent = ""; 330 | dfuDisplay.textContent = ""; 331 | detachButton.disabled = true; 332 | downloadFlashAddressInput.disabled = true; 333 | uploadInternalButton.disabled = true; 334 | uploadExternalButton.disabled = true; 335 | uploadSlotAButton.disabled = true; 336 | uploadSlotBButton.disabled = true; 337 | downloadInternalButton.disabled = true; 338 | downloadExternalButton.disabled = true; 339 | downloadSlotAButton.disabled = true; 340 | downloadSlotBButton.disabled = true; 341 | downloadSlotAUserlandButton.disabled = true; 342 | downloadSlotBUserlandButton.disabled = true; 343 | bootSlotAUserlandButton.disabled = true; 344 | bootSlotBUserlandButton.disabled = true; 345 | firmwareFileField.disabled = true; 346 | } 347 | 348 | function onUnexpectedDisconnect(event) { 349 | if (device !== null && device.device_ !== null) { 350 | if (device.device_ === event.device) { 351 | device.disconnected = true; 352 | onDisconnect("Device disconnected"); 353 | device = null; 354 | } 355 | } 356 | } 357 | 358 | async function connect(device) { 359 | try { 360 | await device.open(); 361 | } catch (error) { 362 | onDisconnect(error); 363 | throw error; 364 | } 365 | 366 | // Attempt to parse the DFU functional descriptor 367 | let desc = {}; 368 | try { 369 | desc = await getDFUDescriptorProperties(device); 370 | } catch (error) { 371 | onDisconnect(error); 372 | throw error; 373 | } 374 | 375 | let memorySummary = ""; 376 | if (desc && Object.keys(desc).length > 0) { 377 | device.properties = desc; 378 | let info = `WillDetach=${desc.WillDetach}, ManifestationTolerant=${desc.ManifestationTolerant}, CanUpload=${desc.CanUpload}, CanDnload=${desc.CanDnload}, TransferSize=${desc.TransferSize}, DetachTimeOut=${desc.DetachTimeOut}, Version=${hex4(desc.DFUVersion)}`; 379 | dfuDisplay.textContent += "\n" + info; 380 | transferSizeField.value = desc.TransferSize; 381 | transferSize = desc.TransferSize; 382 | if (desc.CanDnload) { 383 | manifestationTolerant = desc.ManifestationTolerant; 384 | } 385 | 386 | if (device.settings.alternate.interfaceProtocol == 0x02) { 387 | if (!desc.CanUpload) { 388 | uploadInternalButton.disabled = true; 389 | uploadExternalButton.disabled = true; 390 | uploadSlotAButton.disabled = true; 391 | uploadSlotBButton.disabled = true; 392 | dfuseUploadSizeField.disabled = true; 393 | } 394 | if (!desc.CanDnload) { 395 | downloadInternalButton.disabled = true; 396 | downloadExternalButton.disabled = true; 397 | downloadFlashAddressInput.disabled = true; 398 | downloadSlotAButton.disabled = true; 399 | downloadSlotBButton.disabled = true; 400 | downloadSlotAUserlandButton.disabled = true; 401 | downloadSlotBUserlandButton.disabled = true; 402 | bootSlotAUserlandButton.disabled = true; 403 | bootSlotBUserlandButton.disabled = true; 404 | } 405 | } 406 | 407 | if ((desc.DFUVersion == 0x100 || desc.DFUVersion == 0x011a) && device.settings.alternate.interfaceProtocol == 0x02) { 408 | device = new dfuse.Device(device.device_, device.settings); 409 | if (device.memoryInfo) { 410 | let totalSize = 0; 411 | for (let segment of device.memoryInfo.segments) { 412 | totalSize += segment.end - segment.start; 413 | } 414 | memorySummary = `Selected memory region: ${device.memoryInfo.name} (${niceSize(totalSize)})`; 415 | for (let segment of device.memoryInfo.segments) { 416 | let properties = []; 417 | if (segment.readable) { 418 | properties.push("readable"); 419 | } 420 | if (segment.erasable) { 421 | properties.push("erasable"); 422 | } 423 | if (segment.writable) { 424 | properties.push("writable"); 425 | } 426 | let propertySummary = properties.join(", "); 427 | if (!propertySummary) { 428 | propertySummary = "inaccessible"; 429 | } 430 | 431 | memorySummary += `\n${hexAddr8(segment.start)}-${hexAddr8(segment.end-1)} (${propertySummary})`; 432 | } 433 | } 434 | } 435 | } 436 | 437 | // Bind logging methods 438 | device.logDebug = logDebug; 439 | device.logInfo = logInfo; 440 | device.logWarning = logWarning; 441 | device.logError = logError; 442 | device.logProgress = logProgress; 443 | 444 | // Clear logs 445 | clearLog(uploadLog); 446 | clearLog(downloadLog); 447 | 448 | // Display basic USB information 449 | statusDisplay.textContent = ''; 450 | connectButton.textContent = 'Disconnect'; 451 | infoDisplay.textContent = ( 452 | "Name: " + device.device_.productName + "\n" + 453 | "MFG: " + device.device_.manufacturerName + "\n" + 454 | "Serial: " + device.device_.serialNumber + "\n" 455 | ); 456 | 457 | // Display basic dfu-util style info 458 | dfuDisplay.textContent = formatDFUSummary(device) + "\n" + memorySummary; 459 | 460 | // Update buttons based on capabilities 461 | if (device.settings.alternate.interfaceProtocol == 0x01) { 462 | // Runtime 463 | detachButton.disabled = false; 464 | uploadInternalButton.disabled = true; 465 | uploadExternalButton.disabled = true; 466 | uploadSlotAButton.disabled = true; 467 | uploadSlotBButton.disabled = true; 468 | downloadInternalButton.disabled = true; 469 | downloadExternalButton.disabled = true; 470 | downloadFlashAddressInput.disabled = true; 471 | downloadSlotAButton.disabled = true; 472 | downloadSlotBButton.disabled = true; 473 | downloadSlotAUserlandButton.disabled = true; 474 | downloadSlotBUserlandButton.disabled = true; 475 | bootSlotAUserlandButton.disabled = true; 476 | bootSlotBUserlandButton.disabled = true; 477 | firmwareFileField.disabled = true; 478 | } else { 479 | // DFU 480 | detachButton.disabled = true; 481 | uploadInternalButton.disabled = false; 482 | uploadExternalButton.disabled = false; 483 | uploadSlotAButton.disabled = false; 484 | uploadSlotBButton.disabled = false; 485 | downloadInternalButton.disabled = false; 486 | downloadExternalButton.disabled = false; 487 | downloadFlashAddressInput.disabled = false; 488 | downloadSlotAButton.disabled = false; 489 | downloadSlotBButton.disabled = false; 490 | downloadSlotAUserlandButton.disabled = false; 491 | downloadSlotBUserlandButton.disabled = false; 492 | bootSlotAUserlandButton.disabled = false; 493 | bootSlotBUserlandButton.disabled = false; 494 | firmwareFileField.disabled = false; 495 | } 496 | 497 | if (device.memoryInfo) { 498 | let dfuseFieldsDiv = document.querySelector("#dfuseFields") 499 | dfuseFieldsDiv.hidden = false; 500 | dfuseStartAddressField.disabled = false; 501 | dfuseUploadSizeField.disabled = false; 502 | let segment = device.getFirstWritableSegment(); 503 | if (segment) { 504 | device.startAddress = segment.start; 505 | dfuseStartAddressField.value = "0x" + segment.start.toString(16); 506 | const maxReadSize = device.getMaxReadSize(segment.start); 507 | dfuseUploadSizeField.value = maxReadSize; 508 | dfuseUploadSizeField.max = maxReadSize; 509 | } 510 | } else { 511 | let dfuseFieldsDiv = document.querySelector("#dfuseFields") 512 | dfuseFieldsDiv.hidden = true; 513 | dfuseStartAddressField.disabled = true; 514 | dfuseUploadSizeField.disabled = true; 515 | } 516 | 517 | return device; 518 | } 519 | 520 | function autoConnect(vid, pid, serial) { 521 | dfu.findAllDfuInterfaces().then( 522 | async dfu_devices => { 523 | let matching_devices = []; 524 | for (let dfu_device of dfu_devices) { 525 | if (serial) { 526 | if (dfu_device.device_.serialNumber == serial) { 527 | matching_devices.push(dfu_device); 528 | } 529 | } else { 530 | if ( 531 | (!pid && vid > 0 && dfu_device.device_.vendorId == vid) || 532 | (!vid && pid > 0 && dfu_device.device_.productId == pid) || 533 | (vid > 0 && pid > 0 && dfu_device.device_.vendorId == vid && dfu_device.device_.productId == pid) 534 | ) 535 | { 536 | matching_devices.push(dfu_device); 537 | } 538 | } 539 | } 540 | 541 | if (matching_devices.length == 0) { 542 | statusDisplay.textContent = 'No device found.'; 543 | } else { 544 | if (matching_devices.length == 1 || isNumWorks) { // For NumWorks, we want interface 0 ("Internal Flash") 545 | statusDisplay.textContent = 'Connecting...'; 546 | device = matching_devices[0]; 547 | console.log("Autoconnecting to device:", device); 548 | device = await connect(device); 549 | } else { 550 | statusDisplay.textContent = "Multiple DFU interfaces found."; 551 | } 552 | vidField.value = "0x" + hex4(matching_devices[0].device_.vendorId).toUpperCase(); 553 | vid = matching_devices[0].device_.vendorId; 554 | } 555 | } 556 | ); 557 | } 558 | 559 | vidField.addEventListener("change", function() { 560 | vid = parseInt(vidField.value, 16); 561 | }); 562 | 563 | transferSizeField.addEventListener("change", function() { 564 | transferSize = parseInt(transferSizeField.value); 565 | }); 566 | 567 | dfuseStartAddressField.addEventListener("change", function(event) { 568 | const field = event.target; 569 | let address = parseInt(field.value, 16); 570 | if (isNaN(address)) { 571 | field.setCustomValidity("Invalid hexadecimal start address"); 572 | } else if (device && device.memoryInfo) { 573 | if (device.getSegment(address) !== null) { 574 | device.startAddress = address; 575 | field.setCustomValidity(""); 576 | dfuseUploadSizeField.max = device.getMaxReadSize(address); 577 | } else { 578 | field.setCustomValidity("Address outside of memory map"); 579 | } 580 | } else { 581 | field.setCustomValidity(""); 582 | } 583 | }); 584 | 585 | connectButton.addEventListener('click', function() { 586 | if (device) { 587 | device.close().then(onDisconnect); 588 | device = null; 589 | } else { 590 | let filters = []; 591 | if (serial) { 592 | filters.push({ 'serialNumber': serial }); 593 | } else { 594 | if (vid) { 595 | filters.push({'vendorId': vid}); 596 | } 597 | if (vid && pid) { 598 | filters.push({'productId': pid, 'vendorId': vid}); 599 | } 600 | } 601 | navigator.usb.requestDevice({ 'filters': filters }).then( 602 | async selectedDevice => { 603 | let interfaces = dfu.findDeviceDfuInterfaces(selectedDevice); 604 | if (interfaces.length == 0) { 605 | console.log(selectedDevice); 606 | statusDisplay.textContent = "The selected device does not have any USB DFU interfaces."; 607 | } else if (interfaces.length == 1 || isNumWorks) { // For NumWorks, we want interface 0 ("Internal Flash") 608 | await fixInterfaceNames(selectedDevice, interfaces); 609 | device = await connect(new dfu.Device(selectedDevice, interfaces[0])); 610 | } else { 611 | await fixInterfaceNames(selectedDevice, interfaces); 612 | populateInterfaceList(interfaceForm, selectedDevice, interfaces); 613 | async function connectToSelectedInterface() { 614 | interfaceForm.removeEventListener('submit', this); 615 | const index = interfaceForm.elements["interfaceIndex"].value; 616 | device = await connect(new dfu.Device(selectedDevice, interfaces[index])); 617 | } 618 | 619 | interfaceForm.addEventListener('submit', connectToSelectedInterface); 620 | 621 | interfaceDialog.addEventListener('cancel', function () { 622 | interfaceDialog.removeEventListener('cancel', this); 623 | interfaceForm.removeEventListener('submit', connectToSelectedInterface); 624 | }); 625 | 626 | interfaceDialog.showModal(); 627 | } 628 | } 629 | ).catch(error => { 630 | statusDisplay.textContent = error; 631 | }); 632 | } 633 | }); 634 | 635 | detachButton.addEventListener('click', function() { 636 | if (device) { 637 | device.detach().then( 638 | async len => { 639 | let detached = false; 640 | try { 641 | await device.close(); 642 | await device.waitDisconnected(5000); 643 | detached = true; 644 | } catch (err) { 645 | console.log("Detach failed: " + err); 646 | } 647 | 648 | onDisconnect(); 649 | device = null; 650 | if (detached) { 651 | // Wait a few seconds and try reconnecting 652 | setTimeout(autoConnect, 5000); 653 | } 654 | }, 655 | async error => { 656 | await device.close(); 657 | onDisconnect(error); 658 | device = null; 659 | } 660 | ); 661 | } 662 | }); 663 | 664 | function uploadEventListener(uploadFunction) { 665 | return async function(event) { 666 | event.preventDefault(); 667 | event.stopPropagation(); 668 | if (!configForm.checkValidity()) { 669 | configForm.reportValidity(); 670 | return false; 671 | } 672 | 673 | if (!device || !device.device_.opened) { 674 | onDisconnect(); 675 | device = null; 676 | } else { 677 | setLogContext(uploadLog); 678 | clearLog(uploadLog); 679 | try { 680 | let status = await device.getStatus(); 681 | if (status.state == dfu.dfuERROR) { 682 | await device.clearStatus(); 683 | } 684 | } catch (error) { 685 | device.logWarning("Failed to clear status"); 686 | } 687 | 688 | let maxSize = Infinity; 689 | if (!dfuseUploadSizeField.disabled) { 690 | maxSize = parseInt(dfuseUploadSizeField.value); 691 | } 692 | 693 | try { 694 | await uploadFunction(maxSize); 695 | } catch (error) { 696 | logError(error); 697 | } 698 | 699 | setLogContext(null); 700 | } 701 | 702 | return false; 703 | } 704 | }; 705 | 706 | uploadInternalButton.addEventListener('click', uploadEventListener(async function(maxSize) { 707 | device.startAddress = 0x08000000; 708 | const blob = await device.do_upload(transferSize, maxSize); 709 | saveAs(blob, "internal.bin"); 710 | })); 711 | 712 | uploadExternalButton.addEventListener('click', uploadEventListener(async function(maxSize) { 713 | device.startAddress = 0x90000000; 714 | const blob = await device.do_upload(transferSize, 8192*1024); 715 | saveAs(blob, "external.bin"); 716 | })); 717 | 718 | uploadSlotAButton.addEventListener('click', uploadEventListener(async function(maxSize) { 719 | device.startAddress = 0x90000000; 720 | const blob = await device.do_upload(transferSize, 4096*1024); 721 | saveAs(blob, "slotA.bin"); 722 | })); 723 | 724 | uploadSlotBButton.addEventListener('click', uploadEventListener(async function(maxSize) { 725 | device.startAddress = 0x90400000; 726 | const blob = await device.do_upload(transferSize, 4096*1024); 727 | saveAs(blob, "slotB.bin"); 728 | })); 729 | 730 | firmwareFileField.addEventListener("change", function() { 731 | firmwareFile = null; 732 | if (firmwareFileField.files.length > 0) { 733 | let file = firmwareFileField.files[0]; 734 | let reader = new FileReader(); 735 | reader.onload = function() { 736 | firmwareFile = reader.result; 737 | }; 738 | reader.readAsArrayBuffer(file); 739 | } 740 | }); 741 | 742 | function downloadEventListener(downloadFunction, isReboot=false) { 743 | return async function(event) { 744 | event.preventDefault(); 745 | event.stopPropagation(); 746 | if (!configForm.checkValidity()) { 747 | configForm.reportValidity(); 748 | return false; 749 | } 750 | 751 | if (device && firmwareFile != null) { 752 | setLogContext(downloadLog); 753 | clearLog(downloadLog); 754 | try { 755 | let status = await device.getStatus(); 756 | if (status.state == dfu.dfuERROR) { 757 | await device.clearStatus(); 758 | } 759 | } catch (error) { 760 | device.logWarning("Failed to clear status"); 761 | } 762 | await downloadFunction().then( 763 | () => { 764 | logInfo("Done!"); 765 | setLogContext(null); 766 | if (!manifestationTolerant) { 767 | device.waitDisconnected(5000).then( 768 | dev => { 769 | onDisconnect(); 770 | device = null; 771 | }, 772 | error => { 773 | // It didn't reset and disconnect for some reason... 774 | console.log("Device unexpectedly tolerated manifestation."); 775 | } 776 | ); 777 | } 778 | }, 779 | error => { 780 | logError(error); 781 | setLogContext(null); 782 | } 783 | ) 784 | } else if (device && isReboot) { 785 | setLogContext(downloadLog); 786 | clearLog(downloadLog); 787 | try { 788 | let status = await device.getStatus(); 789 | if (status.state == dfu.dfuERROR) { 790 | await device.clearStatus(); 791 | } 792 | } catch (error) { 793 | device.logWarning("Failed to clear status"); 794 | } 795 | await downloadFunction().then( 796 | () => { 797 | logInfo("Done!"); 798 | setLogContext(null); 799 | if (!manifestationTolerant) { 800 | device.waitDisconnected(5000).then( 801 | dev => { 802 | onDisconnect(); 803 | device = null; 804 | }, 805 | error => { 806 | // It didn't reset and disconnect for some reason... 807 | console.log("Device unexpectedly tolerated manifestation."); 808 | }); 809 | } 810 | }, 811 | error => { 812 | logError(error); 813 | setLogContext(null); 814 | } 815 | ) 816 | } 817 | 818 | 819 | 820 | 821 | //return false; 822 | } 823 | } 824 | 825 | downloadInternalButton.addEventListener('click', downloadEventListener(async function() { 826 | device.startAddress = 0x08000000; 827 | return device.do_download(transferSize, firmwareFile, true); 828 | })); 829 | 830 | downloadExternalButton.addEventListener('click', downloadEventListener(async function() { 831 | device.startAddress = 0x90000000; 832 | return device.do_download(transferSize, firmwareFile, false); 833 | })); 834 | 835 | downloadSlotAButton.addEventListener('click', downloadEventListener(async function() { 836 | device.startAddress = 0x90000000; 837 | return device.do_download(transferSize, firmwareFile, false); 838 | })); 839 | 840 | downloadSlotBButton.addEventListener('click', downloadEventListener(async function() { 841 | device.startAddress = 0x90400000; 842 | return device.do_download(transferSize, firmwareFile, false); 843 | })); 844 | 845 | downloadSlotAUserlandButton.addEventListener('click', downloadEventListener(async function () { 846 | device.startAddress = 0x90010000; 847 | return device.do_download(transferSize, firmwareFile, true); 848 | })); 849 | 850 | downloadSlotBUserlandButton.addEventListener('click', downloadEventListener(async function () { 851 | device.startAddress = 0x90410000; 852 | return device.do_download(transferSize, firmwareFile, true); 853 | })); 854 | 855 | 856 | bootSlotAUserlandButton.addEventListener('click', downloadEventListener(async function () { 857 | device.startAddress = 0x90010000; 858 | return device.do_download(transferSize, firmwareFile, true, true); 859 | }, true)); 860 | 861 | bootSlotBUserlandButton.addEventListener('click', downloadEventListener(async function () { 862 | device.startAddress = 0x90410000; 863 | return device.do_download(transferSize, firmwareFile, true, true); 864 | }, true)); 865 | 866 | // Check if WebUSB is available 867 | if (typeof navigator.usb !== 'undefined') { 868 | navigator.usb.addEventListener("disconnect", onUnexpectedDisconnect); 869 | // Try connecting automatically 870 | if (doAutoConnect) { 871 | autoConnect(vid, pid, serial); 872 | } 873 | } else { 874 | statusDisplay.textContent = 'WebUSB not available.' 875 | connectButton.disabled = true; 876 | } 877 | }); 878 | })(); 879 | -------------------------------------------------------------------------------- /n0110/dfu.js: -------------------------------------------------------------------------------- 1 | var dfu = {}; 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | dfu.DETACH = 0x00; 7 | dfu.DNLOAD = 0x01; 8 | dfu.UPLOAD = 0x02; 9 | dfu.GETSTATUS = 0x03; 10 | dfu.CLRSTATUS = 0x04; 11 | dfu.GETSTATE = 0x05; 12 | dfu.ABORT = 6; 13 | 14 | dfu.appIDLE = 0; 15 | dfu.appDETACH = 1; 16 | dfu.dfuIDLE = 2; 17 | dfu.dfuDNLOAD_SYNC = 3; 18 | dfu.dfuDNBUSY = 4; 19 | dfu.dfuDNLOAD_IDLE = 5; 20 | dfu.dfuMANIFEST_SYNC = 6; 21 | dfu.dfuMANIFEST = 7; 22 | dfu.dfuMANIFEST_WAIT_RESET = 8; 23 | dfu.dfuUPLOAD_IDLE = 9; 24 | dfu.dfuERROR = 10; 25 | 26 | dfu.STATUS_OK = 0x0; 27 | 28 | dfu.Device = function(device, settings) { 29 | this.device_ = device; 30 | this.settings = settings; 31 | this.intfNumber = settings["interface"].interfaceNumber; 32 | }; 33 | 34 | dfu.findDeviceDfuInterfaces = function(device) { 35 | let interfaces = []; 36 | for (let conf of device.configurations) { 37 | for (let intf of conf.interfaces) { 38 | for (let alt of intf.alternates) { 39 | if (alt.interfaceClass == 0xFE && 40 | alt.interfaceSubclass == 0x01 && 41 | (alt.interfaceProtocol == 0x01 || alt.interfaceProtocol == 0x02)) { 42 | let settings = { 43 | "configuration": conf, 44 | "interface": intf, 45 | "alternate": alt, 46 | "name": alt.interfaceName 47 | }; 48 | interfaces.push(settings); 49 | } 50 | } 51 | } 52 | } 53 | 54 | return interfaces; 55 | } 56 | 57 | dfu.findAllDfuInterfaces = function() { 58 | return navigator.usb.getDevices().then( 59 | devices => { 60 | let matches = []; 61 | for (let device of devices) { 62 | let interfaces = dfu.findDeviceDfuInterfaces(device); 63 | for (let interface_ of interfaces) { 64 | matches.push(new dfu.Device(device, interface_)) 65 | } 66 | } 67 | return matches; 68 | } 69 | ) 70 | }; 71 | 72 | dfu.Device.prototype.logDebug = function(msg) { 73 | 74 | }; 75 | 76 | dfu.Device.prototype.logInfo = function(msg) { 77 | console.log(msg); 78 | }; 79 | 80 | dfu.Device.prototype.logWarning = function(msg) { 81 | console.log(msg); 82 | }; 83 | 84 | dfu.Device.prototype.logError = function(msg) { 85 | console.log(msg); 86 | }; 87 | 88 | dfu.Device.prototype.logProgress = function(done, total) { 89 | if (typeof total === 'undefined') { 90 | console.log(done) 91 | } else { 92 | console.log(done + '/' + total); 93 | } 94 | }; 95 | 96 | dfu.Device.prototype.open = async function() { 97 | await this.device_.open(); 98 | const confValue = this.settings.configuration.configurationValue; 99 | if (this.device_.configuration === null || 100 | this.device_.configuration.configurationValue != confValue) { 101 | await this.device_.selectConfiguration(confValue); 102 | } 103 | 104 | const intfNumber = this.settings["interface"].interfaceNumber; 105 | if (!this.device_.configuration.interfaces[intfNumber].claimed) { 106 | await this.device_.claimInterface(intfNumber); 107 | } 108 | 109 | const altSetting = this.settings.alternate.alternateSetting; 110 | let intf = this.device_.configuration.interfaces[intfNumber]; 111 | if (intf.alternate === null || 112 | intf.alternate.alternateSetting != altSetting) { 113 | await this.device_.selectAlternateInterface(intfNumber, altSetting); 114 | } 115 | } 116 | 117 | dfu.Device.prototype.close = async function() { 118 | try { 119 | await this.device_.close(); 120 | } catch (error) { 121 | console.log(error); 122 | } 123 | }; 124 | 125 | dfu.Device.prototype.readDeviceDescriptor = function() { 126 | const GET_DESCRIPTOR = 0x06; 127 | const DT_DEVICE = 0x01; 128 | const wValue = (DT_DEVICE << 8); 129 | 130 | return this.device_.controlTransferIn({ 131 | "requestType": "standard", 132 | "recipient": "device", 133 | "request": GET_DESCRIPTOR, 134 | "value": wValue, 135 | "index": 0 136 | }, 18).then( 137 | result => { 138 | if (result.status == "ok") { 139 | return Promise.resolve(result.data); 140 | } else { 141 | return Promise.reject(result.status); 142 | } 143 | } 144 | ); 145 | }; 146 | 147 | dfu.Device.prototype.readStringDescriptor = async function(index, langID) { 148 | if (typeof langID === 'undefined') { 149 | langID = 0; 150 | } 151 | 152 | const GET_DESCRIPTOR = 0x06; 153 | const DT_STRING = 0x03; 154 | const wValue = (DT_STRING << 8) | index; 155 | 156 | const request_setup = { 157 | "requestType": "standard", 158 | "recipient": "device", 159 | "request": GET_DESCRIPTOR, 160 | "value": wValue, 161 | "index": langID 162 | } 163 | 164 | // Read enough for bLength 165 | var result = await this.device_.controlTransferIn(request_setup, 1); 166 | 167 | if (result.status == "ok") { 168 | // Retrieve the full descriptor 169 | const bLength = result.data.getUint8(0); 170 | result = await this.device_.controlTransferIn(request_setup, bLength); 171 | if (result.status == "ok") { 172 | const len = (bLength-2) / 2; 173 | let u16_words = []; 174 | for (let i=0; i < len; i++) { 175 | u16_words.push(result.data.getUint16(2+i*2, true)); 176 | } 177 | if (langID == 0) { 178 | // Return the langID array 179 | return u16_words; 180 | } else { 181 | // Decode from UCS-2 into a string 182 | return String.fromCharCode.apply(String, u16_words); 183 | } 184 | } 185 | } 186 | 187 | throw `Failed to read string descriptor ${index}: ${result.status}`; 188 | }; 189 | 190 | dfu.Device.prototype.readInterfaceNames = async function() { 191 | const DT_INTERFACE = 4; 192 | 193 | let configs = {}; 194 | let allStringIndices = new Set(); 195 | for (let configIndex=0; configIndex < this.device_.configurations.length; configIndex++) { 196 | const rawConfig = await this.readConfigurationDescriptor(configIndex); 197 | let configDesc = dfu.parseConfigurationDescriptor(rawConfig); 198 | let configValue = configDesc.bConfigurationValue; 199 | configs[configValue] = {}; 200 | 201 | // Retrieve string indices for interface names 202 | for (let desc of configDesc.descriptors) { 203 | if (desc.bDescriptorType == DT_INTERFACE) { 204 | if (!(desc.bInterfaceNumber in configs[configValue])) { 205 | configs[configValue][desc.bInterfaceNumber] = {}; 206 | } 207 | configs[configValue][desc.bInterfaceNumber][desc.bAlternateSetting] = desc.iInterface; 208 | if (desc.iInterface > 0) { 209 | allStringIndices.add(desc.iInterface); 210 | } 211 | } 212 | } 213 | } 214 | 215 | let strings = {}; 216 | // Retrieve interface name strings 217 | for (let index of allStringIndices) { 218 | try { 219 | strings[index] = await this.readStringDescriptor(index, 0x0409); 220 | } catch (error) { 221 | console.log(error); 222 | strings[index] = null; 223 | } 224 | } 225 | 226 | for (let configValue in configs) { 227 | for (let intfNumber in configs[configValue]) { 228 | for (let alt in configs[configValue][intfNumber]) { 229 | const iIndex = configs[configValue][intfNumber][alt]; 230 | configs[configValue][intfNumber][alt] = strings[iIndex]; 231 | } 232 | } 233 | } 234 | 235 | return configs; 236 | }; 237 | 238 | dfu.parseDeviceDescriptor = function(data) { 239 | return { 240 | bLength: data.getUint8(0), 241 | bDescriptorType: data.getUint8(1), 242 | bcdUSB: data.getUint16(2, true), 243 | bDeviceClass: data.getUint8(4), 244 | bDeviceSubClass: data.getUint8(5), 245 | bDeviceProtocol: data.getUint8(6), 246 | bMaxPacketSize: data.getUint8(7), 247 | idVendor: data.getUint16(8, true), 248 | idProduct: data.getUint16(10, true), 249 | bcdDevice: data.getUint16(12, true), 250 | iManufacturer: data.getUint8(14), 251 | iProduct: data.getUint8(15), 252 | iSerialNumber: data.getUint8(16), 253 | bNumConfigurations: data.getUint8(17), 254 | }; 255 | }; 256 | 257 | dfu.parseConfigurationDescriptor = function(data) { 258 | let descriptorData = new DataView(data.buffer.slice(9)); 259 | let descriptors = dfu.parseSubDescriptors(descriptorData); 260 | return { 261 | bLength: data.getUint8(0), 262 | bDescriptorType: data.getUint8(1), 263 | wTotalLength: data.getUint16(2, true), 264 | bNumInterfaces: data.getUint8(4), 265 | bConfigurationValue:data.getUint8(5), 266 | iConfiguration: data.getUint8(6), 267 | bmAttributes: data.getUint8(7), 268 | bMaxPower: data.getUint8(8), 269 | descriptors: descriptors 270 | }; 271 | }; 272 | 273 | dfu.parseInterfaceDescriptor = function(data) { 274 | return { 275 | bLength: data.getUint8(0), 276 | bDescriptorType: data.getUint8(1), 277 | bInterfaceNumber: data.getUint8(2), 278 | bAlternateSetting: data.getUint8(3), 279 | bNumEndpoints: data.getUint8(4), 280 | bInterfaceClass: data.getUint8(5), 281 | bInterfaceSubClass: data.getUint8(6), 282 | bInterfaceProtocol: data.getUint8(7), 283 | iInterface: data.getUint8(8), 284 | descriptors: [] 285 | }; 286 | }; 287 | 288 | dfu.parseFunctionalDescriptor = function(data) { 289 | return { 290 | bLength: data.getUint8(0), 291 | bDescriptorType: data.getUint8(1), 292 | bmAttributes: data.getUint8(2), 293 | wDetachTimeOut: data.getUint16(3, true), 294 | wTransferSize: data.getUint16(5, true), 295 | bcdDFUVersion: data.getUint16(7, true) 296 | }; 297 | }; 298 | 299 | dfu.parseSubDescriptors = function(descriptorData) { 300 | const DT_INTERFACE = 4; 301 | const DT_ENDPOINT = 5; 302 | const DT_DFU_FUNCTIONAL = 0x21; 303 | const USB_CLASS_APP_SPECIFIC = 0xFE; 304 | const USB_SUBCLASS_DFU = 0x01; 305 | let remainingData = descriptorData; 306 | let descriptors = []; 307 | let currIntf; 308 | let inDfuIntf = false; 309 | while (remainingData.byteLength > 2) { 310 | let bLength = remainingData.getUint8(0); 311 | let bDescriptorType = remainingData.getUint8(1); 312 | let descData = new DataView(remainingData.buffer.slice(0, bLength)); 313 | if (bDescriptorType == DT_INTERFACE) { 314 | currIntf = dfu.parseInterfaceDescriptor(descData); 315 | if (currIntf.bInterfaceClass == USB_CLASS_APP_SPECIFIC && 316 | currIntf.bInterfaceSubClass == USB_SUBCLASS_DFU) { 317 | inDfuIntf = true; 318 | } else { 319 | inDfuIntf = false; 320 | } 321 | descriptors.push(currIntf); 322 | } else if (inDfuIntf && bDescriptorType == DT_DFU_FUNCTIONAL) { 323 | let funcDesc = dfu.parseFunctionalDescriptor(descData) 324 | descriptors.push(funcDesc); 325 | currIntf.descriptors.push(funcDesc); 326 | } else { 327 | let desc = { 328 | bLength: bLength, 329 | bDescriptorType: bDescriptorType, 330 | data: descData 331 | }; 332 | descriptors.push(desc); 333 | if (currIntf) { 334 | currIntf.descriptors.push(desc); 335 | } 336 | } 337 | remainingData = new DataView(remainingData.buffer.slice(bLength)); 338 | } 339 | 340 | return descriptors; 341 | }; 342 | 343 | dfu.Device.prototype.readConfigurationDescriptor = function(index) { 344 | const GET_DESCRIPTOR = 0x06; 345 | const DT_CONFIGURATION = 0x02; 346 | const wValue = ((DT_CONFIGURATION << 8) | index); 347 | 348 | return this.device_.controlTransferIn({ 349 | "requestType": "standard", 350 | "recipient": "device", 351 | "request": GET_DESCRIPTOR, 352 | "value": wValue, 353 | "index": 0 354 | }, 4).then( 355 | result => { 356 | if (result.status == "ok") { 357 | // Read out length of the configuration descriptor 358 | let wLength = result.data.getUint16(2, true); 359 | return this.device_.controlTransferIn({ 360 | "requestType": "standard", 361 | "recipient": "device", 362 | "request": GET_DESCRIPTOR, 363 | "value": wValue, 364 | "index": 0 365 | }, wLength); 366 | } else { 367 | return Promise.reject(result.status); 368 | } 369 | } 370 | ).then( 371 | result => { 372 | if (result.status == "ok") { 373 | return Promise.resolve(result.data); 374 | } else { 375 | return Promise.reject(result.status); 376 | } 377 | } 378 | ); 379 | }; 380 | 381 | dfu.Device.prototype.requestOut = function(bRequest, data, wValue=0) { 382 | return this.device_.controlTransferOut({ 383 | "requestType": "class", 384 | "recipient": "interface", 385 | "request": bRequest, 386 | "value": wValue, 387 | "index": this.intfNumber 388 | }, data).then( 389 | result => { 390 | if (result.status == "ok") { 391 | return Promise.resolve(result.bytesWritten); 392 | } else { 393 | return Promise.reject(result.status); 394 | } 395 | }, 396 | error => { 397 | return Promise.reject("ControlTransferOut failed: " + error); 398 | } 399 | ); 400 | }; 401 | 402 | dfu.Device.prototype.requestIn = function(bRequest, wLength, wValue=0) { 403 | return this.device_.controlTransferIn({ 404 | "requestType": "class", 405 | "recipient": "interface", 406 | "request": bRequest, 407 | "value": wValue, 408 | "index": this.intfNumber 409 | }, wLength).then( 410 | result => { 411 | if (result.status == "ok") { 412 | return Promise.resolve(result.data); 413 | } else { 414 | return Promise.reject(result.status); 415 | } 416 | }, 417 | error => { 418 | return Promise.reject("ControlTransferIn failed: " + error); 419 | } 420 | ); 421 | }; 422 | 423 | dfu.Device.prototype.detach = function() { 424 | return this.requestOut(dfu.DETACH, undefined, 1000); 425 | } 426 | 427 | dfu.Device.prototype.waitDisconnected = async function(timeout) { 428 | let device = this; 429 | let usbDevice = this.device_; 430 | return new Promise(function(resolve, reject) { 431 | let timeoutID; 432 | if (timeout > 0) { 433 | function onTimeout() { 434 | navigator.usb.removeEventListener("disconnect", onDisconnect); 435 | if (device.disconnected !== true) { 436 | reject("Disconnect timeout expired"); 437 | } 438 | } 439 | timeoutID = setTimeout(reject, timeout); 440 | } 441 | 442 | function onDisconnect(event) { 443 | if (event.device === usbDevice) { 444 | if (timeout > 0) { 445 | clearTimeout(timeoutID); 446 | } 447 | device.disconnected = true; 448 | navigator.usb.removeEventListener("disconnect", onDisconnect); 449 | event.stopPropagation(); 450 | resolve(device); 451 | } 452 | } 453 | 454 | navigator.usb.addEventListener("disconnect", onDisconnect); 455 | }); 456 | }; 457 | 458 | dfu.Device.prototype.download = function(data, blockNum) { 459 | return this.requestOut(dfu.DNLOAD, data, blockNum); 460 | }; 461 | 462 | dfu.Device.prototype.dnload = dfu.Device.prototype.download; 463 | 464 | dfu.Device.prototype.upload = function(length, blockNum) { 465 | return this.requestIn(dfu.UPLOAD, length, blockNum) 466 | }; 467 | 468 | dfu.Device.prototype.clearStatus = function() { 469 | return this.requestOut(dfu.CLRSTATUS); 470 | }; 471 | 472 | dfu.Device.prototype.clrStatus = dfu.Device.prototype.clearStatus; 473 | 474 | dfu.Device.prototype.getStatus = function() { 475 | return this.requestIn(dfu.GETSTATUS, 6).then( 476 | data => 477 | Promise.resolve({ 478 | "status": data.getUint8(0), 479 | "pollTimeout": data.getUint32(1, true) & 0xFFFFFF, 480 | "state": data.getUint8(4) 481 | }), 482 | error => 483 | Promise.reject("DFU GETSTATUS failed: " + error) 484 | ); 485 | }; 486 | 487 | dfu.Device.prototype.getState = function() { 488 | return this.requestIn(dfu.GETSTATE, 1).then( 489 | data => Promise.resolve(data.getUint8(0)), 490 | error => Promise.reject("DFU GETSTATE failed: " + error) 491 | ); 492 | }; 493 | 494 | dfu.Device.prototype.abort = function() { 495 | return this.requestOut(dfu.ABORT); 496 | }; 497 | 498 | dfu.Device.prototype.abortToIdle = async function() { 499 | await this.abort(); 500 | let state = await this.getState(); 501 | if (state == dfu.dfuERROR) { 502 | await this.clearStatus(); 503 | state = await this.getState(); 504 | } 505 | if (state != dfu.dfuIDLE) { 506 | throw "Failed to return to idle state after abort: state " + state.state; 507 | } 508 | }; 509 | 510 | dfu.Device.prototype.do_upload = async function(xfer_size, max_size=Infinity, first_block=0) { 511 | let transaction = first_block; 512 | let blocks = []; 513 | let bytes_read = 0; 514 | 515 | this.logInfo("Copying data from DFU device to browser"); 516 | // Initialize progress to 0 517 | this.logProgress(0); 518 | 519 | let result; 520 | let bytes_to_read; 521 | do { 522 | bytes_to_read = Math.min(xfer_size, max_size - bytes_read); 523 | result = await this.upload(bytes_to_read, transaction++); 524 | this.logDebug("Read " + result.byteLength + " bytes"); 525 | if (result.byteLength > 0) { 526 | blocks.push(result); 527 | bytes_read += result.byteLength; 528 | } 529 | if (Number.isFinite(max_size)) { 530 | this.logProgress(bytes_read, max_size); 531 | } else { 532 | this.logProgress(bytes_read); 533 | } 534 | } while ((bytes_read < max_size) && (result.byteLength == bytes_to_read)); 535 | 536 | if (bytes_read == max_size) { 537 | await this.abortToIdle(); 538 | } 539 | 540 | this.logInfo(`Read ${bytes_read} bytes`); 541 | 542 | return new Blob(blocks, { type: "application/octet-stream" }); 543 | }; 544 | 545 | dfu.Device.prototype.poll_until = async function(state_predicate) { 546 | let dfu_status = await this.getStatus(); 547 | 548 | let device = this; 549 | function async_sleep(duration_ms) { 550 | return new Promise(function(resolve, reject) { 551 | device.logDebug("Sleeping for " + duration_ms + "ms"); 552 | setTimeout(resolve, duration_ms); 553 | }); 554 | } 555 | 556 | while (!state_predicate(dfu_status.state) && dfu_status.state != dfu.dfuERROR) { 557 | await async_sleep(dfu_status.pollTimeout); 558 | dfu_status = await this.getStatus(); 559 | } 560 | 561 | return dfu_status; 562 | }; 563 | 564 | dfu.Device.prototype.poll_until_idle = function(idle_state) { 565 | return this.poll_until(state => (state == idle_state)); 566 | }; 567 | 568 | dfu.Device.prototype.do_download = async function(xfer_size, data, manifestationTolerant, isReboot=false) { 569 | let bytes_sent = 0; 570 | let expected_size = data.byteLength; 571 | let transaction = 0; 572 | 573 | if (!isReboot) { 574 | this.logInfo("Copying data from browser to DFU device"); 575 | 576 | // Initialize progress to 0 577 | this.logProgress(bytes_sent, expected_size); 578 | 579 | while (bytes_sent < expected_size) { 580 | const bytes_left = expected_size - bytes_sent; 581 | const chunk_size = Math.min(bytes_left, xfer_size); 582 | 583 | let bytes_written = 0; 584 | let dfu_status; 585 | try { 586 | bytes_written = await this.download(data.slice(bytes_sent, bytes_sent + chunk_size), transaction++); 587 | this.logDebug("Sent " + bytes_written + " bytes"); 588 | dfu_status = await this.poll_until_idle(dfu.dfuDNLOAD_IDLE); 589 | } catch (error) { 590 | throw "Error during DFU download: " + error; 591 | } 592 | 593 | if (dfu_status.status != dfu.STATUS_OK) { 594 | throw `DFU DOWNLOAD failed state=${dfu_status.state}, status=${dfu_status.status}`; 595 | } 596 | 597 | this.logDebug("Wrote " + bytes_written + " bytes"); 598 | bytes_sent += bytes_written; 599 | 600 | this.logProgress(bytes_sent, expected_size); 601 | } 602 | 603 | this.logDebug("Sending empty block"); 604 | try { 605 | await this.download(new ArrayBuffer([]), transaction++); 606 | } catch (error) { 607 | throw "Error during final DFU download: " + error; 608 | } 609 | 610 | this.logInfo("Wrote " + bytes_sent + " bytes"); 611 | this.logInfo("Manifesting new firmware"); 612 | } 613 | if (manifestationTolerant) { 614 | // Transition to MANIFEST_SYNC state 615 | let dfu_status; 616 | try { 617 | // Wait until it returns to idle. 618 | // If it's not really manifestation tolerant, it might transition to MANIFEST_WAIT_RESET 619 | dfu_status = await this.poll_until(state => (state == dfu.dfuIDLE || state == dfu.dfuMANIFEST_WAIT_RESET)); 620 | if (dfu_status.state == dfu.dfuMANIFEST_WAIT_RESET) { 621 | this.logDebug("Device transitioned to MANIFEST_WAIT_RESET even though it is manifestation tolerant"); 622 | } 623 | if (dfu_status.status != dfu.STATUS_OK) { 624 | throw `DFU MANIFEST failed state=${dfu_status.state}, status=${dfu_status.status}`; 625 | } 626 | } catch (error) { 627 | if (error.endsWith("ControlTransferIn failed: NotFoundError: Device unavailable.") || 628 | error.endsWith("ControlTransferIn failed: NotFoundError: The device was disconnected.")) { 629 | this.logWarning("Unable to poll final manifestation status"); 630 | } else { 631 | throw "Error during DFU manifest: " + error; 632 | } 633 | } 634 | } else { 635 | // Try polling once to initiate manifestation 636 | try { 637 | let final_status = await this.getStatus(); 638 | this.logDebug(`Final DFU status: state=${final_status.state}, status=${final_status.status}`); 639 | } catch (error) { 640 | this.logDebug("Manifest GET_STATUS poll error: " + error); 641 | } 642 | } 643 | 644 | // Reset to exit MANIFEST_WAIT_RESET 645 | try { 646 | await this.device_.reset(); 647 | } catch (error) { 648 | if (error == "NetworkError: Unable to reset the device." || 649 | error == "NotFoundError: Device unavailable." || 650 | error == "NotFoundError: The device was disconnected.") { 651 | this.logDebug("Ignored reset error"); 652 | } else { 653 | throw "Error during reset for manifestation: " + error; 654 | } 655 | } 656 | 657 | return; 658 | }; 659 | 660 | })(); 661 | -------------------------------------------------------------------------------- /n0110/dfuse.js: -------------------------------------------------------------------------------- 1 | /* dfu.js must be included before dfuse.js */ 2 | 3 | var dfuse = {}; 4 | 5 | (function() { 6 | 'use strict'; 7 | 8 | dfuse.GET_COMMANDS = 0x00; 9 | dfuse.SET_ADDRESS = 0x21; 10 | dfuse.ERASE_SECTOR = 0x41; 11 | 12 | dfuse.Device = function(device, settings) { 13 | dfu.Device.call(this, device, settings); 14 | this.memoryInfo = null; 15 | this.startAddress = NaN; 16 | if (settings.name) { 17 | this.memoryInfo = dfuse.parseMemoryDescriptor(settings.name); 18 | } 19 | } 20 | 21 | dfuse.Device.prototype = Object.create(dfu.Device.prototype); 22 | dfuse.Device.prototype.constructor = dfuse.Device; 23 | 24 | dfuse.parseMemoryDescriptor = function(desc) { 25 | const nameEndIndex = desc.indexOf("/"); 26 | if (!desc.startsWith("@") || nameEndIndex == -1) { 27 | throw `Not a DfuSe memory descriptor: "${desc}"`; 28 | } 29 | 30 | const name = desc.substring(1, nameEndIndex).trim(); 31 | const segmentString = desc.substring(nameEndIndex); 32 | 33 | let segments = []; 34 | 35 | const sectorMultipliers = { 36 | ' ': 1, 37 | 'B': 1, 38 | 'K': 1024, 39 | 'M': 1048576 40 | }; 41 | 42 | let contiguousSegmentRegex = /\/\s*(0x[0-9a-fA-F]{1,8})\s*\/(\s*[0-9]+\s*\*\s*[0-9]+\s?[ BKM]\s*[abcdefg]\s*,?\s*)+/g; 43 | let contiguousSegmentMatch; 44 | while (contiguousSegmentMatch = contiguousSegmentRegex.exec(segmentString)) { 45 | let segmentRegex = /([0-9]+)\s*\*\s*([0-9]+)\s?([ BKM])\s*([abcdefg])\s*,?\s*/g; 46 | let startAddress = parseInt(contiguousSegmentMatch[1], 16); 47 | let segmentMatch; 48 | while (segmentMatch = segmentRegex.exec(contiguousSegmentMatch[0])) { 49 | let segment = {} 50 | let sectorCount = parseInt(segmentMatch[1], 10); 51 | let sectorSize = parseInt(segmentMatch[2]) * sectorMultipliers[segmentMatch[3]]; 52 | let properties = segmentMatch[4].charCodeAt(0) - 'a'.charCodeAt(0) + 1; 53 | segment.start = startAddress; 54 | segment.sectorSize = sectorSize; 55 | segment.end = startAddress + sectorSize * sectorCount; 56 | segment.readable = (properties & 0x1) != 0; 57 | segment.erasable = (properties & 0x2) != 0; 58 | segment.writable = (properties & 0x4) != 0; 59 | segments.push(segment); 60 | 61 | startAddress += sectorSize * sectorCount; 62 | } 63 | } 64 | 65 | return {"name": name, "segments": segments}; 66 | }; 67 | 68 | dfuse.Device.prototype.dfuseCommand = async function(command, param, len) { 69 | if (typeof param === 'undefined' && typeof len === 'undefined') { 70 | param = 0x00; 71 | len = 1; 72 | } 73 | 74 | const commandNames = { 75 | 0x00: "GET_COMMANDS", 76 | 0x21: "SET_ADDRESS", 77 | 0x41: "ERASE_SECTOR" 78 | }; 79 | 80 | let payload = new ArrayBuffer(len + 1); 81 | let view = new DataView(payload); 82 | view.setUint8(0, command); 83 | if (len == 1) { 84 | view.setUint8(1, param); 85 | } else if (len == 4) { 86 | view.setUint32(1, param, true); 87 | } else { 88 | throw "Don't know how to handle data of len " + len; 89 | } 90 | 91 | try { 92 | await this.download(payload, 0); 93 | } catch (error) { 94 | throw "Error during special DfuSe command " + commandNames[command] + ":" + error; 95 | } 96 | 97 | let status = await this.poll_until(state => (state != dfu.dfuDNBUSY)); 98 | if (status.status != dfu.STATUS_OK) { 99 | throw "Special DfuSe command " + commandName + " failed"; 100 | } 101 | }; 102 | 103 | dfuse.Device.prototype.getSegment = function(addr) { 104 | if (!this.memoryInfo || ! this.memoryInfo.segments) { 105 | throw "No memory map information available"; 106 | } 107 | 108 | for (let segment of this.memoryInfo.segments) { 109 | if (segment.start <= addr && addr < segment.end) { 110 | return segment; 111 | } 112 | } 113 | 114 | return null; 115 | }; 116 | 117 | dfuse.Device.prototype.getSectorStart = function(addr, segment) { 118 | if (typeof segment === 'undefined') { 119 | segment = this.getSegment(addr); 120 | } 121 | 122 | if (!segment) { 123 | throw `Address ${addr.toString(16)} outside of memory map`; 124 | } 125 | 126 | const sectorIndex = Math.floor((addr - segment.start)/segment.sectorSize); 127 | return segment.start + sectorIndex * segment.sectorSize; 128 | }; 129 | 130 | dfuse.Device.prototype.getSectorEnd = function(addr, segment) { 131 | if (typeof segment === 'undefined') { 132 | segment = this.getSegment(addr); 133 | } 134 | 135 | if (!segment) { 136 | throw `Address ${addr.toString(16)} outside of memory map`; 137 | } 138 | 139 | const sectorIndex = Math.floor((addr - segment.start)/segment.sectorSize); 140 | return segment.start + (sectorIndex + 1) * segment.sectorSize; 141 | }; 142 | 143 | dfuse.Device.prototype.getFirstWritableSegment = function() { 144 | if (!this.memoryInfo || ! this.memoryInfo.segments) { 145 | throw "No memory map information available"; 146 | } 147 | 148 | for (let segment of this.memoryInfo.segments) { 149 | if (segment.writable) { 150 | return segment; 151 | } 152 | } 153 | 154 | return null; 155 | }; 156 | 157 | dfuse.Device.prototype.getMaxReadSize = function(startAddr) { 158 | if (!this.memoryInfo || ! this.memoryInfo.segments) { 159 | throw "No memory map information available"; 160 | } 161 | 162 | let numBytes = 0; 163 | for (let segment of this.memoryInfo.segments) { 164 | if (segment.start <= startAddr && startAddr < segment.end) { 165 | // Found the first segment the read starts in 166 | if (segment.readable) { 167 | numBytes += segment.end - startAddr; 168 | } else { 169 | return 0; 170 | } 171 | } else if (segment.start == startAddr + numBytes) { 172 | // Include a contiguous segment 173 | if (segment.readable) { 174 | numBytes += (segment.end - segment.start); 175 | } else { 176 | break; 177 | } 178 | } 179 | } 180 | 181 | return numBytes; 182 | }; 183 | 184 | dfuse.Device.prototype.erase = async function(startAddr, length) { 185 | let segment = this.getSegment(startAddr); 186 | let addr = this.getSectorStart(startAddr, segment); 187 | const endAddr = this.getSectorEnd(startAddr + length - 1); 188 | 189 | let bytesErased = 0; 190 | const bytesToErase = endAddr - addr; 191 | if (bytesToErase > 0) { 192 | this.logProgress(bytesErased, bytesToErase); 193 | } 194 | 195 | while (addr < endAddr) { 196 | if (segment.end <= addr) { 197 | segment = this.getSegment(addr); 198 | } 199 | if (!segment.erasable) { 200 | // Skip over the non-erasable section 201 | bytesErased = Math.min(bytesErased + segment.end - addr, bytesToErase); 202 | addr = segment.end; 203 | this.logProgress(bytesErased, bytesToErase); 204 | continue; 205 | } 206 | const sectorIndex = Math.floor((addr - segment.start)/segment.sectorSize); 207 | const sectorAddr = segment.start + sectorIndex * segment.sectorSize; 208 | this.logDebug(`Erasing ${segment.sectorSize}B at 0x${sectorAddr.toString(16)}`); 209 | await this.dfuseCommand(dfuse.ERASE_SECTOR, sectorAddr, 4); 210 | addr = sectorAddr + segment.sectorSize; 211 | bytesErased += segment.sectorSize; 212 | this.logProgress(bytesErased, bytesToErase); 213 | } 214 | }; 215 | 216 | dfuse.Device.prototype.do_download = async function (xfer_size, data, manifestationTolerant, isReboot=false) { 217 | if (!this.memoryInfo || ! this.memoryInfo.segments) { 218 | throw "No memory map available"; 219 | } 220 | let startAddress = this.startAddress; 221 | if (!isReboot) { 222 | this.logInfo("Erasing DFU device memory"); 223 | 224 | let bytes_sent = 0; 225 | let expected_size = data.byteLength; 226 | if (isNaN(startAddress)) { 227 | startAddress = this.memoryInfo.segments[0].start; 228 | this.logWarning("Using inferred start address 0x" + startAddress.toString(16)); 229 | } else if (this.getSegment(startAddress) === null) { 230 | this.logError(`Start address 0x${startAddress.toString(16)} outside of memory map bounds`); 231 | } 232 | await this.erase(startAddress, expected_size); 233 | 234 | this.logInfo("Copying data from browser to DFU device"); 235 | 236 | let address = startAddress; 237 | while (bytes_sent < expected_size) { 238 | const bytes_left = expected_size - bytes_sent; 239 | const chunk_size = Math.min(bytes_left, xfer_size); 240 | 241 | let bytes_written = 0; 242 | let dfu_status; 243 | try { 244 | await this.dfuseCommand(dfuse.SET_ADDRESS, address, 4); 245 | this.logDebug(`Set address to 0x${address.toString(16)}`); 246 | bytes_written = await this.download(data.slice(bytes_sent, bytes_sent + chunk_size), 2); 247 | this.logDebug("Sent " + bytes_written + " bytes"); 248 | dfu_status = await this.poll_until_idle(dfu.dfuDNLOAD_IDLE); 249 | address += chunk_size; 250 | } catch (error) { 251 | throw "Error during DfuSe download: " + error; 252 | } 253 | 254 | if (dfu_status.status != dfu.STATUS_OK) { 255 | throw `DFU DOWNLOAD failed state=${dfu_status.state}, status=${dfu_status.status}`; 256 | } 257 | 258 | this.logDebug("Wrote " + bytes_written + " bytes"); 259 | bytes_sent += bytes_written; 260 | 261 | this.logProgress(bytes_sent, expected_size); 262 | } 263 | this.logInfo(`Wrote ${bytes_sent} bytes`); 264 | } 265 | if(manifestationTolerant) { 266 | this.logInfo("Manifesting new firmware"); 267 | try { 268 | await this.dfuseCommand(dfuse.SET_ADDRESS, startAddress, 4); 269 | await this.download(new ArrayBuffer(), 2); 270 | } catch (error) { 271 | throw "Error during DfuSe manifestation: " + error; 272 | } 273 | 274 | try { 275 | await this.poll_until(state => (state == dfu.dfuMANIFEST)); 276 | } catch (error) { 277 | this.logError(error); 278 | } 279 | } 280 | } 281 | 282 | dfuse.Device.prototype.do_upload = async function(xfer_size, max_size) { 283 | let startAddress = this.startAddress; 284 | if (isNaN(startAddress)) { 285 | startAddress = this.memoryInfo.segments[0].start; 286 | this.logWarning("Using inferred start address 0x" + startAddress.toString(16)); 287 | } else if (this.getSegment(startAddress) === null) { 288 | this.logWarning(`Start address 0x${startAddress.toString(16)} outside of memory map bounds`); 289 | } 290 | 291 | this.logInfo(`Reading up to 0x${max_size.toString(16)} bytes starting at 0x${startAddress.toString(16)}`); 292 | let state = await this.getState(); 293 | if (state != dfu.dfuIDLE) { 294 | await this.abortToIdle(); 295 | } 296 | await this.dfuseCommand(dfuse.SET_ADDRESS, startAddress, 4); 297 | await this.abortToIdle(); 298 | 299 | // DfuSe encodes the read address based on the transfer size, 300 | // the block number - 2, and the SET_ADDRESS pointer. 301 | return await dfu.Device.prototype.do_upload.call(this, xfer_size, max_size, 2); 302 | } 303 | })(); 304 | -------------------------------------------------------------------------------- /n0110/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebUSB DFU for NumWorks 6 | 7 | 8 | 9 | 10 | 25 | 26 | 27 |

28 | 29 |

30 |

31 | 32 | 33 | 34 | 35 |

36 |

37 | Plug in your NumWorks calculator, then click here:
38 | 39 |

40 | 41 | Your device has multiple DFU interfaces. Select one from the list below: 42 |
43 | 44 |
45 |
46 |

47 |

48 |
49 |

50 |
51 | Runtime mode 52 | 53 |
54 |
55 |
56 | 57 | 58 | 64 | 65 | DFU mode 66 |
67 | Flash firmware onto device 68 |

External memory, if available, should be flashed first.

69 |

70 | 71 |

72 |

73 | 74 | 75 | 76 | 77 | 78 |
79 | 80 |
81 | 82 | 83 |

84 |
85 |
86 |
87 | Dump flash from device 88 |

89 | 90 | 91 | 92 | 93 |

94 |
95 |
96 |
97 |
98 |
99 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /n0110/sakura.css: -------------------------------------------------------------------------------- 1 | /* Sakura.css v1.0.0 2 | * ================ 3 | * Minimal css theme. 4 | * Project: https://github.com/oxalorg/sakura 5 | */ 6 | /* Body */ 7 | html { 8 | font-size: 62.5%; 9 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } 10 | 11 | body { 12 | font-size: 1.8rem; 13 | line-height: 1.618; 14 | max-width: 38em; 15 | margin: auto; 16 | color: #4a4a4a; 17 | background-color: #f9f9f9; 18 | padding: 13px; } 19 | 20 | @media (max-width: 684px) { 21 | body { 22 | font-size: 1.53rem; } } 23 | @media (max-width: 382px) { 24 | body { 25 | font-size: 1.35rem; } } 26 | h1, h2, h3, h4, h5, h6 { 27 | line-height: 1.1; 28 | font-family: Verdana, Geneva, sans-serif; 29 | font-weight: 700; 30 | overflow-wrap: break-word; 31 | word-wrap: break-word; 32 | -ms-word-break: break-all; 33 | word-break: break-word; 34 | -ms-hyphens: auto; 35 | -moz-hyphens: auto; 36 | -webkit-hyphens: auto; 37 | hyphens: auto; } 38 | 39 | h1 { 40 | font-size: 2.35em; } 41 | 42 | h2 { 43 | font-size: 2em; } 44 | 45 | h3 { 46 | font-size: 1.75em; } 47 | 48 | h4 { 49 | font-size: 1.5em; } 50 | 51 | h5 { 52 | font-size: 1.25em; } 53 | 54 | h6 { 55 | font-size: 1em; } 56 | 57 | small, sub, sup { 58 | font-size: 75%; } 59 | 60 | hr { 61 | border-color: #2c8898; } 62 | 63 | a { 64 | text-decoration: none; 65 | color: #2c8898; } 66 | a:hover { 67 | color: #982c61; 68 | border-bottom: 2px solid #4a4a4a; } 69 | 70 | ul { 71 | padding-left: 1.4em; } 72 | 73 | li { 74 | margin-bottom: 0.4em; } 75 | 76 | blockquote { 77 | font-style: italic; 78 | margin-left: 1.5em; 79 | padding-left: 1em; 80 | border-left: 3px solid #2c8898; } 81 | 82 | img { 83 | max-width: 100%; } 84 | 85 | /* Pre and Code */ 86 | pre { 87 | background-color: #f1f1f1; 88 | display: block; 89 | padding: 1em; 90 | overflow-x: auto; } 91 | 92 | code { 93 | font-size: 0.9em; 94 | padding: 0 0.5em; 95 | background-color: #f1f1f1; 96 | white-space: pre-wrap; } 97 | 98 | pre > code { 99 | padding: 0; 100 | background-color: transparent; 101 | white-space: pre; } 102 | 103 | /* Tables */ 104 | table { 105 | text-align: justify; 106 | width: 100%; 107 | border-collapse: collapse; } 108 | 109 | td, th { 110 | padding: 0.5em; 111 | border-bottom: 1px solid #f1f1f1; } 112 | 113 | /* Buttons, forms and input */ 114 | input, textarea { 115 | border: 1px solid #4a4a4a; } 116 | input:focus, textarea:focus { 117 | border: 1px solid #2c8898; } 118 | 119 | textarea { 120 | width: 100%; } 121 | 122 | .button, button, input[type="submit"], input[type="reset"], input[type="button"] { 123 | margin: 5px; 124 | padding: 10px 20px; 125 | background-color: #faa039; 126 | color: white; 127 | border: none; 128 | font-size: 14px; 129 | font-weight: bold; 130 | border-radius: 7px; 131 | cursor: pointer; 132 | transition: background-color 0.3s ease; 133 | } 134 | 135 | .button[disabled], button[disabled], input[type="submit"][disabled], input[type="reset"][disabled], input[type="button"][disabled] { 136 | cursor: default; 137 | opacity: .5; } 138 | .button:focus, .button:hover, button:focus, button:hover, input[type="submit"]:focus, input[type="submit"]:hover, input[type="reset"]:focus, input[type="reset"]:hover, input[type="button"]:focus, input[type="button"]:hover { 139 | background-color: #982c61; 140 | border-color: #982c61; 141 | color: #f9f9f9; 142 | outline: 0; } 143 | 144 | textarea, select, input[type] { 145 | color: #4a4a4a; 146 | padding: 6px 10px; 147 | /* The 6px vertically centers text on FF, ignored by Webkit */ 148 | margin-bottom: 10px; 149 | background-color: #f1f1f1; 150 | border: 1px solid #f1f1f1; 151 | border-radius: 4px; 152 | box-shadow: none; 153 | box-sizing: border-box; } 154 | textarea:focus, select:focus, input[type]:focus { 155 | border: 1px solid #2c8898; 156 | outline: 0; } 157 | 158 | label, legend, fieldset { 159 | display: block; 160 | margin-bottom: .5rem; 161 | font-weight: 600; } 162 | 163 | fieldset { 164 | border: 1px solid #ccc; 165 | margin: 10px 0; 166 | padding: 15px; 167 | border-radius: 5px; 168 | } 169 | 170 | legend { 171 | font-weight: bold; 172 | color: #333; 173 | } 174 | 175 | /* Style for buttons inside fieldset */ 176 | 177 | fieldset button:disabled { 178 | background-color: #ccc; 179 | cursor: not-allowed; 180 | } 181 | 182 | fieldset button:hover { 183 | background-color: #f08307; 184 | } 185 | 186 | -------------------------------------------------------------------------------- /sakura.css: -------------------------------------------------------------------------------- 1 | /* Sakura.css v1.0.0 2 | * ================ 3 | * Minimal css theme. 4 | * Project: https://github.com/oxalorg/sakura 5 | */ 6 | /* Body */ 7 | html { 8 | font-size: 62.5%; 9 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 10 | } 11 | 12 | body { 13 | font-size: 1.8rem; 14 | line-height: 1.618; 15 | max-width: 38em; 16 | margin: auto; 17 | color: #4a4a4a; 18 | background-color: #f9f9f9; 19 | padding: 13px; 20 | } 21 | 22 | @media (max-width: 684px) { 23 | body { 24 | font-size: 1.53rem; } } 25 | @media (max-width: 382px) { 26 | body { 27 | font-size: 1.35rem; } } 28 | h1, h2, h3, h4, h5, h6 { 29 | line-height: 1.1; 30 | font-family: Verdana, Geneva, sans-serif; 31 | font-weight: 700; 32 | overflow-wrap: break-word; 33 | word-wrap: break-word; 34 | -ms-word-break: break-all; 35 | word-break: break-word; 36 | -ms-hyphens: auto; 37 | -moz-hyphens: auto; 38 | -webkit-hyphens: auto; 39 | hyphens: auto; } 40 | 41 | h1 { 42 | font-size: 2.35em; } 43 | 44 | h2 { 45 | font-size: 2em; } 46 | 47 | h3 { 48 | font-size: 1.75em; } 49 | 50 | h4 { 51 | font-size: 1.5em; } 52 | 53 | h5 { 54 | font-size: 1.25em; } 55 | 56 | h6 { 57 | font-size: 1em; } 58 | 59 | small, sub, sup { 60 | font-size: 75%; } 61 | 62 | hr { 63 | border-color: #2c8898; } 64 | 65 | a { 66 | margin: 5px; 67 | padding: 10px 20px; 68 | background-color: #faa039; 69 | color: white; 70 | border: none; 71 | font-weight: bold; 72 | border-radius: 7px; 73 | cursor: pointer; 74 | transition: background-color 0.3s ease; 75 | text-decoration: none; 76 | } 77 | a:hover { 78 | background-color: #f3780c; 79 | } 80 | 81 | ul { 82 | padding-left: 1.4em; } 83 | 84 | li { 85 | margin-bottom: 0.4em; } 86 | 87 | blockquote { 88 | font-style: italic; 89 | margin-left: 1.5em; 90 | padding-left: 1em; 91 | border-left: 3px solid #2c8898; } 92 | 93 | img { 94 | max-width: 100%; } 95 | 96 | /* Pre and Code */ 97 | pre { 98 | background-color: #f1f1f1; 99 | display: block; 100 | padding: 1em; 101 | overflow-x: auto; } 102 | 103 | code { 104 | font-size: 0.9em; 105 | padding: 0 0.5em; 106 | background-color: #f1f1f1; 107 | white-space: pre-wrap; } 108 | 109 | pre > code { 110 | padding: 0; 111 | background-color: transparent; 112 | white-space: pre; } 113 | 114 | /* Tables */ 115 | table { 116 | text-align: justify; 117 | width: 100%; 118 | border-collapse: collapse; } 119 | 120 | td, th { 121 | padding: 0.5em; 122 | border-bottom: 1px solid #f1f1f1; } 123 | 124 | /* Buttons, forms and input */ 125 | input, textarea { 126 | border: 1px solid #4a4a4a; } 127 | input:focus, textarea:focus { 128 | border: 1px solid #2c8898; } 129 | 130 | textarea { 131 | width: 100%; } 132 | 133 | .button, button, input[type="submit"], input[type="reset"], input[type="button"] { 134 | display: inline-block; 135 | padding: 5px 10px; 136 | text-align: center; 137 | text-decoration: none; 138 | white-space: nowrap; 139 | background-color: #2c8898; 140 | color: #f9f9f9; 141 | border-radius: 1px; 142 | border: 1px solid #2c8898; 143 | cursor: pointer; 144 | box-sizing: border-box; } 145 | .button[disabled], button[disabled], input[type="submit"][disabled], input[type="reset"][disabled], input[type="button"][disabled] { 146 | cursor: default; 147 | opacity: .5; } 148 | .button:focus, .button:hover, button:focus, button:hover, input[type="submit"]:focus, input[type="submit"]:hover, input[type="reset"]:focus, input[type="reset"]:hover, input[type="button"]:focus, input[type="button"]:hover { 149 | background-color: #982c61; 150 | border-color: #982c61; 151 | color: #f9f9f9; 152 | outline: 0; } 153 | 154 | textarea, select, input[type] { 155 | color: #4a4a4a; 156 | padding: 6px 10px; 157 | /* The 6px vertically centers text on FF, ignored by Webkit */ 158 | margin-bottom: 10px; 159 | background-color: #f1f1f1; 160 | border: 1px solid #f1f1f1; 161 | border-radius: 4px; 162 | box-shadow: none; 163 | box-sizing: border-box; } 164 | textarea:focus, select:focus, input[type]:focus { 165 | border: 1px solid #2c8898; 166 | outline: 0; } 167 | 168 | label, legend, fieldset { 169 | display: block; 170 | margin-bottom: .5rem; 171 | font-weight: 600; } 172 | 173 | #mainContainer { 174 | display: flex; 175 | align-items: center; 176 | justify-content: center; 177 | flex-direction: column; 178 | margin: auto; 179 | position: absolute; 180 | top: 0; 181 | bottom: 0; 182 | left: 0; 183 | right: 0; 184 | font-size: 2em; /* Base font size */ 185 | padding: 20px; 186 | text-align: center; 187 | } 188 | 189 | 190 | /* Responsive styles for smaller screens */ 191 | @media (max-width: 768px) { 192 | #mainContainer { 193 | font-size: 1.5em; /* Adjust font size for smaller screens */ 194 | } 195 | } 196 | --------------------------------------------------------------------------------