tasmota-webserial-esptool 7.3.4 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/css/dark.css +74 -0
- package/css/light.css +74 -0
- package/css/style.css +663 -35
- package/dist/esp_loader.d.ts +102 -14
- package/dist/esp_loader.js +1012 -188
- package/dist/index.d.ts +1 -0
- package/dist/index.js +33 -3
- package/dist/stubs/index.d.ts +1 -2
- package/dist/stubs/index.js +4 -0
- package/dist/web/index.js +1 -1
- package/js/modules/esptool.js +1 -1
- package/js/script.js +215 -85
- package/js/webusb-serial.js +1028 -0
- package/package.json +2 -2
- package/src/esp_loader.ts +1187 -220
- package/src/index.ts +42 -3
- package/src/stubs/index.ts +4 -1
package/js/script.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
// WebUSB serial support will be loaded dynamically when needed on Android
|
|
2
|
+
|
|
1
3
|
let espStub;
|
|
2
4
|
let esp32s2ReconnectInProgress = false;
|
|
5
|
+
let isConnected = false; // Track connection state
|
|
3
6
|
|
|
4
7
|
const baudRates = [2000000, 1500000, 921600, 500000, 460800, 230400, 153600, 128000, 115200];
|
|
5
8
|
const bufferSize = 512;
|
|
@@ -95,9 +98,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
95
98
|
}
|
|
96
99
|
});
|
|
97
100
|
|
|
98
|
-
if
|
|
101
|
+
// Check if Web Serial API or WebUSB is supported
|
|
102
|
+
if ("serial" in navigator || "usb" in navigator) {
|
|
99
103
|
const notSupported = document.getElementById("notSupported");
|
|
100
|
-
notSupported
|
|
104
|
+
notSupported?.classList.add("hidden");
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
initBaudRate();
|
|
@@ -129,35 +133,22 @@ function logMsg(text) {
|
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Append one or more debug-formatted values to the application log when debug mode is enabled.
|
|
138
|
+
*
|
|
139
|
+
* Formats primitive values (strings, numbers, booleans), `null`, and `undefined` as readable text;
|
|
140
|
+
* formats Array and `Uint8Array` elements as hex bytes (e.g., `0x1a`) inside brackets; logs other
|
|
141
|
+
* object types to the browser console and records a message indicating an unhandled type.
|
|
142
|
+
*
|
|
143
|
+
* @param {...any} args - Values to format and append to the debug log. The first argument is written
|
|
144
|
+
* without a leading prefix; subsequent arguments are appended without additional prefixes.
|
|
145
|
+
*/
|
|
132
146
|
function debugMsg(...args) {
|
|
133
147
|
if (!debugMode.checked) {
|
|
134
148
|
return;
|
|
135
149
|
}
|
|
136
|
-
|
|
137
|
-
function getStackTrace() {
|
|
138
|
-
let stack = new Error().stack;
|
|
139
|
-
//console.log(stack);
|
|
140
|
-
stack = stack.split("\n").map((v) => v.trim());
|
|
141
|
-
stack.shift();
|
|
142
|
-
stack.shift();
|
|
143
|
-
|
|
144
|
-
let trace = [];
|
|
145
|
-
for (let line of stack) {
|
|
146
|
-
line = line.replace("at ", "");
|
|
147
|
-
trace.push({
|
|
148
|
-
func: line.substr(0, line.indexOf("(") - 1),
|
|
149
|
-
pos: line.substring(line.indexOf(".js:") + 4, line.lastIndexOf(":")),
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return trace;
|
|
154
|
-
}
|
|
155
150
|
|
|
156
|
-
let
|
|
157
|
-
stack.shift();
|
|
158
|
-
let top = stack.shift();
|
|
159
|
-
let prefix =
|
|
160
|
-
'<span class="debug-function">[' + top.func + ":" + top.pos + "]</span> ";
|
|
151
|
+
let prefix = "";
|
|
161
152
|
for (let arg of args) {
|
|
162
153
|
if (arg === undefined) {
|
|
163
154
|
logMsg(prefix + "undefined");
|
|
@@ -216,6 +207,11 @@ function enableStyleSheet(node, enabled) {
|
|
|
216
207
|
node.disabled = !enabled;
|
|
217
208
|
}
|
|
218
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Format a MAC address byte array as colon-separated uppercase hexadecimal octets.
|
|
212
|
+
* @param {Array<number>|Uint8Array} macAddr - Array of bytes representing the MAC address (each 0–255).
|
|
213
|
+
* @returns {string} Colon-separated uppercase hex octets, e.g. "AA:BB:CC:DD:EE:FF".
|
|
214
|
+
*/
|
|
219
215
|
function formatMacAddr(macAddr) {
|
|
220
216
|
return macAddr
|
|
221
217
|
.map((value) => value.toString(16).toUpperCase().padStart(2, "0"))
|
|
@@ -223,27 +219,140 @@ function formatMacAddr(macAddr) {
|
|
|
223
219
|
}
|
|
224
220
|
|
|
225
221
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
222
|
+
* Format a byte value as a two-digit hexadecimal string prefixed with `0x`.
|
|
223
|
+
* @param {number} value - Numeric value to format (treated as a byte).
|
|
224
|
+
* @returns {string} Hex string in the form `0xNN` with lowercase letters and at least two digits.
|
|
225
|
+
*/
|
|
226
|
+
function toHex(value) {
|
|
227
|
+
return "0x" + value.toString(16).padStart(2, "0");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Parse flash size string (e.g., "256KB", "4MB") to bytes
|
|
232
|
+
* @param {string} sizeStr - Flash size string with unit (KB or MB)
|
|
233
|
+
* @returns {number} Size in bytes
|
|
234
|
+
*/
|
|
235
|
+
function parseFlashSize(sizeStr) {
|
|
236
|
+
if (!sizeStr || typeof sizeStr !== 'string') {
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Extract number and unit
|
|
241
|
+
const match = sizeStr.match(/^(\d+)(KB|MB)$/i);
|
|
242
|
+
if (!match) {
|
|
243
|
+
// If no unit, assume it's already in MB (legacy behavior)
|
|
244
|
+
const num = parseInt(sizeStr);
|
|
245
|
+
return isNaN(num) ? 0 : num * 1024 * 1024;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const value = parseInt(match[1]);
|
|
249
|
+
const unit = match[2].toUpperCase();
|
|
250
|
+
|
|
251
|
+
if (unit === 'KB') {
|
|
252
|
+
return value * 1024; // KB to bytes
|
|
253
|
+
} else if (unit === 'MB') {
|
|
254
|
+
return value * 1024 * 1024; // MB to bytes
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Load WebUSB serial wrapper for Android
|
|
262
|
+
*/
|
|
263
|
+
async function loadWebUSBSerial() {
|
|
264
|
+
// Check if already loaded
|
|
265
|
+
if (globalThis.requestSerialPort) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Dynamically load the WebUSB serial script
|
|
270
|
+
return new Promise((resolve, reject) => {
|
|
271
|
+
const script = document.createElement("script");
|
|
272
|
+
script.src = "js/webusb-serial.js";
|
|
273
|
+
script.onload = () => {
|
|
274
|
+
// Verify it loaded correctly
|
|
275
|
+
if (globalThis.requestSerialPort) {
|
|
276
|
+
resolve();
|
|
277
|
+
} else {
|
|
278
|
+
reject(new Error("WebUSB serial script loaded but requestSerialPort not found"));
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
script.onerror = () => reject(new Error("Failed to load WebUSB serial script"));
|
|
282
|
+
document.head.appendChild(script);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Toggle the connection state: connect to an ESP device (using WebUSB on Android or Web Serial on desktop) or disconnect if already connected.
|
|
288
|
+
*
|
|
289
|
+
* On connect, detect platform and transport, initialize the esploader, handle ESP32-S2 native USB reconnection flow when required (showing a modal on desktop or guidance on Android), run the device stub, update UI state, set the detected flash size and selected baud rate, and install a disconnect handler. On disconnect, remove the handler, close the port, clear the stub, and update the UI.
|
|
228
290
|
*/
|
|
229
291
|
async function clickConnect() {
|
|
292
|
+
console.log('[clickConnect] Function called');
|
|
293
|
+
|
|
230
294
|
if (espStub) {
|
|
295
|
+
console.log('[clickConnect] Already connected, disconnecting...');
|
|
296
|
+
// Remove disconnect event listener to prevent it from firing during manual disconnect
|
|
297
|
+
if (espStub.handleDisconnect) {
|
|
298
|
+
espStub.removeEventListener("disconnect", espStub.handleDisconnect);
|
|
299
|
+
}
|
|
300
|
+
|
|
231
301
|
await espStub.disconnect();
|
|
232
|
-
|
|
302
|
+
try {
|
|
303
|
+
await espStub.port?.close?.();
|
|
304
|
+
} catch (e) {
|
|
305
|
+
// ignore double-close
|
|
306
|
+
}
|
|
233
307
|
toggleUIConnected(false);
|
|
234
308
|
espStub = undefined;
|
|
309
|
+
|
|
235
310
|
return;
|
|
236
311
|
}
|
|
237
312
|
|
|
313
|
+
console.log('[clickConnect] Getting esploaderMod...');
|
|
238
314
|
const esploaderMod = await window.esptoolPackage;
|
|
239
315
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
316
|
+
// Platform detection: Android always uses WebUSB, Desktop uses Web Serial
|
|
317
|
+
const userAgent = navigator.userAgent || '';
|
|
318
|
+
isAndroidPlatform = /Android/i.test(userAgent);
|
|
319
|
+
|
|
320
|
+
// Load WebUSB support for Android
|
|
321
|
+
if (isAndroidPlatform && "usb" in navigator) {
|
|
322
|
+
try {
|
|
323
|
+
await loadWebUSBSerial();
|
|
324
|
+
logMsg("WebUSB support loaded");
|
|
325
|
+
} catch (err) {
|
|
326
|
+
errorMsg(`Failed to load WebUSB support: ${err.message}`);
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Only log platform details to UI in debug mode (avoid fingerprinting surface)
|
|
332
|
+
if (debugMode.checked) {
|
|
333
|
+
const platformMsg = `Platform: ${isAndroidPlatform ? 'Android' : 'Desktop'} (UA: ${userAgent.substring(0, 50)}...)`;
|
|
334
|
+
logMsg(platformMsg);
|
|
335
|
+
}
|
|
336
|
+
logMsg(`Using: ${isAndroidPlatform ? 'WebUSB' : 'Web Serial'}`);
|
|
337
|
+
|
|
338
|
+
// Use esploaderMod.connect() which will automatically use globalThis.requestSerialPort if available
|
|
339
|
+
let esploader;
|
|
340
|
+
try {
|
|
341
|
+
esploader = await esploaderMod.connect({
|
|
342
|
+
log: (...args) => logMsg(...args),
|
|
343
|
+
debug: (...args) => debugMsg(...args),
|
|
344
|
+
error: (...args) => errorMsg(...args),
|
|
345
|
+
});
|
|
346
|
+
} catch (err) {
|
|
347
|
+
logMsg(`Connection failed: ${err.message || err}`);
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
245
350
|
|
|
246
|
-
//
|
|
351
|
+
// Store port info for ESP32-S2 detection
|
|
352
|
+
let portInfo = esploader.port?.getInfo ? esploader.port.getInfo() : {};
|
|
353
|
+
let isESP32S2 = portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x0002;
|
|
354
|
+
|
|
355
|
+
// Handle ESP32-S2 Native USB reconnection requirement for BROWSER
|
|
247
356
|
// Only add listener if not already in reconnect mode
|
|
248
357
|
if (!esp32s2ReconnectInProgress) {
|
|
249
358
|
esploader.addEventListener("esp32s2-usb-reconnect", async () => {
|
|
@@ -258,75 +367,68 @@ async function clickConnect() {
|
|
|
258
367
|
espStub = undefined;
|
|
259
368
|
|
|
260
369
|
try {
|
|
261
|
-
|
|
370
|
+
// Close the port first
|
|
371
|
+
await esploader.port?.close();
|
|
262
372
|
|
|
263
|
-
|
|
373
|
+
// For Android WebUSB: ESP32-S2 automatic reconnection doesn't work
|
|
374
|
+
// Show message and let user reconnect manually with BOOT button
|
|
375
|
+
if (isAndroidPlatform) {
|
|
376
|
+
logMsg("ESP32-S2 has switched to CDC mode");
|
|
377
|
+
logMsg("Please press and HOLD the BOOT button on your ESP32-S2, then click Connect");
|
|
378
|
+
toggleUIConnected(false);
|
|
379
|
+
esp32s2ReconnectInProgress = false;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
// For Desktop Web Serial: Use the modal dialog approach
|
|
383
|
+
if (esploader.port?.forget) {
|
|
264
384
|
await esploader.port.forget();
|
|
265
385
|
}
|
|
266
386
|
} catch (disconnectErr) {
|
|
267
387
|
// Ignore disconnect errors
|
|
388
|
+
console.warn("Error during disconnect:", disconnectErr);
|
|
268
389
|
}
|
|
269
390
|
|
|
270
|
-
// Show modal dialog
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
modal.classList.remove("hidden");
|
|
275
|
-
|
|
276
|
-
// Handle reconnect button click
|
|
277
|
-
const handleReconnect = async () => {
|
|
278
|
-
modal.classList.add("hidden");
|
|
279
|
-
reconnectBtn.removeEventListener("click", handleReconnect);
|
|
391
|
+
// Show modal dialog ONLY for Desktop
|
|
392
|
+
if (!isAndroidPlatform) {
|
|
393
|
+
const modal = document.getElementById("esp32s2Modal");
|
|
394
|
+
const reconnectBtn = document.getElementById("butReconnectS2");
|
|
280
395
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
396
|
+
modal.classList.remove("hidden");
|
|
397
|
+
|
|
398
|
+
// Handle reconnect button click
|
|
399
|
+
const handleReconnect = async () => {
|
|
400
|
+
modal.classList.add("hidden");
|
|
401
|
+
reconnectBtn.removeEventListener("click", handleReconnect);
|
|
402
|
+
|
|
403
|
+
logMsg("Requesting new device selection...");
|
|
404
|
+
|
|
405
|
+
// Trigger port selection
|
|
406
|
+
try {
|
|
407
|
+
await clickConnect();
|
|
408
|
+
// Reset flag on successful connection
|
|
409
|
+
esp32s2ReconnectInProgress = false;
|
|
410
|
+
} catch (err) {
|
|
411
|
+
errorMsg("Failed to reconnect: " + err);
|
|
412
|
+
// Reset flag on error so user can try again
|
|
413
|
+
esp32s2ReconnectInProgress = false;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
reconnectBtn.addEventListener("click", handleReconnect);
|
|
418
|
+
}
|
|
294
419
|
});
|
|
295
420
|
}
|
|
296
421
|
|
|
297
422
|
try {
|
|
298
423
|
await esploader.initialize();
|
|
299
|
-
|
|
300
|
-
logMsg("Connected to " + esploader.chipName);
|
|
301
|
-
logMsg("MAC Address: " + formatMacAddr(esploader.macAddr()));
|
|
302
|
-
|
|
303
|
-
espStub = await esploader.runStub();
|
|
304
|
-
toggleUIConnected(true);
|
|
305
|
-
toggleUIToolbar(true);
|
|
306
|
-
|
|
307
|
-
// Set detected flash size in the read size field
|
|
308
|
-
if (espStub.flashSize) {
|
|
309
|
-
const flashSizeBytes = parseInt(espStub.flashSize) * 1024 * 1024; // Convert MB to bytes
|
|
310
|
-
readSize.value = "0x" + flashSizeBytes.toString(16);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Set the selected baud rate
|
|
314
|
-
let baud = parseInt(baudRate.value);
|
|
315
|
-
if (baudRates.includes(baud)) {
|
|
316
|
-
await espStub.setBaudrate(baud);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
espStub.addEventListener("disconnect", () => {
|
|
320
|
-
toggleUIConnected(false);
|
|
321
|
-
espStub = false;
|
|
322
|
-
});
|
|
323
424
|
} catch (err) {
|
|
324
|
-
// If ESP32-S2 reconnect is in progress, suppress the error
|
|
425
|
+
// If ESP32-S2 reconnect is in progress (handled by event listener), suppress the error
|
|
325
426
|
if (esp32s2ReconnectInProgress) {
|
|
326
427
|
logMsg("Initialization interrupted for ESP32-S2 reconnection.");
|
|
327
428
|
return;
|
|
328
429
|
}
|
|
329
430
|
|
|
431
|
+
// Not ESP32-S2 or other error
|
|
330
432
|
try {
|
|
331
433
|
await esploader.disconnect();
|
|
332
434
|
} catch (disconnectErr) {
|
|
@@ -334,6 +436,34 @@ async function clickConnect() {
|
|
|
334
436
|
}
|
|
335
437
|
throw err;
|
|
336
438
|
}
|
|
439
|
+
|
|
440
|
+
logMsg("Connected to " + esploader.chipName);
|
|
441
|
+
logMsg("MAC Address: " + formatMacAddr(esploader.macAddr()));
|
|
442
|
+
|
|
443
|
+
espStub = await esploader.runStub();
|
|
444
|
+
|
|
445
|
+
toggleUIConnected(true);
|
|
446
|
+
toggleUIToolbar(true);
|
|
447
|
+
|
|
448
|
+
// Set detected flash size in the read size field
|
|
449
|
+
if (espStub.flashSize) {
|
|
450
|
+
const flashSizeBytes = parseFlashSize(espStub.flashSize);
|
|
451
|
+
readSize.value = "0x" + flashSizeBytes.toString(16);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Set the selected baud rate
|
|
455
|
+
let baud = parseInt(baudRate.value);
|
|
456
|
+
if (baudRates.includes(baud)) {
|
|
457
|
+
await espStub.setBaudrate(baud);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Store disconnect handler so we can remove it later
|
|
461
|
+
const handleDisconnect = () => {
|
|
462
|
+
toggleUIConnected(false);
|
|
463
|
+
espStub = undefined;
|
|
464
|
+
};
|
|
465
|
+
espStub.handleDisconnect = handleDisconnect; // Store reference on espStub
|
|
466
|
+
espStub.addEventListener("disconnect", handleDisconnect);
|
|
337
467
|
}
|
|
338
468
|
|
|
339
469
|
/**
|