tasmota-webserial-esptool 6.5.3 → 7.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.
Files changed (101) hide show
  1. package/.vscode/settings.json +2 -0
  2. package/README.md +4 -3
  3. package/READ_FLASH_FEATURE.md +130 -0
  4. package/css/light.css +11 -0
  5. package/css/style.css +213 -45
  6. package/dist/const.d.ts +42 -3
  7. package/dist/const.js +102 -4
  8. package/dist/esp_loader.d.ts +27 -3
  9. package/dist/esp_loader.js +376 -17
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.js +1 -2
  12. package/dist/partition.d.ts +26 -0
  13. package/dist/partition.js +129 -0
  14. package/dist/stubs/esp32.json +4 -4
  15. package/dist/stubs/esp32c2.json +4 -4
  16. package/dist/stubs/esp32c3.json +4 -4
  17. package/dist/stubs/esp32c5.json +4 -4
  18. package/dist/stubs/esp32c6.json +4 -4
  19. package/dist/stubs/esp32c61.json +4 -4
  20. package/dist/stubs/esp32h2.json +4 -4
  21. package/dist/stubs/esp32p4.json +4 -4
  22. package/dist/stubs/esp32p4r3.json +4 -4
  23. package/dist/stubs/esp32s2.json +4 -4
  24. package/dist/stubs/esp32s3.json +4 -4
  25. package/dist/stubs/esp8266.json +3 -3
  26. package/dist/stubs/index.d.ts +1 -1
  27. package/dist/stubs/index.js +7 -1
  28. package/dist/web/esp32-CijhsJH1.js +1 -0
  29. package/dist/web/esp32c2-C17SM4gO.js +1 -0
  30. package/dist/web/esp32c3-DxRGijbg.js +1 -0
  31. package/dist/web/esp32c5-3mDOIGa4.js +1 -0
  32. package/dist/web/esp32c6-h6U0SQTm.js +1 -0
  33. package/dist/web/esp32c61-BKtexhPZ.js +1 -0
  34. package/dist/web/esp32h2-RtuWSEmP.js +1 -0
  35. package/dist/web/esp32p4-5nkIjxqJ.js +1 -0
  36. package/dist/web/esp32p4r3-CpHBYEwI.js +1 -0
  37. package/dist/web/esp32s2-IiDBtXxo.js +1 -0
  38. package/dist/web/esp32s3-6yv5yxum.js +1 -0
  39. package/dist/web/esp8266-CUwxJpGa.js +1 -0
  40. package/dist/web/index.js +1 -1
  41. package/index.html +158 -34
  42. package/js/modules/esp32-CijhsJH1.js +1 -0
  43. package/js/modules/esp32c2-C17SM4gO.js +1 -0
  44. package/js/modules/esp32c3-DxRGijbg.js +1 -0
  45. package/js/modules/esp32c5-3mDOIGa4.js +1 -0
  46. package/js/modules/esp32c6-h6U0SQTm.js +1 -0
  47. package/js/modules/esp32c61-BKtexhPZ.js +1 -0
  48. package/js/modules/esp32h2-RtuWSEmP.js +1 -0
  49. package/js/modules/esp32p4-5nkIjxqJ.js +1 -0
  50. package/js/modules/esp32p4r3-CpHBYEwI.js +1 -0
  51. package/js/modules/esp32s2-IiDBtXxo.js +1 -0
  52. package/js/modules/esp32s3-6yv5yxum.js +1 -0
  53. package/js/modules/esp8266-CUwxJpGa.js +1 -0
  54. package/js/modules/esptool.js +1 -1
  55. package/js/script.js +456 -11
  56. package/package.json +6 -6
  57. package/src/const.ts +109 -5
  58. package/src/esp_loader.ts +491 -18
  59. package/src/index.ts +3 -1
  60. package/src/partition.ts +155 -0
  61. package/src/stubs/README.md +1 -1
  62. package/src/stubs/esp32.json +4 -4
  63. package/src/stubs/esp32c2.json +4 -4
  64. package/src/stubs/esp32c3.json +4 -4
  65. package/src/stubs/esp32c5.json +4 -4
  66. package/src/stubs/esp32c6.json +4 -4
  67. package/src/stubs/esp32c61.json +4 -4
  68. package/src/stubs/esp32h2.json +4 -4
  69. package/src/stubs/esp32p4.json +4 -4
  70. package/src/stubs/esp32p4r3.json +4 -4
  71. package/src/stubs/esp32s2.json +4 -4
  72. package/src/stubs/esp32s3.json +4 -4
  73. package/src/stubs/esp8266.json +3 -3
  74. package/src/stubs/index.ts +14 -2
  75. package/BUGFIX_GET_SECURITY_INFO.md +0 -126
  76. package/IMPLEMENTATION_SUMMARY.md +0 -232
  77. package/SECURITY_INFO_EXPLANATION.md +0 -145
  78. package/dist/web/esp32-BNIFdu1P.js +0 -1
  79. package/dist/web/esp32c2-BqxquOKw.js +0 -1
  80. package/dist/web/esp32c3-BOOqe8me.js +0 -1
  81. package/dist/web/esp32c5-mcj52-K1.js +0 -1
  82. package/dist/web/esp32c6-Cg5qYgg7.js +0 -1
  83. package/dist/web/esp32c61-CzCdsydk.js +0 -1
  84. package/dist/web/esp32h2-DZa_lpff.js +0 -1
  85. package/dist/web/esp32p4-DyGqUAeZ.js +0 -1
  86. package/dist/web/esp32p4r3-Cle9QJmZ.js +0 -1
  87. package/dist/web/esp32s2-Bk4mqADi.js +0 -1
  88. package/dist/web/esp32s3-Df3OUCOC.js +0 -1
  89. package/dist/web/esp8266-CQFcqJ_a.js +0 -1
  90. package/js/modules/esp32-BNIFdu1P.js +0 -1
  91. package/js/modules/esp32c2-BqxquOKw.js +0 -1
  92. package/js/modules/esp32c3-BOOqe8me.js +0 -1
  93. package/js/modules/esp32c5-mcj52-K1.js +0 -1
  94. package/js/modules/esp32c6-Cg5qYgg7.js +0 -1
  95. package/js/modules/esp32c61-CzCdsydk.js +0 -1
  96. package/js/modules/esp32h2-DZa_lpff.js +0 -1
  97. package/js/modules/esp32p4-DyGqUAeZ.js +0 -1
  98. package/js/modules/esp32p4r3-Cle9QJmZ.js +0 -1
  99. package/js/modules/esp32s2-Bk4mqADi.js +0 -1
  100. package/js/modules/esp32s3-Df3OUCOC.js +0 -1
  101. package/js/modules/esp8266-CQFcqJ_a.js +0 -1
