tasmota-webserial-esptool 7.2.4 → 7.2.6

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/README.md CHANGED
@@ -1,21 +1,40 @@
1
- # WebSerial ESPTool
2
1
 
3
- JavaScript package to install and make backups of firmwares on ESP devices via the browser using WebSerial.
2
+ # 🚀 WebSerial ESPTool Flash. Backup. Enjoy.
4
3
 
5
- WebSerial ESPTool is **not** based on esptool.js
4
+ **The next-generation solution for ESP firmware – right in your browser!**
6
5
 
6
+ WebSerial ESPTool brings you hassle-free firmware flashing and backup for your ESP devices, directly from your browser. No drivers, no command line, no headaches. Just plug in your ESP, open your browser, and experience seamless, lightning-fast firmware management.
7
7
 
8
- ## Local development
8
+ ---
9
9
 
10
- - Clone this repository.
11
- - Install dependencies with `npm install`
12
- - Run `script/develop`
13
- - Open http://localhost:5004/
10
+ **Why choose WebSerial ESPTool?**
14
11
 
15
- ## Origin
12
+ - **Plug & Play:** Install and back up firmware straight from your browser – no software installation required!
13
+ - **Cutting-Edge Compatibility:** Supports the latest ESP MCUs and chip variants (including P4 revisions).
14
+ - **Blazing Fast & Reliable:** Optimized flashing and reading for maximum performance.
15
+ - **Truly Independent:** Not a clone of esptool.js – 100% original, innovative codebase!
16
16
 
