├── .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 | Vendor ID (hex):
32 |
33 | Product ID (hex):
34 |
35 |
36 |
37 | Plug in your NumWorks calculator and press its reset button, then click here:
38 | Connect to NumWorks calculator
39 |
40 |
41 | Your device has multiple DFU interfaces. Select one from the list below:
42 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Runtime mode
52 | Detach DFU
53 |
54 |
55 |
84 |
85 |
86 |
87 |
88 | WebUSB is currently only supported by Chromium / Google Chrome.
89 | 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.
90 |
91 |
92 | On Windows, that means that an appropriate WinUSB/libusb driver must first be installed. This can be done manually with programs such as Zadig .
93 | On Linux, that means that the current user must have permission to access the device.
94 | On macOS and ChromeBooks, it should work directly.
95 |
96 |
97 |
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 | Vendor ID (hex):
32 |
33 | Product ID (hex):
34 |
35 |
36 |
37 | Plug in your NumWorks calculator, then click here:
38 | Connect to NumWorks calculator
39 |
40 |
41 | Your device has multiple DFU interfaces. Select one from the list below:
42 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Runtime mode
52 | Detach DFU
53 |
54 |
55 |
97 |
98 |
99 |
100 |
101 | WebUSB is currently only supported by Chromium / Google Chrome.
102 | 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.
103 |
104 |
105 | On Windows, that means that an appropriate WinUSB/libusb driver must first be installed. This can be done manually with programs such as Zadig .
106 | On Linux, that means that the current user must have permission to access the device.
107 | On macOS and ChromeBooks, it should work directly.
108 |
109 |
110 |
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 |
--------------------------------------------------------------------------------