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/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 ("serial" in navigator) {
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.classList.add("hidden");
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 stack = getStackTrace();
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
- * @name clickConnect
227
- * Click handler for the connect/disconnect button.
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
- await espStub.port.close();
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
- const esploader = await esploaderMod.connect({
241
- log: (...args) => logMsg(...args),
242
- debug: (...args) => debugMsg(...args),
243
- error: (...args) => errorMsg(...args),
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
- // Handle ESP32-S2 Native USB reconnection requirement - must be set on esploader, not espStub
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
- await esploader.port.close();
370
+ // Close the port first
371
+ await esploader.port?.close();
262
372
 
263
- if (esploader.port.forget) {
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
- const modal = document.getElementById("esp32s2Modal");
272
- const reconnectBtn = document.getElementById("butReconnectS2");
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
- // Trigger port selection
282
- try {
283
- await clickConnect();
284
- // Reset flag on successful connection
285
- esp32s2ReconnectInProgress = false;
286
- } catch (err) {
287
- errorMsg("Failed to reconnect: " + err);
288
- // Reset flag on error so user can try again
289
- esp32s2ReconnectInProgress = false;
290
- }
291
- };
292
-
293
- reconnectBtn.addEventListener("click", handleReconnect);
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
  /**