17
- This project was originally written by [Melissa LeBlanc-Williams](https://github.com/makermelissa). [Nabu Casa](https://www.nabucasa.com) ported the code over to TypeScript and in March 2022 took over maintenance from Adafruit. In July 2022, the Nabucasa stopped maintaining the project in favor of an official, but very early release of Espressif's [esptool-js](https://github.com/espressif/esptool-js/). Due to the instability of the tool, Adafruit updated their fork with Nabucasa's changes in November 2022 and took over maintenance once again. In December 2024, the tool was once again updated to use Espressif's esptool-js as the backend.
18
- Since Adafruit is slow in adding new MCUs (support for C2, C6 and H2 is from this fork) and uses now esptool.js which is still buggy, i decided to maintain my own version and do not provide PRs upstream anymore.
19
- In 12/2025 support for new MCUs and chip variant support for the different P4 revisions and flash read was added.
17
+ > **Did you know?**
18
+ > - The [ESP32 Swiss Army Knife](https://github.com/Jason2866/esp32tool) is based on this project and available [online](https://jason2866.github.io/esp32tool/).
19
+ > - [ESPConnect](https://github.com/thelastoutpostworkshop/ESPConnect) also uses WebSerial ESPTool under the hood.
20
20
 
21
- Copyright: Adafruit, Nabu Casa and Johann Obermeier
21
+ ---
22
+
23
+ ## 🛠️ Quick Start for Developers
24
+
25
+ 1. Clone this repository
26
+ 2. Install dependencies: `npm install`
27
+ 3. Start the dev environment: `script/develop`
28
+ 4. Open [http://localhost:5004/](http://localhost:5004/) in your browser
29
+
30
+ ---
31
+
32
+ ## 🏆 The Story
33
+
34
+ Originally created by [Melissa LeBlanc-Williams](https://github.com/makermelissa), further developed by [Nabu Casa](https://www.nabucasa.com) and Adafruit, and now with new features.
35
+
36
+ **Latest update:** December 2025 – now with support for new MCUs, chip variants (P4), and ultra-fast flash reading!
37
+
38
+ ---
39
+
40
+ © Adafruit, Nabu Casa & Johann Obermeier
package/css/style.css CHANGED
@@ -275,7 +275,7 @@ div.clear {
275
275
  .onoffswitch {
276
276
  display: inline-block;
277
277
  position: relative;
278
- width: 50px;
278
+ width: 70px;
279
279
  margin-right: 5px;
280
280
  -webkit-user-select: none;
281
281
  -moz-user-select: none;
@@ -93,6 +93,22 @@ export declare class ESPLoader extends EventTarget {
93
93
  checksum(data: number[], state?: number): number;
94
94
  setBaudrate(baud: number): Promise<void>;
95
95
  reconfigurePort(baud: number): Promise<void>;
96
+ /**
97
+ * @name connectWithResetStrategies
98
+ * Try different reset strategies to enter bootloader mode
99
+ * Similar to esptool.py's connect() method with multiple reset strategies
100
+ */
101
+ connectWithResetStrategies(): Promise<void>;
102
+ /**
103
+ * @name hardResetUSBJTAGSerial
104
+ * USB-JTAG/Serial reset sequence for ESP32-C3, ESP32-S3, ESP32-C6, etc.
105
+ */
106
+ hardResetUSBJTAGSerial(): Promise<void>;
107
+ /**
108
+ * @name hardResetClassic
109
+ * Classic reset sequence for USB-to-Serial bridge chips (CH340, CP2102, etc.)
110
+ */
111
+ hardResetClassic(): Promise<void>;
96
112
  /**
97
113
  * @name sync
98
114
  * Put into ROM bootload mode & attempt to synchronize with the
@@ -189,13 +205,13 @@ export declare class ESPLoader extends EventTarget {
189
205
  *
190
206
  * @param bufferingTime - Time in milliseconds to wait for the buffer to fill
191
207
  */
192
- private drainInputBuffer;
208
+ drainInputBuffer(bufferingTime?: number): Promise<void>;
193
209
  /**
194
210
  * @name flushSerialBuffers
195
211
  * Flush any pending data in the TX and RX serial port buffers
196
212
  * This clears both the application RX buffer and waits for hardware buffers to drain
197
213
  */
198
- private flushSerialBuffers;
214
+ flushSerialBuffers(): Promise<void>;
199
215
  /**
200
216
  * @name readFlash
201
217
  * Read flash memory from the chip (only works with stub loader)
@@ -86,7 +86,6 @@ export class ESPLoader extends EventTarget {
86
86
  };
87
87
  }
88
88
  async initialize() {
89
- await this.hardReset(true);
90
89
  if (!this._parent) {
91
90
  this.__inputBuffer = [];
92
91
  this.__totalBytesRead = 0;
@@ -107,9 +106,8 @@ export class ESPLoader extends EventTarget {
107
106
  // Don't await this promise so it doesn't block rest of method.
108
107
  this.readLoop();
109
108
  }
110
- // Clear buffer again after starting read loop
111
- await this.flushSerialBuffers();
112
- await this.sync();
109
+ // Try to connect with different reset strategies
110
+ await this.connectWithResetStrategies();
113
111
  // Detect chip type
114
112
  await this.detectChip();
115
113
  // Read the OTP data for this chip and store into this.efuses array
@@ -156,6 +154,9 @@ export class ESPLoader extends EventTarget {
156
154
  catch (error) {
157
155
  // GET_SECURITY_INFO not supported, fall back to magic value detection
158
156
  this.logger.debug(`GET_SECURITY_INFO failed, using magic value detection: ${error}`);
157
+ // Drain input buffer for CP210x compatibility on Windows
158
+ // This ensures all error responses are cleared before continuing
159
+ await this.drainInputBuffer(200);
159
160
  // Clear input buffer and re-sync to recover from failed command
160
161
  this._inputBuffer.length = 0;
161
162
  await sleep(SYNC_TIMEOUT);
@@ -298,35 +299,12 @@ export class ESPLoader extends EventTarget {
298
299
  if (bootloader) {
299
300
  // enter flash mode
300
301
  if (this.port.getInfo().usbProductId === USB_JTAG_SERIAL_PID) {
301
- // esp32c3 esp32s3 etc. build-in USB serial.
302
- // when connect to computer direct via usb, using following signals
303
- // to enter flash mode automatically.
304
- await this.setDTR(false);
305
- await this.setRTS(false);
306
- await this.sleep(100);
307
- await this.setDTR(true);
308
- await this.setRTS(false);
309
- await this.sleep(100);
310
- await this.setRTS(true);
311
- await this.setDTR(false);
312
- await this.setRTS(true);
313
- await this.sleep(100);
314
- await this.setDTR(false);
315
- await this.setRTS(false);
316
- this.logger.log("USB MCU reset.");
302
+ await this.hardResetUSBJTAGSerial();
303
+ this.logger.log("USB-JTAG/Serial reset.");
317
304
  }
318
305
  else {
319
- // otherwise, esp chip should be connected to computer via usb-serial
320
- // bridge chip like ch340,CP2102 etc.
321
- // use normal way to enter flash mode.
322
- await this.setDTR(false);
323
- await this.setRTS(true);
324
- await this.sleep(100);
325
- await this.setDTR(true);
326
- await this.setRTS(false);
327
- await this.sleep(50);
328
- await this.setDTR(false);
329
- this.logger.log("DTR/RTS USB serial chip reset.");
306
+ await this.hardResetClassic();
307
+ this.logger.log("Classic reset.");
330
308
  }
331
309
  }
332
310
  else {
@@ -670,6 +648,108 @@ export class ESPLoader extends EventTarget {
670
648
  throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
671
649
  }
672
650
  }
651
+ /**
652
+ * @name connectWithResetStrategies
653
+ * Try different reset strategies to enter bootloader mode
654
+ * Similar to esptool.py's connect() method with multiple reset strategies
655
+ */
656
+ async connectWithResetStrategies() {
657
+ var _a, _b;
658
+ const portInfo = this.port.getInfo();
659
+ const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
660
+ const isEspressifUSB = portInfo.usbVendorId === 0x303a;
661
+ this.logger.log(`Detected USB: VID=0x${((_a = portInfo.usbVendorId) === null || _a === void 0 ? void 0 : _a.toString(16)) || "unknown"}, PID=0x${((_b = portInfo.usbProductId) === null || _b === void 0 ? void 0 : _b.toString(16)) || "unknown"}`);
662
+ // Define reset strategies to try in order
663
+ const resetStrategies = [];
664
+ // Strategy 1: USB-JTAG/Serial reset (for ESP32-C3, C6, S3, etc.)
665
+ // Try this first if we detect Espressif USB VID or the specific PID
666
+ if (isUSBJTAGSerial || isEspressifUSB) {
667
+ resetStrategies.push({
668
+ name: "USB-JTAG/Serial",
669
+ fn: async () => await this.hardResetUSBJTAGSerial(),
670
+ });
671
+ }
672
+ // Strategy 2: Classic reset (for USB-to-Serial bridges)
673
+ resetStrategies.push({
674
+ name: "Classic",
675
+ fn: async () => await this.hardResetClassic(),
676
+ });
677
+ // Strategy 3: If USB-JTAG/Serial was not tried yet, try it as fallback
678
+ if (!isUSBJTAGSerial && !isEspressifUSB) {
679
+ resetStrategies.push({
680
+ name: "USB-JTAG/Serial (fallback)",
681
+ fn: async () => await this.hardResetUSBJTAGSerial(),
682
+ });
683
+ }
684
+ let lastError = null;
685
+ // Try each reset strategy
686
+ for (const strategy of resetStrategies) {
687
+ try {
688
+ this.logger.log(`Trying ${strategy.name} reset...`);
689
+ // Check if port is still open, if not, skip this strategy
690
+ if (!this.connected || !this.port.writable) {
691
+ this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
692
+ continue;
693
+ }
694
+ await strategy.fn();
695
+ // Try to sync after reset
696
+ await this.sync();
697
+ // If we get here, sync succeeded
698
+ this.logger.log(`Connected successfully with ${strategy.name} reset.`);
699
+ return;
700
+ }
701
+ catch (error) {
702
+ lastError = error;
703
+ this.logger.log(`${strategy.name} reset failed: ${error.message}`);
704
+ // If port got disconnected, we can't try more strategies
705
+ if (!this.connected || !this.port.writable) {
706
+ this.logger.log(`Port disconnected during reset attempt`);
707
+ break;
708
+ }
709
+ // Clear buffers before trying next strategy
710
+ this._inputBuffer.length = 0;
711
+ await this.drainInputBuffer(200);
712
+ await this.flushSerialBuffers();
713
+ }
714
+ }
715
+ // All strategies failed
716
+ throw new Error(`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
717
+ }
718
+ /**
719
+ * @name hardResetUSBJTAGSerial
720
+ * USB-JTAG/Serial reset sequence for ESP32-C3, ESP32-S3, ESP32-C6, etc.
721
+ */
722
+ async hardResetUSBJTAGSerial() {
723
+ await this.setRTS(false);
724
+ await this.setDTR(false); // Idle
725
+ await this.sleep(100);
726
+ await this.setDTR(true); // Set IO0
727
+ await this.setRTS(false);
728
+ await this.sleep(100);
729
+ await this.setRTS(true); // Reset. Calls inverted to go through (1,1) instead of (0,0)
730
+ await this.setDTR(false);
731
+ await this.setRTS(true); // RTS set as Windows only propagates DTR on RTS setting
732
+ await this.sleep(100);
733
+ await this.setDTR(false);
734
+ await this.setRTS(false); // Chip out of reset
735
+ // Wait for chip to boot into bootloader
736
+ await this.sleep(200);
737
+ }
738
+ /**
739
+ * @name hardResetClassic
740
+ * Classic reset sequence for USB-to-Serial bridge chips (CH340, CP2102, etc.)
741
+ */
742
+ async hardResetClassic() {
743
+ await this.setDTR(false); // IO0=HIGH
744
+ await this.setRTS(true); // EN=LOW, chip in reset
745
+ await this.sleep(100);
746
+ await this.setDTR(true); // IO0=LOW
747
+ await this.setRTS(false); // EN=HIGH, chip out of reset
748
+ await this.sleep(50);
749
+ await this.setDTR(false); // IO0=HIGH, done
750
+ // Wait for chip to boot into bootloader
751
+ await this.sleep(200);
752
+ }
673
753
  /**
674
754
  * @name sync
675
755
  * Put into ROM bootload mode & attempt to synchronize with the
@@ -1328,9 +1408,10 @@ export class ESPLoader extends EventTarget {
1328
1408
  const chunkSize = Math.min(CHUNK_SIZE, remainingSize);
1329
1409
  let chunkSuccess = false;
1330
1410
  let retryCount = 0;
1331
- const MAX_RETRIES = 3;
1411
+ const MAX_RETRIES = 5;
1332
1412
  // Retry loop for this chunk
1333
1413
  while (!chunkSuccess && retryCount <= MAX_RETRIES) {
1414
+ let resp = new Uint8Array(0);
1334
1415
  try {
1335
1416
  this.logger.debug(`Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`);
1336
1417
  // Send read flash command for this chunk
@@ -1339,7 +1420,6 @@ export class ESPLoader extends EventTarget {
1339
1420
  if (res != 0) {
1340
1421
  throw new Error("Failed to read memory: " + res);
1341
1422
  }
1342
- let resp = new Uint8Array(0);
1343
1423
  while (resp.length < chunkSize) {
1344
1424
  // Read a SLIP packet
1345
1425
  let packet;
@@ -1349,6 +1429,20 @@ export class ESPLoader extends EventTarget {
1349
1429
  catch (err) {
1350
1430
  if (err instanceof SlipReadError) {
1351
1431
  this.logger.debug(`SLIP read error at ${resp.length} bytes: ${err.message}`);
1432
+ // Send final ACK for any data we did receive before the error
1433
+ if (resp.length > 0) {
1434
+ try {
1435
+ const ackData = pack("<I", resp.length);
1436
+ const slipEncodedAck = slipEncode(ackData);
1437
+ await this.writeToStream(slipEncodedAck);
1438
+ }
1439
+ catch (ackErr) {
1440
+ this.logger.debug(`ACK send error: ${ackErr}`);
1441
+ }
1442
+ }
1443
+ // Drain input buffer for CP210x compatibility on Windows
1444
+ // This clears any stale data that may be causing the error
1445
+ await this.drainInputBuffer(300);
1352
1446
  // If we've read all the data we need, break
1353
1447
  if (resp.length >= chunkSize) {
1354
1448
  break;
@@ -1378,17 +1472,20 @@ export class ESPLoader extends EventTarget {
1378
1472
  }
1379
1473
  catch (err) {
1380
1474
  retryCount++;
1381
- // Check if it's a timeout error
1382
- if (err instanceof SlipReadError &&
1383
- err.message.includes("Timed out")) {
1475
+ // Check if it's a timeout error or SLIP error
1476
+ if (err instanceof SlipReadError) {
1384
1477
  if (retryCount <= MAX_RETRIES) {
1385
- this.logger.log(`⚠️ Timeout error at 0x${currentAddr.toString(16)}. Reconnecting and retrying (attempt ${retryCount}/${MAX_RETRIES})...`);
1478
+ this.logger.log(`⚠️ ${err.message} at 0x${currentAddr.toString(16)}. Draining buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`);
1386
1479
  try {
1387
- await this.reconnect();
1388
- // Continue to retry the same chunk
1480
+ await this.drainInputBuffer(300);
1481
+ // Clear application buffer
1482
+ await this.flushSerialBuffers();
1483
+ // Wait before retry to let hardware settle
1484
+ await sleep(SYNC_TIMEOUT);
1485
+ // Continue to retry the same chunk (will send new read command)
1389
1486
  }
1390
- catch (reconnectErr) {
1391
- throw new Error(`Reconnect failed: ${reconnectErr}`);
1487
+ catch (drainErr) {
1488
+ this.logger.debug(`Buffer drain error: ${drainErr}`);
1392
1489
  }
1393
1490
  }
1394
1491
  else {
@@ -1396,7 +1493,7 @@ export class ESPLoader extends EventTarget {
1396
1493
  }
1397
1494
  }
1398
1495
  else {
1399
- // Non-timeout error, don't retry
1496
+ // Non-SLIP error, don't retry
1400
1497
  throw err;
1401
1498
  }
1402
1499
  }