tasmota-webserial-esptool 6.5.4 → 7.0.1

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