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/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 ("serial" in navigator) {
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.classList.add("hidden");
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
- return trace;
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
- * @name clickConnect
227
- * Click handler for the connect/disconnect button.
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
- await espStub.port.close();
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
- const esploader = await esploaderMod.connect({
241
- log: (...args) => logMsg(...args),
242
- debug: (...args) => debugMsg(...args),
243
- error: (...args) => errorMsg(...args),
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
- // Handle ESP32-S2 Native USB reconnection requirement - must be set on esploader, not espStub
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
- await esploader.port.close();
353
+ // Close the port first
354
+ await esploader.port?.close();
262
355
 
263
- if (esploader.port.forget) {
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
- 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);
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
- // 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);
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
  /**