package/src/esp_loader.ts CHANGED
@@ -10,7 +10,10 @@ import {
10
10
  CHIP_FAMILY_ESP32C6,
11
11
  CHIP_FAMILY_ESP32C61,
12
12
  CHIP_FAMILY_ESP32H2,
13
+ CHIP_FAMILY_ESP32H4,
14
+ CHIP_FAMILY_ESP32H21,
13
15
  CHIP_FAMILY_ESP32P4,
16
+ CHIP_FAMILY_ESP32S31,
14
17
  CHIP_FAMILY_ESP8266,
15
18
  MAX_TIMEOUT,
16
19
  Logger,
@@ -39,7 +42,9 @@ import {
39
42
  USB_RAM_BLOCK,
40
43
  ChipFamily,
41
44
  ESP_ERASE_FLASH,
45
+ ESP_READ_FLASH,
42
46
  CHIP_ERASE_TIMEOUT,
47
+ FLASH_READ_TIMEOUT,
43
48
  timeoutPerMb,
44
49
  ESP_ROM_BAUD,
45
50
  USB_JTAG_SERIAL_PID,
@@ -74,6 +79,9 @@ export class ESPLoader extends EventTarget {
74
79
  flashSize: string | null = null;
75
80
 
76
81
  __inputBuffer?: number[];
82
+ __totalBytesRead?: number;
83
+ private _currentBaudRate: number = ESP_ROM_BAUD;
84
+ private _maxUSBSerialBaudrate?: number;
77
85
  private _reader?: ReadableStreamDefaultReader<Uint8Array>;
78
86
 
79
87
  constructor(
@@ -88,14 +96,96 @@ export class ESPLoader extends EventTarget {
88
96
  return this._parent ? this._parent._inputBuffer : this.__inputBuffer!;
89
97
  }
90
98
 
99
+ private get _totalBytesRead(): number {
100
+ return this._parent
101
+ ? this._parent._totalBytesRead
102
+ : this.__totalBytesRead || 0;
103
+ }
104
+
105
+ private set _totalBytesRead(value: number) {
106
+ if (this._parent) {
107
+ this._parent._totalBytesRead = value;
108
+ } else {
109
+ this.__totalBytesRead = value;
110
+ }
111
+ }
112
+
113
+ private detectUSBSerialChip(
114
+ vendorId: number,
115
+ productId: number,
116
+ ): { name: string; maxBaudrate?: number } {
117
+ // Common USB-Serial chip vendors and their products
118
+ const chips: Record<
119
+ number,
120
+ Record<number, { name: string; maxBaudrate?: number }>
121
+ > = {
122
+ 0x1a86: {
123
+ // QinHeng Electronics
124
+ 0x7523: { name: "CH340", maxBaudrate: 460800 },
125
+ 0x55d4: { name: "CH9102", maxBaudrate: 6000000 },
126
+ },
127
+ 0x10c4: {
128
+ // Silicon Labs
129
+ 0xea60: { name: "CP2102(n)", maxBaudrate: 3000000 },
130
+ 0xea70: { name: "CP2105", maxBaudrate: 2000000 },
131
+ 0xea71: { name: "CP2108", maxBaudrate: 2000000 },
132
+ },
133
+ 0x0403: {
134
+ // FTDI
135
+ 0x6001: { name: "FT232R", maxBaudrate: 3000000 },
136
+ 0x6010: { name: "FT2232", maxBaudrate: 3000000 },
137
+ 0x6011: { name: "FT4232", maxBaudrate: 3000000 },
138
+ 0x6014: { name: "FT232H", maxBaudrate: 12000000 },
139
+ 0x6015: { name: "FT230X", maxBaudrate: 3000000 },
140
+ },
141
+ 0x303a: {
142
+ // Espressif (native USB)
143
+ 0x1001: { name: "ESP32 Native USB", maxBaudrate: 2000000 },
144
+ 0x1002: { name: "ESP32 Native USB", maxBaudrate: 2000000 },
145
+ 0x4002: { name: "ESP32 Native USB", maxBaudrate: 2000000 },
146
+ 0x1000: { name: "ESP32 Native USB", maxBaudrate: 2000000 },
147
+ },
148
+ };
149
+
150
+ const vendor = chips[vendorId];
151
+ if (vendor && vendor[productId]) {
152
+ return vendor[productId];
153
+ }
154
+
155
+ return {
156
+ name: `Unknown (VID: 0x${vendorId.toString(16)}, PID: 0x${productId.toString(16)})`,
157
+ };
158
+ }
159
+
91
160
  async initialize() {
92
161
  await this.hardReset(true);
93
162
 
94
163
  if (!this._parent) {
95
164
  this.__inputBuffer = [];
165
+ this.__totalBytesRead = 0;
166
+
167
+ // Detect and log USB-Serial chip info
168
+ const portInfo = this.port.getInfo();
169
+ if (portInfo.usbVendorId && portInfo.usbProductId) {
170
+ const chipInfo = this.detectUSBSerialChip(
171
+ portInfo.usbVendorId,
172
+ portInfo.usbProductId,
173
+ );
174
+ this.logger.log(
175
+ `USB-Serial: ${chipInfo.name} (VID: 0x${portInfo.usbVendorId.toString(16)}, PID: 0x${portInfo.usbProductId.toString(16)})`,
176
+ );
177
+ if (chipInfo.maxBaudrate) {
178
+ this._maxUSBSerialBaudrate = chipInfo.maxBaudrate;
179
+ this.logger.log(`Max baudrate: ${chipInfo.maxBaudrate}`);
180
+ }
181
+ }
182
+
96
183
  // Don't await this promise so it doesn't block rest of method.
97
184
  this.readLoop();
98
185
  }
186
+
187
+ // Clear buffer again after starting read loop
188
+ await this.flushSerialBuffers();
99
189
  await this.sync();
100
190
 
101
191
  // Detect chip type
@@ -108,7 +198,9 @@ export class ESPLoader extends EventTarget {
108
198
  this._efuses[i] = await this.readRegister(AddrMAC + 4 * i);
109
199
  }
110
200
  this.logger.log(`Chip type ${this.chipName}`);
111
- //this.logger.log("FLASHID");
201
+ this.logger.debug(
202
+ `Bootloader flash offset: 0x${FlAddr.flashOffs.toString(16)}`,
203
+ );
112
204
  }
113
205
 
114
206
  /**
@@ -156,7 +248,7 @@ export class ESPLoader extends EventTarget {
156
248
 
157
249
  // Clear input buffer and re-sync to recover from failed command
158
250
  this._inputBuffer.length = 0;
159
- await sleep(100);
251
+ await sleep(SYNC_TIMEOUT);
160
252
 
161
253
  // Re-sync with the chip to ensure clean communication
162
254
  try {
@@ -293,7 +385,14 @@ export class ESPLoader extends EventTarget {
293
385
  if (!value || value.length === 0) {
294
386
  continue;
295
387
  }
296
- this._inputBuffer.push(...Array.from(value));
388
+
389
+ // Always read from browser's serial buffer immediately
390
+ // to prevent browser buffer overflow. Don't apply back-pressure here.
391
+ const chunk = Array.from(value);
392
+ Array.prototype.push.apply(this._inputBuffer, chunk);
393
+
394
+ // Track total bytes read from serial port
395
+ this._totalBytesRead += value.length;
297
396
  }
298
397
  } catch (err) {
299
398
  console.error("Read loop got disconnected");
@@ -324,7 +423,6 @@ export class ESPLoader extends EventTarget {
324
423
  }
325
424
 
326
425
  async hardReset(bootloader = false) {
327
- this.logger.log("Try hard reset.");
328
426
  if (bootloader) {
329
427
  // enter flash mode
330
428
  if (this.port.getInfo().usbProductId === USB_JTAG_SERIAL_PID) {
@@ -346,6 +444,7 @@ export class ESPLoader extends EventTarget {
346
444
  await this.sleep(100);
347
445
  await this.setDTR(false);
348
446
  await this.setRTS(false);
447
+ this.logger.log("USB MCU reset.");
349
448
  } else {
350
449
  // otherwise, esp chip should be connected to computer via usb-serial
351
450
  // bridge chip like ch340,CP2102 etc.
@@ -357,12 +456,14 @@ export class ESPLoader extends EventTarget {
357
456
  await this.setRTS(false);
358
457
  await this.sleep(50);
359
458
  await this.setDTR(false);
459
+ this.logger.log("DTR/RTS USB serial chip reset.");
360
460
  }
361
461
  } else {
362
462
  // just reset
363
463
  await this.setRTS(true); // EN->LOW
364
464
  await this.sleep(100);
365
465
  await this.setRTS(false);
466
+ this.logger.log("Hard reset.");
366
467
  }
367
468
  await new Promise((resolve) => setTimeout(resolve, 1000));
368
469
  }
@@ -411,7 +512,10 @@ export class ESPLoader extends EventTarget {
411
512
  this.chipFamily == CHIP_FAMILY_ESP32C6 ||
412
513
  this.chipFamily == CHIP_FAMILY_ESP32C61 ||
413
514
  this.chipFamily == CHIP_FAMILY_ESP32H2 ||
414
- this.chipFamily == CHIP_FAMILY_ESP32P4
515
+ this.chipFamily == CHIP_FAMILY_ESP32H4 ||
516
+ this.chipFamily == CHIP_FAMILY_ESP32H21 ||
517
+ this.chipFamily == CHIP_FAMILY_ESP32P4 ||
518
+ this.chipFamily == CHIP_FAMILY_ESP32S31
415
519
  ) {
416
520
  macAddr[0] = (mac1 >> 8) & 0xff;
417
521
  macAddr[1] = mac1 & 0xff;
@@ -470,7 +574,10 @@ export class ESPLoader extends EventTarget {
470
574
  CHIP_FAMILY_ESP32C6,
471
575
  CHIP_FAMILY_ESP32C61,
472
576
  CHIP_FAMILY_ESP32H2,
577
+ CHIP_FAMILY_ESP32H4,
578
+ CHIP_FAMILY_ESP32H21,
473
579
  CHIP_FAMILY_ESP32P4,
580
+ CHIP_FAMILY_ESP32S31,
474
581
  ].includes(this.chipFamily)
475
582
  ) {
476
583
  statusLen = 4;
@@ -528,8 +635,6 @@ export class ESPLoader extends EventTarget {
528
635
  * @name readPacket
529
636
  * Generator to read SLIP packets from a serial port.
530
637
  * Yields one full SLIP packet at a time, raises exception on timeout or invalid data.
531
- * Designed to avoid too many calls to serial.read(1), which can bog
532
- * down on slow systems.
533
638
  */
534
639
 
535
640
  async readPacket(timeout: number): Promise<number[]> {
@@ -544,7 +649,8 @@ export class ESPLoader extends EventTarget {
544
649
  readBytes.push(this._inputBuffer.shift()!);
545
650
  break;
546
651
  } else {
547
- await sleep(10);
652
+ // Reduced sleep time for faster response during high-speed transfers
653
+ await sleep(1);
548
654
  }
549
655
  }
550
656
  if (readBytes.length == 0) {
@@ -663,8 +769,6 @@ export class ESPLoader extends EventTarget {
663
769
  throw new Error("Changing baud rate is not supported on the ESP8266");
664
770
  }
665
771
 
666
- this.logger.log("Attempting to change baud rate to " + baud + "...");
667
-
668
772
  try {
669
773
  // Send ESP_ROM_BAUD(115200) as the old one if running STUB otherwise 0
670
774
  let buffer = pack("<II", baud, this.IS_STUB ? ESP_ROM_BAUD : 0);
@@ -682,6 +786,26 @@ export class ESPLoader extends EventTarget {
682
786
  await this.reconfigurePort(baud);
683
787
  }
684
788
 
789
+ // Track current baudrate for reconnect
790
+ if (this._parent) {
791
+ this._parent._currentBaudRate = baud;
792
+ } else {
793
+ this._currentBaudRate = baud;
794
+ }
795
+
796
+ // Warn if baudrate exceeds USB-Serial chip capability
797
+ const maxBaud = this._parent
798
+ ? this._parent._maxUSBSerialBaudrate
799
+ : this._maxUSBSerialBaudrate;
800
+ if (maxBaud && baud > maxBaud) {
801
+ this.logger.log(
802
+ `⚠️ WARNING: Baudrate ${baud} exceeds USB-Serial chip limit (${maxBaud})!`,
803
+ );
804
+ this.logger.log(
805
+ `⚠️ This may cause data corruption or connection failures!`,
806
+ );
807
+ }
808
+
685
809
  this.logger.log(`Changed baud rate to ${baud}`);
686
810
  }
687
811
 
@@ -697,6 +821,9 @@ export class ESPLoader extends EventTarget {
697
821
  // Reopen Port
698
822
  await this.port.open({ baudRate: baud });
699
823
 
824
+ // Clear buffer again
825
+ await this.flushSerialBuffers();
826
+
700
827
  // Restart Readloop
701
828
  this.readLoop();
702
829
  } catch (e) {
@@ -715,10 +842,10 @@ export class ESPLoader extends EventTarget {
715
842
  this._inputBuffer.length = 0;
716
843
  let response = await this._sync();
717
844
  if (response) {
718
- await sleep(100);
845
+ await sleep(SYNC_TIMEOUT);
719
846
  return true;
720
847
  }
721
- await sleep(100);
848
+ await sleep(SYNC_TIMEOUT);
722
849
  }
723
850
 
724
851
  throw new Error("Couldn't sync to ESP. Try resetting.");
@@ -896,6 +1023,9 @@ export class ESPLoader extends EventTarget {
896
1023
  * number of blocks requred.
897
1024
  */
898
1025
  async flashBegin(size = 0, offset = 0, encrypted = false) {
1026
+ // Flush serial buffers before flash write operation
1027
+ await this.flushSerialBuffers();
1028
+
899
1029
  let eraseSize;
900
1030
  let buffer;
901
1031
  let flashWriteSize = this.getFlashWriteSize();
@@ -911,7 +1041,10 @@ export class ESPLoader extends EventTarget {
911
1041
  CHIP_FAMILY_ESP32C6,
912
1042
  CHIP_FAMILY_ESP32C61,
913
1043
  CHIP_FAMILY_ESP32H2,
1044
+ CHIP_FAMILY_ESP32H4,
1045
+ CHIP_FAMILY_ESP32H21,
914
1046
  CHIP_FAMILY_ESP32P4,
1047
+ CHIP_FAMILY_ESP32S31,
915
1048
  ].includes(this.chipFamily)
916
1049
  ) {
917
1050
  await this.checkCommand(ESP_SPI_ATTACH, new Array(8).fill(0));
@@ -942,7 +1075,10 @@ export class ESPLoader extends EventTarget {
942
1075
  this.chipFamily == CHIP_FAMILY_ESP32C6 ||
943
1076
  this.chipFamily == CHIP_FAMILY_ESP32C61 ||
944
1077
  this.chipFamily == CHIP_FAMILY_ESP32H2 ||
945
- this.chipFamily == CHIP_FAMILY_ESP32P4
1078
+ this.chipFamily == CHIP_FAMILY_ESP32H4 ||
1079
+ this.chipFamily == CHIP_FAMILY_ESP32H21 ||
1080
+ this.chipFamily == CHIP_FAMILY_ESP32P4 ||
1081
+ this.chipFamily == CHIP_FAMILY_ESP32S31
946
1082
  ) {
947
1083
  buffer = buffer.concat(pack("<I", encrypted ? 1 : 0));
948
1084
  }
@@ -1265,12 +1401,20 @@ export class ESPLoader extends EventTarget {
1265
1401
  return await this.checkCommand(ESP_MEM_END, data, 0, timeout);
1266
1402
  }
1267
1403
 
1268
- async runStub(): Promise<EspStubLoader> {
1269
- const stub: Record<string, any> = await getStubCode(
1404
+ async runStub(skipFlashDetection = false): Promise<EspStubLoader> {
1405
+ const stub: Record<string, any> | null = await getStubCode(
1270
1406
  this.chipFamily,
1271
1407
  this.chipRevision,
1272
1408
  );
1273
1409
 
1410
+ // No stub available for this chip, return ROM loader
1411
+ if (stub === null) {
1412
+ this.logger.log(
1413
+ `Stub flasher is not yet supported on ${this.chipName}, using ROM loader`,
1414
+ );
1415
+ return this as unknown as EspStubLoader;
1416
+ }
1417
+
1274
1418
  // We're transferring over USB, right?
1275
1419
  let ramBlock = USB_RAM_BLOCK;
1276
1420
 
@@ -1292,7 +1436,6 @@ export class ESPLoader extends EventTarget {
1292
1436
  }
1293
1437
  }
1294
1438
  }
1295
- this.logger.log("Running stub...");
1296
1439
  await this.memFinish(stub["entry"]);
1297
1440
 
1298
1441
  let pChar: string;
@@ -1306,8 +1449,10 @@ export class ESPLoader extends EventTarget {
1306
1449
  this.logger.log("Stub is now running...");
1307
1450
  const espStubLoader = new EspStubLoader(this.port, this.logger, this);
1308
1451
 
1309
- // Try to autodetect the flash size as soon as the stub is running.
1310
- await espStubLoader.detectFlashSize();
1452
+ // Try to autodetect the flash size.
1453
+ if (!skipFlashDetection) {
1454
+ await espStubLoader.detectFlashSize();
1455
+ }
1311
1456
 
1312
1457
  return espStubLoader;
1313
1458
  }
@@ -1337,6 +1482,328 @@ export class ESPLoader extends EventTarget {
1337
1482
  });
1338
1483
  this.connected = false;
1339
1484
  }
1485
+
1486
+ /**
1487
+ * @name reconnectAndResume
1488
+ * Reconnect the serial port to flush browser buffers and reload stub
1489
+ */
1490
+ async reconnect(): Promise<void> {
1491
+ if (this._parent) {
1492
+ await this._parent.reconnect();
1493
+ return;
1494
+ }
1495
+
1496
+ this.logger.log("Reconnecting serial port...");
1497
+
1498
+ this.connected = false;
1499
+ this.__inputBuffer = [];
1500
+
1501
+ // Cancel reader
1502
+ if (this._reader) {
1503
+ try {
1504
+ await this._reader.cancel();
1505
+ } catch (err) {
1506
+ this.logger.debug(`Reader cancel error: ${err}`);
1507
+ }
1508
+ this._reader = undefined;
1509
+ }
1510
+
1511
+ await sleep(SYNC_TIMEOUT);
1512
+
1513
+ // Close port
1514
+ try {
1515
+ await this.port.close();
1516
+ this.logger.log("Port closed");
1517
+ } catch (err) {
1518
+ this.logger.debug(`Port close error: ${err}`);
1519
+ }
1520
+
1521
+ // Wait for port to fully close
1522
+ await sleep(SYNC_TIMEOUT);
1523
+
1524
+ // Open the port
1525
+ this.logger.debug("Opening port...");
1526
+ try {
1527
+ await this.port.open({ baudRate: ESP_ROM_BAUD });
1528
+ this.connected = true;
1529
+ } catch (err) {
1530
+ throw new Error(`Failed to open port: ${err}`);
1531
+ }
1532
+
1533
+ // Wait for port to be fully ready
1534
+ await sleep(SYNC_TIMEOUT);
1535
+
1536
+ // Verify port streams are available
1537
+ if (!this.port.readable || !this.port.writable) {
1538
+ throw new Error(
1539
+ `Port streams not available after open (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`,
1540
+ );
1541
+ }
1542
+
1543
+ // Save chip info and flash size (no need to detect again)
1544
+ const savedChipFamily = this.chipFamily;
1545
+ const savedChipName = this.chipName;
1546
+ const savedChipRevision = this.chipRevision;
1547
+ const savedChipVariant = this.chipVariant;
1548
+ const savedFlashSize = this.flashSize;
1549
+
1550
+ // Reinitialize without chip detection
1551
+ await this.hardReset(true);
1552
+
1553
+ if (!this._parent) {
1554
+ this.__inputBuffer = [];
1555
+ this.__totalBytesRead = 0;
1556
+ this.readLoop();
1557
+ }
1558
+
1559
+ await this.flushSerialBuffers();
1560
+ await this.sync();
1561
+
1562
+ // Restore chip info (skip detection)
1563
+ this.chipFamily = savedChipFamily;
1564
+ this.chipName = savedChipName;
1565
+ this.chipRevision = savedChipRevision;
1566
+ this.chipVariant = savedChipVariant;
1567
+ this.flashSize = savedFlashSize;
1568
+
1569
+ this.logger.debug(`Reconnect complete (chip: ${this.chipName})`);
1570
+
1571
+ // Verify port is ready
1572
+ if (!this.port.writable || !this.port.readable) {
1573
+ throw new Error("Port not ready after reconnect");
1574
+ }
1575
+
1576
+ // Load stub (skip flash detection)
1577
+ const stubLoader = await this.runStub(true);
1578
+ this.logger.debug("Stub loaded");
1579
+
1580
+ // Restore baudrate if it was changed
1581
+ if (this._currentBaudRate !== ESP_ROM_BAUD) {
1582
+ await stubLoader.setBaudrate(this._currentBaudRate);
1583
+
1584
+ // Wait for port to be ready after baudrate change
1585
+ await sleep(SYNC_TIMEOUT);
1586
+
1587
+ // Verify port is still ready after baudrate change
1588
+ if (!this.port.writable || !this.port.readable) {
1589
+ throw new Error(
1590
+ `Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`,
1591
+ );
1592
+ }
1593
+ }
1594
+
1595
+ // Copy stub state to this instance if we're a stub loader
1596
+ if (this.IS_STUB) {
1597
+ Object.assign(this, stubLoader);
1598
+ }
1599
+ this.logger.debug("Reconnection successful");
1600
+ }
1601
+
1602
+ /**
1603
+ * @name flushSerialBuffers
1604
+ * Flush any pending data in the TX and RX serial port buffers
1605
+ * This clears both the application RX buffer and waits for hardware buffers to drain
1606
+ */
1607
+ private async flushSerialBuffers(): Promise<void> {
1608
+ // Clear application RX buffer
1609
+ if (!this._parent) {
1610
+ this.__inputBuffer = [];
1611
+ }
1612
+
1613
+ // Wait for any pending TX operations and in-flight RX data
1614
+ await sleep(SYNC_TIMEOUT);
1615
+
1616
+ // Clear RX buffer again
1617
+ if (!this._parent) {
1618
+ this.__inputBuffer = [];
1619
+ }
1620
+
1621
+ // Wait longer to ensure all stale data has been received and discarded
1622
+ await sleep(SYNC_TIMEOUT * 2);
1623
+
1624
+ // Final clear
1625
+ if (!this._parent) {
1626
+ this.__inputBuffer = [];
1627
+ }
1628
+
1629
+ this.logger.debug("Serial buffers flushed");
1630
+ }
1631
+
1632
+ /**
1633
+ * @name readFlash
1634
+ * Read flash memory from the chip (only works with stub loader)
1635
+ * @param addr - Address to read from
1636
+ * @param size - Number of bytes to read
1637
+ * @param onPacketReceived - Optional callback function called when packet is received
1638
+ * @returns Uint8Array containing the flash data
1639
+ */
1640
+ async readFlash(
1641
+ addr: number,
1642
+ size: number,
1643
+ onPacketReceived?: (
1644
+ packet: Uint8Array,
1645
+ progress: number,
1646
+ totalSize: number,
1647
+ ) => void,
1648
+ ): Promise<Uint8Array> {
1649
+ if (!this.IS_STUB) {
1650
+ throw new Error(
1651
+ "Reading flash is only supported in stub mode. Please run runStub() first.",
1652
+ );
1653
+ }
1654
+
1655
+ // Check if we should reconnect BEFORE starting the read
1656
+ // Reconnect if total bytes read >= 4MB to ensure clean state
1657
+ if (this._totalBytesRead >= 4 * 1024 * 1024) {
1658
+ this.logger.log(
1659
+ // `Total bytes read: ${this._totalBytesRead}. Reconnecting before new read...`,
1660
+ `Reconnecting before new read...`,
1661
+ );
1662
+
1663
+ try {
1664
+ await this.reconnect();
1665
+ } catch (err) {
1666
+ // If reconnect fails, throw error - don't continue with potentially broken state
1667
+ throw new Error(`Reconnect failed: ${err}`);
1668
+ }
1669
+ }
1670
+
1671
+ // Flush serial buffers before flash read operation
1672
+ await this.flushSerialBuffers();
1673
+
1674
+ this.logger.log(
1675
+ `Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`,
1676
+ );
1677
+
1678
+ const CHUNK_SIZE = 0x10000; // 64KB chunks
1679
+
1680
+ let allData = new Uint8Array(0);
1681
+ let currentAddr = addr;
1682
+ let remainingSize = size;
1683
+
1684
+ while (remainingSize > 0) {
1685
+ // Reconnect every 4MB to prevent browser buffer issues
1686
+ if (allData.length > 0 && allData.length % (4 * 1024 * 1024) === 0) {
1687
+ this.logger.debug(
1688
+ `Read ${allData.length} bytes. Reconnecting to clear buffers...`,
1689
+ );
1690
+ try {
1691
+ await this.reconnect();
1692
+ } catch (err) {
1693
+ throw new Error(`Reconnect failed during read: ${err}`);
1694
+ }
1695
+ }
1696
+
1697
+ const chunkSize = Math.min(CHUNK_SIZE, remainingSize);
1698
+ let chunkSuccess = false;
1699
+ let retryCount = 0;
1700
+ const MAX_RETRIES = 3;
1701
+
1702
+ // Retry loop for this chunk
1703
+ while (!chunkSuccess && retryCount <= MAX_RETRIES) {
1704
+ try {
1705
+ this.logger.debug(
1706
+ `Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`,
1707
+ );
1708
+
1709
+ // Send read flash command for this chunk
1710
+ let pkt = pack("<IIII", currentAddr, chunkSize, 0x1000, 1024);
1711
+ const [res, _] = await this.checkCommand(ESP_READ_FLASH, pkt);
1712
+
1713
+ if (res != 0) {
1714
+ throw new Error("Failed to read memory: " + res);
1715
+ }
1716
+
1717
+ let resp = new Uint8Array(0);
1718
+
1719
+ while (resp.length < chunkSize) {
1720
+ // Read a SLIP packet
1721
+ let packet: number[];
1722
+ try {
1723
+ packet = await this.readPacket(FLASH_READ_TIMEOUT);
1724
+ } catch (err) {
1725
+ if (err instanceof SlipReadError) {
1726
+ this.logger.debug(
1727
+ `SLIP read error at ${resp.length} bytes: ${err.message}`,
1728
+ );
1729
+ // If we've read all the data we need, break
1730
+ if (resp.length >= chunkSize) {
1731
+ break;
1732
+ }
1733
+ }
1734
+ throw err;
1735
+ }
1736
+
1737
+ if (packet && packet.length > 0) {
1738
+ const packetData = new Uint8Array(packet);
1739
+
1740
+ // Append to response
1741
+ const newResp = new Uint8Array(resp.length + packetData.length);
1742
+ newResp.set(resp);
1743
+ newResp.set(packetData, resp.length);
1744
+ resp = newResp;
1745
+
1746
+ // Send acknowledgment
1747
+ const ackData = pack("<I", resp.length);
1748
+ const slipEncodedAck = slipEncode(ackData);
1749
+ await this.writeToStream(slipEncodedAck);
1750
+ }
1751
+ }
1752
+
1753
+ // Chunk read successfully - append to all data
1754
+ const newAllData = new Uint8Array(allData.length + resp.length);
1755
+ newAllData.set(allData);
1756
+ newAllData.set(resp, allData.length);
1757
+ allData = newAllData;
1758
+
1759
+ chunkSuccess = true;
1760
+ } catch (err) {
1761
+ retryCount++;
1762
+
1763
+ // Check if it's a timeout error
1764
+ if (
1765
+ err instanceof SlipReadError &&
1766
+ err.message.includes("Timed out")
1767
+ ) {
1768
+ if (retryCount <= MAX_RETRIES) {
1769
+ this.logger.log(
1770
+ `⚠️ Timeout error at 0x${currentAddr.toString(16)}. Reconnecting and retrying (attempt ${retryCount}/${MAX_RETRIES})...`,
1771
+ );
1772
+
1773
+ try {
1774
+ await this.reconnect();
1775
+ // Continue to retry the same chunk
1776
+ } catch (reconnectErr) {
1777
+ throw new Error(`Reconnect failed: ${reconnectErr}`);
1778
+ }
1779
+ } else {
1780
+ throw new Error(
1781
+ `Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries: ${err}`,
1782
+ );
1783
+ }
1784
+ } else {
1785
+ // Non-timeout error, don't retry
1786
+ throw err;
1787
+ }
1788
+ }
1789
+ }
1790
+
1791
+ // Update progress (use empty array since we already appended to allData)
1792
+ if (onPacketReceived) {
1793
+ onPacketReceived(new Uint8Array(chunkSize), allData.length, size);
1794
+ }
1795
+
1796
+ currentAddr += chunkSize;
1797
+ remainingSize -= chunkSize;
1798
+
1799
+ this.logger.debug(
1800
+ `Total progress: 0x${allData.length.toString(16)}/0x${size.toString(16)} bytes`,
1801
+ );
1802
+ }
1803
+
1804
+ this.logger.debug(`Successfully read ${allData.length} bytes from flash`);
1805
+ return allData;
1806
+ }
1340
1807
  }
1341
1808
 
1342
1809
  class EspStubLoader extends ESPLoader {
@@ -1357,6 +1824,12 @@ class EspStubLoader extends ESPLoader {
1357
1824
  offset: number,
1358
1825
  ): Promise<any> {
1359
1826
  let stub = await getStubCode(this.chipFamily, this.chipRevision);
1827
+
1828
+ // Stub may be null for chips without stub support
1829
+ if (stub === null) {
1830
+ return;
1831
+ }
1832
+
1360
1833
  let load_start = offset;
1361
1834
  let load_end = offset + size;
1362
1835
  console.log(load_start, load_end);