tasmota-webserial-esptool 7.3.3 → 9.0.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 +1015 -186
- package/dist/index.d.ts +1 -0
- package/dist/index.js +31 -2
- 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 +198 -85
- package/js/webusb-serial.js +1028 -0
- package/package.json +2 -2
- package/src/esp_loader.ts +1184 -216
- package/src/index.ts +38 -2
- package/src/stubs/index.ts +4 -1
package/js/script.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
// Import WebUSB serial support for Android compatibility
|
|
2
|
+
import { WebUSBSerial, requestSerialPort } from './webusb-serial.js';
|
|
3
|
+
|
|
4
|
+
// Make requestSerialPort available globally for esptool.js
|
|
5
|
+
// Use defensive assignment to avoid accidental overwrites
|
|
6
|
+
if (!globalThis.requestSerialPort) {
|
|
7
|
+
globalThis.requestSerialPort = requestSerialPort;
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
let espStub;
|
|
2
11
|
let esp32s2ReconnectInProgress = false;
|
|
12
|
+
let isConnected = false; // Track connection state
|
|
3
13
|
|
|
4
14
|
const baudRates = [2000000, 1500000, 921600, 500000, 460800, 230400, 153600, 128000, 115200];
|
|
5
15
|
const bufferSize = 512;
|
|
@@ -95,9 +105,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
95
105
|
}
|
|
96
106
|
});
|
|
97
107
|
|
|
98
|
-
if
|
|
108
|
+
// Check if Web Serial API or WebUSB is supported
|
|
109
|
+
if ("serial" in navigator || "usb" in navigator) {
|
|
99
110
|
const notSupported = document.getElementById("notSupported");
|
|
100
|
-
notSupported
|
|
111
|
+
notSupported?.classList.add("hidden");
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
initBaudRate();
|
|
@@ -129,35 +140,22 @@ function logMsg(text) {
|
|
|
129
140
|
}
|
|
130
141
|
}
|
|
131
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Append one or more debug-formatted values to the application log when debug mode is enabled.
|
|
145
|
+
*
|
|
146
|
+
* Formats primitive values (strings, numbers, booleans), `null`, and `undefined` as readable text;
|
|
147
|
+
* formats Array and `Uint8Array` elements as hex bytes (e.g., `0x1a`) inside brackets; logs other
|
|
148
|
+
* object types to the browser console and records a message indicating an unhandled type.
|
|
149
|
+
*
|
|
150
|
+
* @param {...any} args - Values to format and append to the debug log. The first argument is written
|
|
151
|
+
* without a leading prefix; subsequent arguments are appended without additional prefixes.
|
|
152
|
+
*/
|
|
132
153
|
function debugMsg(...args) {
|
|
133
154
|
if (!debugMode.checked) {
|
|
134
155
|
return;
|
|
135
156
|
}
|
|
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
157
|
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
let stack = getStackTrace();
|
|
157
|
-
stack.shift();
|
|
158
|
-
let top = stack.shift();
|
|
159
|
-
let prefix =
|
|
160
|
-
'<span class="debug-function">[' + top.func + ":" + top.pos + "]</span> ";
|
|
158
|
+
let prefix = "";
|
|
161
159
|
for (let arg of args) {
|
|
162
160
|
if (arg === undefined) {
|
|
163
161
|
logMsg(prefix + "undefined");
|
|
@@ -216,6 +214,11 @@ function enableStyleSheet(node, enabled) {
|
|
|
216
214
|
node.disabled = !enabled;
|
|
217
215
|
}
|
|
218
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Format a MAC address byte array as colon-separated uppercase hexadecimal octets.
|
|
219
|
+
* @param {Array<number>|Uint8Array} macAddr - Array of bytes representing the MAC address (each 0–255).
|
|
220
|
+
* @returns {string} Colon-separated uppercase hex octets, e.g. "AA:BB:CC:DD:EE:FF".
|
|
221
|
+
*/
|
|
219
222
|
function formatMacAddr(macAddr) {
|
|
220
223
|
return macAddr
|
|
221
224
|
.map((value) => value.toString(16).toUpperCase().padStart(2, "0"))
|
|
@@ -223,27 +226,116 @@ function formatMacAddr(macAddr) {
|
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
229
|
+
* Format a byte value as a two-digit hexadecimal string prefixed with `0x`.
|
|
230
|
+
* @param {number} value - Numeric value to format (treated as a byte).
|
|
231
|
+
* @returns {string} Hex string in the form `0xNN` with lowercase letters and at least two digits.
|
|
232
|
+
*/
|
|
233
|
+
function toHex(value) {
|
|
234
|
+
return "0x" + value.toString(16).padStart(2, "0");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Parse flash size string (e.g., "256KB", "4MB") to bytes
|
|
239
|
+
* @param {string} sizeStr - Flash size string with unit (KB or MB)
|
|
240
|
+
* @returns {number} Size in bytes
|
|
241
|
+
*/
|
|
242
|
+
function parseFlashSize(sizeStr) {
|
|
243
|
+
if (!sizeStr || typeof sizeStr !== 'string') {
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Extract number and unit
|
|
248
|
+
const match = sizeStr.match(/^(\d+)(KB|MB)$/i);
|
|
249
|
+
if (!match) {
|
|
250
|
+
// If no unit, assume it's already in MB (legacy behavior)
|
|
251
|
+
const num = parseInt(sizeStr);
|
|
252
|
+
return isNaN(num) ? 0 : num * 1024 * 1024;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const value = parseInt(match[1]);
|
|
256
|
+
const unit = match[2].toUpperCase();
|
|
257
|
+
|
|
258
|
+
if (unit === 'KB') {
|
|
259
|
+
return value * 1024; // KB to bytes
|
|
260
|
+
} else if (unit === 'MB') {
|
|
261
|
+
return value * 1024 * 1024; // MB to bytes
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Toggle the connection state: connect to an ESP device (using WebUSB on Android or Web Serial on desktop) or disconnect if already connected.
|
|
269
|
+
*
|
|
270
|
+
* 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
271
|
*/
|
|
229
272
|
async function clickConnect() {
|
|
273
|
+
console.log('[clickConnect] Function called');
|
|
274
|
+
|
|
230
275
|
if (espStub) {
|
|
276
|
+
console.log('[clickConnect] Already connected, disconnecting...');
|
|
277
|
+
// Remove disconnect event listener to prevent it from firing during manual disconnect
|
|
278
|
+
if (espStub.handleDisconnect) {
|
|
279
|
+
espStub.removeEventListener("disconnect", espStub.handleDisconnect);
|
|
280
|
+
}
|
|
281
|
+
|
|
231
282
|
await espStub.disconnect();
|
|
232
|
-
|
|
283
|
+
try {
|
|
284
|
+
await espStub.port?.close?.();
|
|
285
|
+
} catch (e) {
|
|
286
|
+
// ignore double-close
|
|
287
|
+
}
|
|
233
288
|
toggleUIConnected(false);
|
|
234
289
|
espStub = undefined;
|
|
290
|
+
|
|
235
291
|
return;
|
|
236
292
|
}
|
|
237
293
|
|
|
294
|
+
console.log('[clickConnect] Getting esploaderMod...');
|
|
238
295
|
const esploaderMod = await window.esptoolPackage;
|
|
239
296
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
297
|
+
// Platform detection: Android always uses WebUSB, Desktop uses Web Serial
|
|
298
|
+
const userAgent = navigator.userAgent || '';
|
|
299
|
+
const isAndroid = /Android/i.test(userAgent);
|
|
300
|
+
|
|
301
|
+
// Only log platform details to UI in debug mode (avoid fingerprinting surface)
|
|
302
|
+
if (debugMode.checked) {
|
|
303
|
+
const platformMsg = `Platform: ${isAndroid ? 'Android' : 'Desktop'} (UA: ${userAgent.substring(0, 50)}...)`;
|
|
304
|
+
logMsg(platformMsg);
|
|
305
|
+
}
|
|
306
|
+
logMsg(`Using: ${isAndroid ? 'WebUSB' : 'Web Serial'}`);
|
|
307
|
+
|
|
308
|
+
let esploader;
|
|
245
309
|
|
|
246
|
-
|
|
310
|
+
if (isAndroid) {
|
|
311
|
+
// Android: Use WebUSB directly
|
|
312
|
+
console.log('[Connect] Using WebUSB for Android');
|
|
313
|
+
try {
|
|
314
|
+
const port = await WebUSBSerial.requestPort((...args) => logMsg(...args));
|
|
315
|
+
esploader = await esploaderMod.connectWithPort(port, {
|
|
316
|
+
log: (...args) => logMsg(...args),
|
|
317
|
+
debug: (...args) => debugMsg(...args),
|
|
318
|
+
error: (...args) => errorMsg(...args),
|
|
319
|
+
});
|
|
320
|
+
} catch (err) {
|
|
321
|
+
logMsg(`WebUSB connection failed: ${err.message || err}`);
|
|
322
|
+
throw err;
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
// Desktop: Use Web Serial (standard esptool connect)
|
|
326
|
+
console.log('[Connect] Using Web Serial for Desktop');
|
|
327
|
+
esploader = await esploaderMod.connect({
|
|
328
|
+
log: (...args) => logMsg(...args),
|
|
329
|
+
debug: (...args) => debugMsg(...args),
|
|
330
|
+
error: (...args) => errorMsg(...args),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Store port info for ESP32-S2 detection
|
|
335
|
+
let portInfo = esploader.port?.getInfo ? esploader.port.getInfo() : {};
|
|
336
|
+
let isESP32S2 = portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x0002;
|
|
337
|
+
|
|
338
|
+
// Handle ESP32-S2 Native USB reconnection requirement for BROWSER
|
|
247
339
|
// Only add listener if not already in reconnect mode
|
|
248
340
|
if (!esp32s2ReconnectInProgress) {
|
|
249
341
|
esploader.addEventListener("esp32s2-usb-reconnect", async () => {
|
|
@@ -258,75 +350,68 @@ async function clickConnect() {
|
|
|
258
350
|
espStub = undefined;
|
|
259
351
|
|
|
260
352
|
try {
|
|
261
|
-
|
|
353
|
+
// Close the port first
|
|
354
|
+
await esploader.port?.close();
|
|
262
355
|
|
|
263
|
-
|
|
356
|
+
// For Android WebUSB: ESP32-S2 automatic reconnection doesn't work
|
|
357
|
+
// Show message and let user reconnect manually with BOOT button
|
|
358
|
+
if (isAndroid) {
|
|
359
|
+
logMsg("ESP32-S2 has switched to CDC mode");
|
|
360
|
+
logMsg("Please press and HOLD the BOOT button on your ESP32-S2, then click Connect");
|
|
361
|
+
toggleUIConnected(false);
|
|
362
|
+
esp32s2ReconnectInProgress = false;
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// For Desktop Web Serial: Use the modal dialog approach
|
|
366
|
+
if (esploader.port?.forget) {
|
|
264
367
|
await esploader.port.forget();
|
|
265
368
|
}
|
|
266
369
|
} catch (disconnectErr) {
|
|
267
370
|
// Ignore disconnect errors
|
|
371
|
+
console.warn("Error during disconnect:", disconnectErr);
|
|
268
372
|
}
|
|
269
373
|
|
|
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);
|
|
374
|
+
// Show modal dialog ONLY for Desktop
|
|
375
|
+
if (!isAndroid) {
|
|
376
|
+
const modal = document.getElementById("esp32s2Modal");
|
|
377
|
+
const reconnectBtn = document.getElementById("butReconnectS2");
|
|
280
378
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
379
|
+
modal.classList.remove("hidden");
|
|
380
|
+
|
|
381
|
+
// Handle reconnect button click
|
|
382
|
+
const handleReconnect = async () => {
|
|
383
|
+
modal.classList.add("hidden");
|
|
384
|
+
reconnectBtn.removeEventListener("click", handleReconnect);
|
|
385
|
+
|
|
386
|
+
logMsg("Requesting new device selection...");
|
|
387
|
+
|
|
388
|
+
// Trigger port selection
|
|
389
|
+
try {
|
|
390
|
+
await clickConnect();
|
|
391
|
+
// Reset flag on successful connection
|
|
392
|
+
esp32s2ReconnectInProgress = false;
|
|
393
|
+
} catch (err) {
|
|
394
|
+
errorMsg("Failed to reconnect: " + err);
|
|
395
|
+
// Reset flag on error so user can try again
|
|
396
|
+
esp32s2ReconnectInProgress = false;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
reconnectBtn.addEventListener("click", handleReconnect);
|
|
401
|
+
}
|
|
294
402
|
});
|
|
295
403
|
}
|
|
296
404
|
|
|
297
405
|
try {
|
|
298
406
|
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
407
|
} catch (err) {
|
|
324
|
-
// If ESP32-S2 reconnect is in progress, suppress the error
|
|
408
|
+
// If ESP32-S2 reconnect is in progress (handled by event listener), suppress the error
|
|
325
409
|
if (esp32s2ReconnectInProgress) {
|
|
326
410
|
logMsg("Initialization interrupted for ESP32-S2 reconnection.");
|
|
327
411
|
return;
|
|
328
412
|
}
|
|
329
413
|
|
|
414
|
+
// Not ESP32-S2 or other error
|
|
330
415
|
try {
|
|
331
416
|
await esploader.disconnect();
|
|
332
417
|
} catch (disconnectErr) {
|
|
@@ -334,6 +419,34 @@ async function clickConnect() {
|
|
|
334
419
|
}
|
|
335
420
|
throw err;
|
|
336
421
|
}
|
|
422
|
+
|
|
423
|
+
logMsg("Connected to " + esploader.chipName);
|
|
424
|
+
logMsg("MAC Address: " + formatMacAddr(esploader.macAddr()));
|
|
425
|
+
|
|
426
|
+
espStub = await esploader.runStub();
|
|
427
|
+
|
|
428
|
+
toggleUIConnected(true);
|
|
429
|
+
toggleUIToolbar(true);
|
|
430
|
+
|
|
431
|
+
// Set detected flash size in the read size field
|
|
432
|
+
if (espStub.flashSize) {
|
|
433
|
+
const flashSizeBytes = parseFlashSize(espStub.flashSize);
|
|
434
|
+
readSize.value = "0x" + flashSizeBytes.toString(16);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Set the selected baud rate
|
|
438
|
+
let baud = parseInt(baudRate.value);
|
|
439
|
+
if (baudRates.includes(baud)) {
|
|
440
|
+
await espStub.setBaudrate(baud);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Store disconnect handler so we can remove it later
|
|
444
|
+
const handleDisconnect = () => {
|
|
445
|
+
toggleUIConnected(false);
|
|
446
|
+
espStub = undefined;
|
|
447
|
+
};
|
|
448
|
+
espStub.handleDisconnect = handleDisconnect; // Store reference on espStub
|
|
449
|
+
espStub.addEventListener("disconnect", handleDisconnect);
|
|
337
450
|
}
|
|
338
451
|
|
|
339
452
|
/**
|