esp32tool 1.1.8 → 1.2.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 (49) hide show
  1. package/.nojekyll +0 -0
  2. package/README.md +100 -6
  3. package/apple-touch-icon.png +0 -0
  4. package/build-electron-cli.cjs +177 -0
  5. package/build-single-binary.cjs +295 -0
  6. package/css/light.css +11 -0
  7. package/css/style.css +225 -35
  8. package/dist/cli.d.ts +17 -0
  9. package/dist/cli.js +458 -0
  10. package/dist/esp_loader.d.ts +129 -21
  11. package/dist/esp_loader.js +1227 -222
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.js +37 -4
  14. package/dist/node-usb-adapter.d.ts +47 -0
  15. package/dist/node-usb-adapter.js +725 -0
  16. package/dist/stubs/index.d.ts +1 -2
  17. package/dist/stubs/index.js +4 -0
  18. package/dist/web/index.js +1 -1
  19. package/electron/cli-main.cjs +74 -0
  20. package/electron/main.cjs +338 -0
  21. package/electron/main.js +7 -2
  22. package/favicon.ico +0 -0
  23. package/fix-cli-imports.cjs +127 -0
  24. package/generate-icons.sh +89 -0
  25. package/icons/icon-128.png +0 -0
  26. package/icons/icon-144.png +0 -0
  27. package/icons/icon-152.png +0 -0
  28. package/icons/icon-192.png +0 -0
  29. package/icons/icon-384.png +0 -0
  30. package/icons/icon-512.png +0 -0
  31. package/icons/icon-72.png +0 -0
  32. package/icons/icon-96.png +0 -0
  33. package/index.html +94 -64
  34. package/install-android.html +411 -0
  35. package/js/modules/esptool.js +1 -1
  36. package/js/script.js +165 -160
  37. package/js/webusb-serial.js +1017 -0
  38. package/license.md +1 -1
  39. package/manifest.json +89 -0
  40. package/package.cli.json +29 -0
  41. package/package.json +31 -21
  42. package/screenshots/desktop.png +0 -0
  43. package/screenshots/mobile.png +0 -0
  44. package/src/cli.ts +618 -0
  45. package/src/esp_loader.ts +1438 -254
  46. package/src/index.ts +69 -3
  47. package/src/node-usb-adapter.ts +924 -0
  48. package/src/stubs/index.ts +4 -1
  49. package/sw.js +155 -0
@@ -1,9 +1,8 @@
1
1
  /// <reference types="@types/w3c-web-serial" />
2
- import { CHIP_FAMILY_ESP32, CHIP_FAMILY_ESP32S2, CHIP_FAMILY_ESP32S3, CHIP_FAMILY_ESP32C2, CHIP_FAMILY_ESP32C3, CHIP_FAMILY_ESP32C5, CHIP_FAMILY_ESP32C6, CHIP_FAMILY_ESP32C61, CHIP_FAMILY_ESP32H2, CHIP_FAMILY_ESP32H4, CHIP_FAMILY_ESP32H21, CHIP_FAMILY_ESP32P4, CHIP_FAMILY_ESP32S31, CHIP_FAMILY_ESP8266, MAX_TIMEOUT, DEFAULT_TIMEOUT, ERASE_REGION_TIMEOUT_PER_MB, ESP_CHANGE_BAUDRATE, ESP_CHECKSUM_MAGIC, ESP_FLASH_BEGIN, ESP_FLASH_DATA, ESP_FLASH_END, ESP_MEM_BEGIN, ESP_MEM_DATA, ESP_MEM_END, ESP_READ_REG, ESP_WRITE_REG, ESP_SPI_ATTACH, ESP_SYNC, ESP_GET_SECURITY_INFO, FLASH_SECTOR_SIZE, FLASH_WRITE_SIZE, STUB_FLASH_WRITE_SIZE, MEM_END_ROM_TIMEOUT, ROM_INVALID_RECV_MSG, SYNC_PACKET, SYNC_TIMEOUT, USB_RAM_BLOCK, ESP_ERASE_FLASH, ESP_READ_FLASH, CHIP_ERASE_TIMEOUT, FLASH_READ_TIMEOUT, timeoutPerMb, ESP_ROM_BAUD, USB_JTAG_SERIAL_PID, ESP_FLASH_DEFL_BEGIN, ESP_FLASH_DEFL_DATA, ESP_FLASH_DEFL_END, getSpiFlashAddresses, DETECTED_FLASH_SIZES, CHIP_DETECT_MAGIC_REG_ADDR, CHIP_DETECT_MAGIC_VALUES, CHIP_ID_TO_INFO, ESP32P4_EFUSE_BLOCK1_ADDR, SlipReadError, } from "./const";
2
+ import { CHIP_FAMILY_ESP32, CHIP_FAMILY_ESP32S2, CHIP_FAMILY_ESP32S3, CHIP_FAMILY_ESP32C2, CHIP_FAMILY_ESP32C3, CHIP_FAMILY_ESP32C5, CHIP_FAMILY_ESP32C6, CHIP_FAMILY_ESP32C61, CHIP_FAMILY_ESP32H2, CHIP_FAMILY_ESP32H4, CHIP_FAMILY_ESP32H21, CHIP_FAMILY_ESP32P4, CHIP_FAMILY_ESP32S31, CHIP_FAMILY_ESP8266, MAX_TIMEOUT, DEFAULT_TIMEOUT, ERASE_REGION_TIMEOUT_PER_MB, ESP_CHANGE_BAUDRATE, ESP_CHECKSUM_MAGIC, ESP_FLASH_BEGIN, ESP_FLASH_DATA, ESP_FLASH_END, ESP_MEM_BEGIN, ESP_MEM_DATA, ESP_MEM_END, ESP_READ_REG, ESP_WRITE_REG, ESP_SPI_ATTACH, ESP_SYNC, ESP_GET_SECURITY_INFO, FLASH_SECTOR_SIZE, FLASH_WRITE_SIZE, STUB_FLASH_WRITE_SIZE, MEM_END_ROM_TIMEOUT, ROM_INVALID_RECV_MSG, SYNC_PACKET, SYNC_TIMEOUT, USB_RAM_BLOCK, ESP_ERASE_FLASH, ESP_ERASE_REGION, ESP_READ_FLASH, CHIP_ERASE_TIMEOUT, FLASH_READ_TIMEOUT, timeoutPerMb, ESP_ROM_BAUD, USB_JTAG_SERIAL_PID, ESP_FLASH_DEFL_BEGIN, ESP_FLASH_DEFL_DATA, ESP_FLASH_DEFL_END, getSpiFlashAddresses, DETECTED_FLASH_SIZES, CHIP_DETECT_MAGIC_REG_ADDR, CHIP_DETECT_MAGIC_VALUES, CHIP_ID_TO_INFO, ESP32P4_EFUSE_BLOCK1_ADDR, SlipReadError, } from "./const";
3
3
  import { getStubCode } from "./stubs";
4
4
  import { hexFormatter, sleep, slipEncode, toHex } from "./util";
5
- // @ts-expect-error pako ESM module doesn't have proper type definitions
6
- import { deflate } from "pako/dist/pako.esm.mjs";
5
+ import { deflate } from "pako";
7
6
  import { pack, unpack } from "./struct";
8
7
  export class ESPLoader extends EventTarget {
9
8
  constructor(port, logger, _parent) {
@@ -11,9 +10,9 @@ export class ESPLoader extends EventTarget {
11
10
  this.port = port;
12
11
  this.logger = logger;
13
12
  this._parent = _parent;
14
- this.chipName = null;
15
- this.chipRevision = null;
16
- this.chipVariant = null;
13
+ this.__chipName = null;
14
+ this.__chipRevision = null;
15
+ this.__chipVariant = null;
17
16
  this._efuses = new Array(4).fill(0);
18
17
  this._flashsize = 4 * 1024 * 1024;
19
18
  this.debug = false;
@@ -24,12 +23,110 @@ export class ESPLoader extends EventTarget {
24
23
  this._isESP32S2NativeUSB = false;
25
24
  this._initializationSucceeded = false;
26
25
  this.__commandLock = Promise.resolve([0, []]);
27
- this._isReconfiguring = false;
26
+ this.__isReconfiguring = false;
27
+ this.__abandonCurrentOperation = false;
28
+ // Adaptive speed adjustment for flash read operations
29
+ this.__adaptiveBlockMultiplier = 1;
30
+ this.__adaptiveMaxInFlightMultiplier = 1;
31
+ this.__consecutiveSuccessfulChunks = 0;
32
+ this.__lastAdaptiveAdjustment = 0;
33
+ this.__isCDCDevice = false;
28
34
  this.state_DTR = false;
35
+ this.state_RTS = false;
29
36
  this.__writeChain = Promise.resolve();
30
37
  }
38
+ // Chip properties with parent delegation
39
+ // chipFamily accessed before initialization as designed
40
+ get chipFamily() {
41
+ return this._parent ? this._parent.chipFamily : this.__chipFamily;
42
+ }
43
+ set chipFamily(value) {
44
+ if (this._parent) {
45
+ this._parent.chipFamily = value;
46
+ }
47
+ else {
48
+ this.__chipFamily = value;
49
+ }
50
+ }
51
+ get chipName() {
52
+ return this._parent ? this._parent.chipName : this.__chipName;
53
+ }
54
+ set chipName(value) {
55
+ if (this._parent) {
56
+ this._parent.chipName = value;
57
+ }
58
+ else {
59
+ this.__chipName = value;
60
+ }
61
+ }
62
+ get chipRevision() {
63
+ return this._parent ? this._parent.chipRevision : this.__chipRevision;
64
+ }
65
+ set chipRevision(value) {
66
+ if (this._parent) {
67
+ this._parent.chipRevision = value;
68
+ }
69
+ else {
70
+ this.__chipRevision = value;
71
+ }
72
+ }
73
+ get chipVariant() {
74
+ return this._parent ? this._parent.chipVariant : this.__chipVariant;
75
+ }
76
+ set chipVariant(value) {
77
+ if (this._parent) {
78
+ this._parent.chipVariant = value;
79
+ }
80
+ else {
81
+ this.__chipVariant = value;
82
+ }
83
+ }
31
84
  get _inputBuffer() {
32
- return this._parent ? this._parent._inputBuffer : this.__inputBuffer;
85
+ if (this._parent) {
86
+ return this._parent._inputBuffer;
87
+ }
88
+ if (this.__inputBuffer === undefined) {
89
+ throw new Error("_inputBuffer accessed before initialization");
90
+ }
91
+ return this.__inputBuffer;
92
+ }
93
+ get _inputBufferReadIndex() {
94
+ return this._parent
95
+ ? this._parent._inputBufferReadIndex
96
+ : this.__inputBufferReadIndex || 0;
97
+ }
98
+ set _inputBufferReadIndex(value) {
99
+ if (this._parent) {
100
+ this._parent._inputBufferReadIndex = value;
101
+ }
102
+ else {
103
+ this.__inputBufferReadIndex = value;
104
+ }
105
+ }
106
+ // Get available bytes in buffer (from read index to end)
107
+ get _inputBufferAvailable() {
108
+ return this._inputBuffer.length - this._inputBufferReadIndex;
109
+ }
110
+ // Read one byte from buffer (ring-buffer style with index pointer)
111
+ _readByte() {
112
+ if (this._inputBufferReadIndex >= this._inputBuffer.length) {
113
+ return undefined;
114
+ }
115
+ return this._inputBuffer[this._inputBufferReadIndex++];
116
+ }
117
+ // Clear input buffer and reset read index
118
+ _clearInputBuffer() {
119
+ this._inputBuffer.length = 0;
120
+ this._inputBufferReadIndex = 0;
121
+ }
122
+ // Compact buffer when read index gets too large (prevent memory growth)
123
+ _compactInputBuffer() {
124
+ if (this._inputBufferReadIndex > 1000 &&
125
+ this._inputBufferReadIndex > this._inputBuffer.length / 2) {
126
+ // Remove already-read bytes and reset index
127
+ this._inputBuffer.splice(0, this._inputBufferReadIndex);
128
+ this._inputBufferReadIndex = 0;
129
+ }
33
130
  }
34
131
  get _totalBytesRead() {
35
132
  return this._parent
@@ -55,6 +152,95 @@ export class ESPLoader extends EventTarget {
55
152
  this.__commandLock = value;
56
153
  }
57
154
  }
155
+ get _isReconfiguring() {
156
+ return this._parent
157
+ ? this._parent._isReconfiguring
158
+ : this.__isReconfiguring;
159
+ }
160
+ set _isReconfiguring(value) {
161
+ if (this._parent) {
162
+ this._parent._isReconfiguring = value;
163
+ }
164
+ else {
165
+ this.__isReconfiguring = value;
166
+ }
167
+ }
168
+ get _abandonCurrentOperation() {
169
+ return this._parent
170
+ ? this._parent._abandonCurrentOperation
171
+ : this.__abandonCurrentOperation;
172
+ }
173
+ set _abandonCurrentOperation(value) {
174
+ if (this._parent) {
175
+ this._parent._abandonCurrentOperation = value;
176
+ }
177
+ else {
178
+ this.__abandonCurrentOperation = value;
179
+ }
180
+ }
181
+ get _adaptiveBlockMultiplier() {
182
+ return this._parent
183
+ ? this._parent._adaptiveBlockMultiplier
184
+ : this.__adaptiveBlockMultiplier;
185
+ }
186
+ set _adaptiveBlockMultiplier(value) {
187
+ if (this._parent) {
188
+ this._parent._adaptiveBlockMultiplier = value;
189
+ }
190
+ else {
191
+ this.__adaptiveBlockMultiplier = value;
192
+ }
193
+ }
194
+ get _adaptiveMaxInFlightMultiplier() {
195
+ return this._parent
196
+ ? this._parent._adaptiveMaxInFlightMultiplier
197
+ : this.__adaptiveMaxInFlightMultiplier;
198
+ }
199
+ set _adaptiveMaxInFlightMultiplier(value) {
200
+ if (this._parent) {
201
+ this._parent._adaptiveMaxInFlightMultiplier = value;
202
+ }
203
+ else {
204
+ this.__adaptiveMaxInFlightMultiplier = value;
205
+ }
206
+ }
207
+ get _consecutiveSuccessfulChunks() {
208
+ return this._parent
209
+ ? this._parent._consecutiveSuccessfulChunks
210
+ : this.__consecutiveSuccessfulChunks;
211
+ }
212
+ set _consecutiveSuccessfulChunks(value) {
213
+ if (this._parent) {
214
+ this._parent._consecutiveSuccessfulChunks = value;
215
+ }
216
+ else {
217
+ this.__consecutiveSuccessfulChunks = value;
218
+ }
219
+ }
220
+ get _lastAdaptiveAdjustment() {
221
+ return this._parent
222
+ ? this._parent._lastAdaptiveAdjustment
223
+ : this.__lastAdaptiveAdjustment;
224
+ }
225
+ set _lastAdaptiveAdjustment(value) {
226
+ if (this._parent) {
227
+ this._parent._lastAdaptiveAdjustment = value;
228
+ }
229
+ else {
230
+ this.__lastAdaptiveAdjustment = value;
231
+ }
232
+ }
233
+ get _isCDCDevice() {
234
+ return this._parent ? this._parent._isCDCDevice : this.__isCDCDevice;
235
+ }
236
+ set _isCDCDevice(value) {
237
+ if (this._parent) {
238
+ this._parent._isCDCDevice = value;
239
+ }
240
+ else {
241
+ this.__isCDCDevice = value;
242
+ }
243
+ }
58
244
  detectUSBSerialChip(vendorId, productId) {
59
245
  // Common USB-Serial chip vendors and their products
60
246
  const chips = {
@@ -102,6 +288,7 @@ export class ESPLoader extends EventTarget {
102
288
  async initialize() {
103
289
  if (!this._parent) {
104
290
  this.__inputBuffer = [];
291
+ this.__inputBufferReadIndex = 0;
105
292
  this.__totalBytesRead = 0;
106
293
  // Detect and log USB-Serial chip info
107
294
  const portInfo = this.port.getInfo();
@@ -116,6 +303,12 @@ export class ESPLoader extends EventTarget {
116
303
  if (portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x2) {
117
304
  this._isESP32S2NativeUSB = true;
118
305
  }
306
+ // Detect CDC devices for adaptive speed adjustment
307
+ // Espressif Native USB (VID: 0x303a) or CH343 (VID: 0x1a86, PID: 0x55d3)
308
+ if (portInfo.usbVendorId === 0x303a ||
309
+ (portInfo.usbVendorId === 0x1a86 && portInfo.usbProductId === 0x55d3)) {
310
+ this._isCDCDevice = true;
311
+ }
119
312
  }
120
313
  // Don't await this promise so it doesn't block rest of method.
121
314
  this.readLoop();
@@ -172,7 +365,7 @@ export class ESPLoader extends EventTarget {
172
365
  // This ensures all error responses are cleared before continuing
173
366
  await this.drainInputBuffer(200);
174
367
  // Clear input buffer and re-sync to recover from failed command
175
- this._inputBuffer.length = 0;
368
+ this._clearInputBuffer();
176
369
  await sleep(SYNC_TIMEOUT);
177
370
  // Re-sync with the chip to ensure clean communication
178
371
  try {
@@ -249,6 +442,18 @@ export class ESPLoader extends EventTarget {
249
442
  apiVersion,
250
443
  };
251
444
  }
445
+ /**
446
+ * Get MAC address from efuses
447
+ */
448
+ async getMacAddress() {
449
+ if (!this._initializationSucceeded) {
450
+ throw new Error("getMacAddress() requires initialize() to have completed successfully");
451
+ }
452
+ const macBytes = this.macAddr(); // chip-family-aware
453
+ return macBytes
454
+ .map((b) => b.toString(16).padStart(2, "0").toUpperCase())
455
+ .join(":");
456
+ }
252
457
  /**
253
458
  * @name readLoop
254
459
  * Reads data from the input stream and places it in the inputBuffer
@@ -281,6 +486,12 @@ export class ESPLoader extends EventTarget {
281
486
  catch {
282
487
  this.logger.error("Read loop got disconnected");
283
488
  }
489
+ finally {
490
+ // Always reset reconfiguring flag when read loop ends
491
+ // This prevents "Cannot write during port reconfiguration" errors
492
+ // when the read loop dies unexpectedly
493
+ this._isReconfiguring = false;
494
+ }
284
495
  // Disconnected!
285
496
  this.connected = false;
286
497
  // Check if this is ESP32-S2 Native USB that needs port reselection
@@ -297,6 +508,9 @@ export class ESPLoader extends EventTarget {
297
508
  sleep(ms = 100) {
298
509
  return new Promise((resolve) => setTimeout(resolve, ms));
299
510
  }
511
+ // ============================================================================
512
+ // Web Serial (Desktop) - DTR/RTS Signal Handling & Reset Strategies
513
+ // ============================================================================
300
514
  async setRTS(state) {
301
515
  await this.port.setSignals({ requestToSend: state });
302
516
  // Work-around for adapters on Windows using the usbser.sys driver:
@@ -309,6 +523,511 @@ export class ESPLoader extends EventTarget {
309
523
  this.state_DTR = state;
310
524
  await this.port.setSignals({ dataTerminalReady: state });
311
525
  }
526
+ /**
527
+ * @name hardResetUSBJTAGSerial
528
+ * USB-JTAG/Serial reset for Web Serial (Desktop)
529
+ */
530
+ async hardResetUSBJTAGSerial() {
531
+ await this.setRTS(false);
532
+ await this.setDTR(false); // Idle
533
+ await this.sleep(100);
534
+ await this.setDTR(true); // Set IO0
535
+ await this.setRTS(false);
536
+ await this.sleep(100);
537
+ await this.setRTS(true); // Reset
538
+ await this.setDTR(false);
539
+ await this.setRTS(true);
540
+ await this.sleep(100);
541
+ await this.setDTR(false);
542
+ await this.setRTS(false); // Chip out of reset
543
+ await this.sleep(200);
544
+ }
545
+ /**
546
+ * @name hardResetClassic
547
+ * Classic reset for Web Serial (Desktop)
548
+ */
549
+ async hardResetClassic() {
550
+ await this.setDTR(false); // IO0=HIGH
551
+ await this.setRTS(true); // EN=LOW, chip in reset
552
+ await this.sleep(100);
553
+ await this.setDTR(true); // IO0=LOW
554
+ await this.setRTS(false); // EN=HIGH, chip out of reset
555
+ await this.sleep(50);
556
+ await this.setDTR(false); // IO0=HIGH, done
557
+ await this.sleep(200);
558
+ }
559
+ // ============================================================================
560
+ // WebUSB (Android) - DTR/RTS Signal Handling & Reset Strategies
561
+ // ============================================================================
562
+ async setRTSWebUSB(state) {
563
+ this.state_RTS = state;
564
+ // Always specify both signals to avoid flipping the other line
565
+ // The WebUSB setSignals() now preserves unspecified signals, but being explicit is safer
566
+ await this.port.setSignals({
567
+ requestToSend: state,
568
+ dataTerminalReady: this.state_DTR,
569
+ });
570
+ }
571
+ async setDTRWebUSB(state) {
572
+ this.state_DTR = state;
573
+ // Always specify both signals to avoid flipping the other line
574
+ await this.port.setSignals({
575
+ dataTerminalReady: state,
576
+ requestToSend: this.state_RTS, // Explicitly preserve current RTS state
577
+ });
578
+ }
579
+ async setDTRandRTSWebUSB(dtr, rts) {
580
+ this.state_DTR = dtr;
581
+ this.state_RTS = rts;
582
+ await this.port.setSignals({
583
+ dataTerminalReady: dtr,
584
+ requestToSend: rts,
585
+ });
586
+ }
587
+ /**
588
+ * @name hardResetUSBJTAGSerialWebUSB
589
+ * USB-JTAG/Serial reset for WebUSB (Android)
590
+ */
591
+ async hardResetUSBJTAGSerialWebUSB() {
592
+ await this.setRTSWebUSB(false);
593
+ await this.setDTRWebUSB(false); // Idle
594
+ await this.sleep(100);
595
+ await this.setDTRWebUSB(true); // Set IO0
596
+ await this.setRTSWebUSB(false);
597
+ await this.sleep(100);
598
+ await this.setRTSWebUSB(true); // Reset
599
+ await this.setDTRWebUSB(false);
600
+ await this.setRTSWebUSB(true);
601
+ await this.sleep(100);
602
+ await this.setDTRWebUSB(false);
603
+ await this.setRTSWebUSB(false); // Chip out of reset
604
+ await this.sleep(200);
605
+ }
606
+ /**
607
+ * @name hardResetUSBJTAGSerialInvertedDTRWebUSB
608
+ * USB-JTAG/Serial reset with inverted DTR for WebUSB (Android)
609
+ */
610
+ async hardResetUSBJTAGSerialInvertedDTRWebUSB() {
611
+ await this.setRTSWebUSB(false);
612
+ await this.setDTRWebUSB(true); // Idle (DTR inverted)
613
+ await this.sleep(100);
614
+ await this.setDTRWebUSB(false); // Set IO0 (DTR inverted)
615
+ await this.setRTSWebUSB(false);
616
+ await this.sleep(100);
617
+ await this.setRTSWebUSB(true); // Reset
618
+ await this.setDTRWebUSB(true); // (DTR inverted)
619
+ await this.setRTSWebUSB(true);
620
+ await this.sleep(100);
621
+ await this.setDTRWebUSB(true); // (DTR inverted)
622
+ await this.setRTSWebUSB(false); // Chip out of reset
623
+ await this.sleep(200);
624
+ }
625
+ /**
626
+ * @name hardResetClassicWebUSB
627
+ * Classic reset for WebUSB (Android)
628
+ */
629
+ async hardResetClassicWebUSB() {
630
+ await this.setDTRWebUSB(false); // IO0=HIGH
631
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
632
+ await this.sleep(100);
633
+ await this.setDTRWebUSB(true); // IO0=LOW
634
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
635
+ await this.sleep(50);
636
+ await this.setDTRWebUSB(false); // IO0=HIGH, done
637
+ await this.sleep(200);
638
+ }
639
+ /**
640
+ * @name hardResetUnixTightWebUSB
641
+ * Unix Tight reset for WebUSB (Android) - sets DTR and RTS simultaneously
642
+ */
643
+ async hardResetUnixTightWebUSB() {
644
+ await this.setDTRandRTSWebUSB(false, false);
645
+ await this.setDTRandRTSWebUSB(true, true);
646
+ await this.setDTRandRTSWebUSB(false, true); // IO0=HIGH & EN=LOW, chip in reset
647
+ await this.sleep(100);
648
+ await this.setDTRandRTSWebUSB(true, false); // IO0=LOW & EN=HIGH, chip out of reset
649
+ await this.sleep(50);
650
+ await this.setDTRandRTSWebUSB(false, false); // IO0=HIGH, done
651
+ await this.setDTRWebUSB(false); // Ensure IO0=HIGH
652
+ await this.sleep(200);
653
+ }
654
+ /**
655
+ * @name hardResetClassicLongDelayWebUSB
656
+ * Classic reset with longer delays for WebUSB (Android)
657
+ * Specifically for CP2102/CH340 which may need more time
658
+ */
659
+ async hardResetClassicLongDelayWebUSB() {
660
+ await this.setDTRWebUSB(false); // IO0=HIGH
661
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
662
+ await this.sleep(500); // Extra long delay
663
+ await this.setDTRWebUSB(true); // IO0=LOW
664
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
665
+ await this.sleep(200);
666
+ await this.setDTRWebUSB(false); // IO0=HIGH, done
667
+ await this.sleep(500); // Extra long delay
668
+ }
669
+ /**
670
+ * @name hardResetClassicShortDelayWebUSB
671
+ * Classic reset with shorter delays for WebUSB (Android)
672
+ */
673
+ async hardResetClassicShortDelayWebUSB() {
674
+ await this.setDTRWebUSB(false); // IO0=HIGH
675
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
676
+ await this.sleep(50);
677
+ await this.setDTRWebUSB(true); // IO0=LOW
678
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
679
+ await this.sleep(25);
680
+ await this.setDTRWebUSB(false); // IO0=HIGH, done
681
+ await this.sleep(100);
682
+ }
683
+ /**
684
+ * @name hardResetInvertedWebUSB
685
+ * Inverted reset sequence for WebUSB (Android) - both signals inverted
686
+ */
687
+ async hardResetInvertedWebUSB() {
688
+ await this.setDTRWebUSB(true); // IO0=HIGH (inverted)
689
+ await this.setRTSWebUSB(false); // EN=LOW, chip in reset (inverted)
690
+ await this.sleep(100);
691
+ await this.setDTRWebUSB(false); // IO0=LOW (inverted)
692
+ await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (inverted)
693
+ await this.sleep(50);
694
+ await this.setDTRWebUSB(true); // IO0=HIGH, done (inverted)
695
+ await this.sleep(200);
696
+ }
697
+ /**
698
+ * @name hardResetInvertedDTRWebUSB
699
+ * Only DTR inverted for WebUSB (Android)
700
+ */
701
+ async hardResetInvertedDTRWebUSB() {
702
+ await this.setDTRWebUSB(true); // IO0=HIGH (DTR inverted)
703
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset (RTS normal)
704
+ await this.sleep(100);
705
+ await this.setDTRWebUSB(false); // IO0=LOW (DTR inverted)
706
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset (RTS normal)
707
+ await this.sleep(50);
708
+ await this.setDTRWebUSB(true); // IO0=HIGH, done (DTR inverted)
709
+ await this.sleep(200);
710
+ }
711
+ /**
712
+ * @name hardResetInvertedRTSWebUSB
713
+ * Only RTS inverted for WebUSB (Android)
714
+ */
715
+ async hardResetInvertedRTSWebUSB() {
716
+ await this.setDTRWebUSB(false); // IO0=HIGH (DTR normal)
717
+ await this.setRTSWebUSB(false); // EN=LOW, chip in reset (RTS inverted)
718
+ await this.sleep(100);
719
+ await this.setDTRWebUSB(true); // IO0=LOW (DTR normal)
720
+ await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (RTS inverted)
721
+ await this.sleep(50);
722
+ await this.setDTRWebUSB(false); // IO0=HIGH, done (DTR normal)
723
+ await this.sleep(200);
724
+ }
725
+ /**
726
+ * Check if we're using WebUSB (Android) or Web Serial (Desktop)
727
+ */
728
+ isWebUSB() {
729
+ // WebUSBSerial class has isWebUSB flag - this is the most reliable check
730
+ return this.port.isWebUSB === true;
731
+ }
732
+ /**
733
+ * @name connectWithResetStrategies
734
+ * Try different reset strategies to enter bootloader mode
735
+ * Similar to esptool.py's connect() method with multiple reset strategies
736
+ */
737
+ async connectWithResetStrategies() {
738
+ const portInfo = this.port.getInfo();
739
+ const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
740
+ const isEspressifUSB = portInfo.usbVendorId === 0x303a;
741
+ // this.logger.log(
742
+ // `Detected USB: VID=0x${portInfo.usbVendorId?.toString(16) || "unknown"}, PID=0x${portInfo.usbProductId?.toString(16) || "unknown"}`,
743
+ // );
744
+ // Define reset strategies to try in order
745
+ const resetStrategies = [];
746
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
747
+ const self = this;
748
+ // WebUSB (Android) uses different reset methods than Web Serial (Desktop)
749
+ if (this.isWebUSB()) {
750
+ // For USB-Serial chips (CP2102, CH340, etc.), try inverted strategies first
751
+ const isUSBSerialChip = !isUSBJTAGSerial && !isEspressifUSB;
752
+ // Detect specific chip types once
753
+ const isCP2102 = portInfo.usbVendorId === 0x10c4;
754
+ const isCH34x = portInfo.usbVendorId === 0x1a86;
755
+ // Check for ESP32-S2 Native USB (VID: 0x303a, PID: 0x0002)
756
+ const isESP32S2NativeUSB = portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x0002;
757
+ // WebUSB Strategy 1: USB-JTAG/Serial reset (for Native USB only)
758
+ if (isUSBJTAGSerial || isEspressifUSB) {
759
+ if (isESP32S2NativeUSB) {
760
+ // ESP32-S2 Native USB: Try multiple strategies
761
+ // The device might be in JTAG mode OR CDC mode
762
+ // Strategy 1: USB-JTAG/Serial (works in CDC mode on Desktop)
763
+ resetStrategies.push({
764
+ name: "USB-JTAG/Serial (WebUSB) - ESP32-S2",
765
+ fn: async () => {
766
+ return await self.hardResetUSBJTAGSerialWebUSB();
767
+ },
768
+ });
769
+ // Strategy 2: USB-JTAG/Serial Inverted DTR (works in JTAG mode)
770
+ resetStrategies.push({
771
+ name: "USB-JTAG/Serial Inverted DTR (WebUSB) - ESP32-S2",
772
+ fn: async () => {
773
+ return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
774
+ },
775
+ });
776
+ // Strategy 3: UnixTight (CDC fallback)
777
+ resetStrategies.push({
778
+ name: "UnixTight (WebUSB) - ESP32-S2 CDC",
779
+ fn: async () => {
780
+ return await self.hardResetUnixTightWebUSB();
781
+ },
782
+ });
783
+ // Strategy 4: Classic reset (CDC fallback)
784
+ resetStrategies.push({
785
+ name: "Classic (WebUSB) - ESP32-S2 CDC",
786
+ fn: async () => {
787
+ return await self.hardResetClassicWebUSB();
788
+ },
789
+ });
790
+ }
791
+ else {
792
+ // Other USB-JTAG chips: Try Inverted DTR first - works best for ESP32-H2 and other JTAG chips
793
+ resetStrategies.push({
794
+ name: "USB-JTAG/Serial Inverted DTR (WebUSB)",
795
+ fn: async () => {
796
+ return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
797
+ },
798
+ });
799
+ resetStrategies.push({
800
+ name: "USB-JTAG/Serial (WebUSB)",
801
+ fn: async () => {
802
+ return await self.hardResetUSBJTAGSerialWebUSB();
803
+ },
804
+ });
805
+ resetStrategies.push({
806
+ name: "Inverted DTR Classic (WebUSB)",
807
+ fn: async () => {
808
+ return await self.hardResetInvertedDTRWebUSB();
809
+ },
810
+ });
811
+ }
812
+ }
813
+ // For USB-Serial chips, try inverted strategies first
814
+ if (isUSBSerialChip) {
815
+ if (isCH34x) {
816
+ // CH340/CH343: UnixTight works best (like CP2102)
817
+ resetStrategies.push({
818
+ name: "UnixTight (WebUSB) - CH34x",
819
+ fn: async () => {
820
+ return await self.hardResetUnixTightWebUSB();
821
+ },
822
+ });
823
+ resetStrategies.push({
824
+ name: "Classic (WebUSB) - CH34x",
825
+ fn: async () => {
826
+ return await self.hardResetClassicWebUSB();
827
+ },
828
+ });
829
+ resetStrategies.push({
830
+ name: "Inverted Both (WebUSB) - CH34x",
831
+ fn: async () => {
832
+ return await self.hardResetInvertedWebUSB();
833
+ },
834
+ });
835
+ resetStrategies.push({
836
+ name: "Inverted RTS (WebUSB) - CH34x",
837
+ fn: async () => {
838
+ return await self.hardResetInvertedRTSWebUSB();
839
+ },
840
+ });
841
+ resetStrategies.push({
842
+ name: "Inverted DTR (WebUSB) - CH34x",
843
+ fn: async () => {
844
+ return await self.hardResetInvertedDTRWebUSB();
845
+ },
846
+ });
847
+ }
848
+ else if (isCP2102) {
849
+ // CP2102: UnixTight works best (tested and confirmed)
850
+ // Try it first, then fallback to other strategies
851
+ resetStrategies.push({
852
+ name: "UnixTight (WebUSB) - CP2102",
853
+ fn: async () => {
854
+ return await self.hardResetUnixTightWebUSB();
855
+ },
856
+ });
857
+ resetStrategies.push({
858
+ name: "Classic (WebUSB) - CP2102",
859
+ fn: async () => {
860
+ return await self.hardResetClassicWebUSB();
861
+ },
862
+ });
863
+ resetStrategies.push({
864
+ name: "Inverted Both (WebUSB) - CP2102",
865
+ fn: async () => {
866
+ return await self.hardResetInvertedWebUSB();
867
+ },
868
+ });
869
+ resetStrategies.push({
870
+ name: "Inverted RTS (WebUSB) - CP2102",
871
+ fn: async () => {
872
+ return await self.hardResetInvertedRTSWebUSB();
873
+ },
874
+ });
875
+ resetStrategies.push({
876
+ name: "Inverted DTR (WebUSB) - CP2102",
877
+ fn: async () => {
878
+ return await self.hardResetInvertedDTRWebUSB();
879
+ },
880
+ });
881
+ }
882
+ else {
883
+ // For other USB-Serial chips, try UnixTight first, then multiple strategies
884
+ resetStrategies.push({
885
+ name: "UnixTight (WebUSB)",
886
+ fn: async () => {
887
+ return await self.hardResetUnixTightWebUSB();
888
+ },
889
+ });
890
+ resetStrategies.push({
891
+ name: "Classic (WebUSB)",
892
+ fn: async function () {
893
+ return await self.hardResetClassicWebUSB();
894
+ },
895
+ });
896
+ resetStrategies.push({
897
+ name: "Inverted Both (WebUSB)",
898
+ fn: async function () {
899
+ return await self.hardResetInvertedWebUSB();
900
+ },
901
+ });
902
+ resetStrategies.push({
903
+ name: "Inverted RTS (WebUSB)",
904
+ fn: async function () {
905
+ return await self.hardResetInvertedRTSWebUSB();
906
+ },
907
+ });
908
+ resetStrategies.push({
909
+ name: "Inverted DTR (WebUSB)",
910
+ fn: async function () {
911
+ return await self.hardResetInvertedDTRWebUSB();
912
+ },
913
+ });
914
+ }
915
+ }
916
+ // Add general fallback strategies only for non-CP2102 and non-ESP32-S2 Native USB chips
917
+ if (!isCP2102 && !isESP32S2NativeUSB) {
918
+ // Classic reset (for chips not handled above)
919
+ if (portInfo.usbVendorId !== 0x1a86) {
920
+ resetStrategies.push({
921
+ name: "Classic (WebUSB)",
922
+ fn: async function () {
923
+ return await self.hardResetClassicWebUSB();
924
+ },
925
+ });
926
+ }
927
+ // UnixTight reset (sets DTR/RTS simultaneously)
928
+ resetStrategies.push({
929
+ name: "UnixTight (WebUSB)",
930
+ fn: async function () {
931
+ return await self.hardResetUnixTightWebUSB();
932
+ },
933
+ });
934
+ // WebUSB Strategy: Classic with long delays
935
+ resetStrategies.push({
936
+ name: "Classic Long Delay (WebUSB)",
937
+ fn: async function () {
938
+ return await self.hardResetClassicLongDelayWebUSB();
939
+ },
940
+ });
941
+ // WebUSB Strategy: Classic with short delays
942
+ resetStrategies.push({
943
+ name: "Classic Short Delay (WebUSB)",
944
+ fn: async function () {
945
+ return await self.hardResetClassicShortDelayWebUSB();
946
+ },
947
+ });
948
+ // WebUSB Strategy: USB-JTAG/Serial fallback
949
+ if (!isUSBJTAGSerial && !isEspressifUSB) {
950
+ resetStrategies.push({
951
+ name: "USB-JTAG/Serial fallback (WebUSB)",
952
+ fn: async function () {
953
+ return await self.hardResetUSBJTAGSerialWebUSB();
954
+ },
955
+ });
956
+ }
957
+ }
958
+ }
959
+ else {
960
+ // Strategy: USB-JTAG/Serial reset
961
+ if (isUSBJTAGSerial || isEspressifUSB) {
962
+ resetStrategies.push({
963
+ name: "USB-JTAG/Serial",
964
+ fn: async function () {
965
+ return await self.hardResetUSBJTAGSerial();
966
+ },
967
+ });
968
+ }
969
+ // Strategy: Classic reset
970
+ resetStrategies.push({
971
+ name: "Classic",
972
+ fn: async function () {
973
+ return await self.hardResetClassic();
974
+ },
975
+ });
976
+ // Strategy: USB-JTAG/Serial fallback
977
+ if (!isUSBJTAGSerial && !isEspressifUSB) {
978
+ resetStrategies.push({
979
+ name: "USB-JTAG/Serial (fallback)",
980
+ fn: async function () {
981
+ return await self.hardResetUSBJTAGSerial();
982
+ },
983
+ });
984
+ }
985
+ }
986
+ let lastError = null;
987
+ // Try each reset strategy with timeout
988
+ for (const strategy of resetStrategies) {
989
+ try {
990
+ // Check if port is still open, if not, skip this strategy
991
+ if (!this.connected || !this.port.writable) {
992
+ this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
993
+ continue;
994
+ }
995
+ // Clear abandon flag before starting new strategy
996
+ this._abandonCurrentOperation = false;
997
+ await strategy.fn();
998
+ // Try to sync after reset with internally time-bounded sync (3 seconds per strategy)
999
+ const syncSuccess = await this.syncWithTimeout(3000);
1000
+ if (syncSuccess) {
1001
+ // Sync succeeded
1002
+ this.logger.log(`Connected successfully with ${strategy.name} reset.`);
1003
+ return;
1004
+ }
1005
+ else {
1006
+ throw new Error("Sync timeout or abandoned");
1007
+ }
1008
+ }
1009
+ catch (error) {
1010
+ lastError = error;
1011
+ this.logger.log(`${strategy.name} reset failed: ${error.message}`);
1012
+ // Set abandon flag to stop any in-flight operations
1013
+ this._abandonCurrentOperation = true;
1014
+ // Wait a bit for in-flight operations to abort
1015
+ await sleep(100);
1016
+ // If port got disconnected, we can't try more strategies
1017
+ if (!this.connected || !this.port.writable) {
1018
+ this.logger.log(`Port disconnected during reset attempt`);
1019
+ break;
1020
+ }
1021
+ // Clear buffers before trying next strategy
1022
+ this._clearInputBuffer();
1023
+ await this.drainInputBuffer(200);
1024
+ await this.flushSerialBuffers();
1025
+ }
1026
+ }
1027
+ // All strategies failed - reset abandon flag before throwing
1028
+ this._abandonCurrentOperation = false;
1029
+ throw new Error(`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
1030
+ }
312
1031
  async hardReset(bootloader = false) {
313
1032
  if (bootloader) {
314
1033
  // enter flash mode
@@ -317,16 +1036,34 @@ export class ESPLoader extends EventTarget {
317
1036
  this.logger.log("USB-JTAG/Serial reset.");
318
1037
  }
319
1038
  else {
320
- await this.hardResetClassic();
321
- this.logger.log("Classic reset.");
1039
+ // Use different reset strategy for WebUSB (Android) vs Web Serial (Desktop)
1040
+ if (this.isWebUSB()) {
1041
+ await this.hardResetClassicWebUSB();
1042
+ this.logger.log("Classic reset (WebUSB/Android).");
1043
+ }
1044
+ else {
1045
+ await this.hardResetClassic();
1046
+ this.logger.log("Classic reset.");
1047
+ }
322
1048
  }
323
1049
  }
324
1050
  else {
325
- // just reset
326
- await this.setRTS(true); // EN->LOW
327
- await this.sleep(100);
328
- await this.setRTS(false);
329
- this.logger.log("Hard reset.");
1051
+ // just reset (no bootloader mode)
1052
+ if (this.isWebUSB()) {
1053
+ // WebUSB: Use longer delays for better compatibility
1054
+ await this.setRTSWebUSB(true); // EN->LOW
1055
+ await this.sleep(200);
1056
+ await this.setRTSWebUSB(false);
1057
+ await this.sleep(200);
1058
+ this.logger.log("Hard reset (WebUSB).");
1059
+ }
1060
+ else {
1061
+ // Web Serial: Standard reset
1062
+ await this.setRTS(true); // EN->LOW
1063
+ await this.sleep(100);
1064
+ await this.setRTS(false);
1065
+ this.logger.log("Hard reset.");
1066
+ }
330
1067
  }
331
1068
  await new Promise((resolve) => setTimeout(resolve, 1000));
332
1069
  }
@@ -451,6 +1188,12 @@ export class ESPLoader extends EventTarget {
451
1188
  else if ([2, 4].includes(data.length)) {
452
1189
  statusLen = data.length;
453
1190
  }
1191
+ else {
1192
+ // Default to 2-byte status if we can't determine
1193
+ // This prevents silent data corruption when statusLen would be 0
1194
+ statusLen = 2;
1195
+ this.logger.debug(`Unknown chip family, defaulting to 2-byte status (opcode: ${toHex(opcode)}, data.length: ${data.length})`);
1196
+ }
454
1197
  }
455
1198
  if (data.length < statusLen) {
456
1199
  throw new Error("Didn't get enough status bytes");
@@ -499,71 +1242,169 @@ export class ESPLoader extends EventTarget {
499
1242
  * @name readPacket
500
1243
  * Generator to read SLIP packets from a serial port.
501
1244
  * Yields one full SLIP packet at a time, raises exception on timeout or invalid data.
1245
+ *
1246
+ * Two implementations:
1247
+ * - Burst: CDC devices (Native USB) and CH343 - very fast processing
1248
+ * - Byte-by-byte: CH340, CP2102, and other USB-Serial adapters - stable fast processing
502
1249
  */
503
1250
  async readPacket(timeout) {
504
1251
  let partialPacket = null;
505
1252
  let inEscape = false;
506
- const startTime = Date.now();
507
- while (true) {
508
- // Check timeout
509
- if (Date.now() - startTime > timeout) {
510
- const waitingFor = partialPacket === null ? "header" : "content";
511
- throw new SlipReadError("Timed out waiting for packet " + waitingFor);
512
- }
513
- // If no data available, wait a bit
514
- if (this._inputBuffer.length === 0) {
515
- await sleep(1);
516
- continue;
517
- }
518
- // Process all available bytes without going back to outer loop
519
- // This is critical for handling high-speed burst transfers
520
- while (this._inputBuffer.length > 0) {
521
- const b = this._inputBuffer.shift();
522
- if (partialPacket === null) {
523
- // waiting for packet header
524
- if (b == 0xc0) {
525
- partialPacket = [];
1253
+ // CDC devices use burst processing, non-CDC use byte-by-byte
1254
+ if (this._isCDCDevice) {
1255
+ // Burst version: Process all available bytes in one pass for ultra-high-speed transfers
1256
+ // Used for: CDC devices (all platforms) and CH343
1257
+ const startTime = Date.now();
1258
+ while (true) {
1259
+ // Check abandon flag (for reset strategy timeout)
1260
+ if (this._abandonCurrentOperation) {
1261
+ throw new SlipReadError("Operation abandoned (reset strategy timeout)");
1262
+ }
1263
+ // Check timeout
1264
+ if (Date.now() - startTime > timeout) {
1265
+ const waitingFor = partialPacket === null ? "header" : "content";
1266
+ throw new SlipReadError("Timed out waiting for packet " + waitingFor);
1267
+ }
1268
+ // If no data available, wait a bit
1269
+ if (this._inputBufferAvailable === 0) {
1270
+ await sleep(1);
1271
+ continue;
1272
+ }
1273
+ // Process all available bytes without going back to outer loop
1274
+ // This is critical for handling high-speed burst transfers
1275
+ while (this._inputBufferAvailable > 0) {
1276
+ // Periodic timeout check to prevent hang on slow data
1277
+ if (Date.now() - startTime > timeout) {
1278
+ const waitingFor = partialPacket === null ? "header" : "content";
1279
+ throw new SlipReadError("Timed out waiting for packet " + waitingFor);
526
1280
  }
527
- else {
528
- if (this.debug) {
529
- this.logger.debug("Read invalid data: " + toHex(b));
530
- this.logger.debug("Remaining data in serial buffer: " +
531
- hexFormatter(this._inputBuffer));
1281
+ const b = this._readByte();
1282
+ if (partialPacket === null) {
1283
+ // waiting for packet header
1284
+ if (b == 0xc0) {
1285
+ partialPacket = [];
1286
+ }
1287
+ else {
1288
+ if (this.debug) {
1289
+ this.logger.debug("Read invalid data: " + toHex(b));
1290
+ this.logger.debug("Remaining data in serial buffer: " +
1291
+ hexFormatter(this._inputBuffer));
1292
+ }
1293
+ throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
532
1294
  }
533
- throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
534
1295
  }
535
- }
536
- else if (inEscape) {
537
- // part-way through escape sequence
538
- inEscape = false;
539
- if (b == 0xdc) {
540
- partialPacket.push(0xc0);
1296
+ else if (inEscape) {
1297
+ // part-way through escape sequence
1298
+ inEscape = false;
1299
+ if (b == 0xdc) {
1300
+ partialPacket.push(0xc0);
1301
+ }
1302
+ else if (b == 0xdd) {
1303
+ partialPacket.push(0xdb);
1304
+ }
1305
+ else {
1306
+ if (this.debug) {
1307
+ this.logger.debug("Read invalid data: " + toHex(b));
1308
+ this.logger.debug("Remaining data in serial buffer: " +
1309
+ hexFormatter(this._inputBuffer));
1310
+ }
1311
+ throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
1312
+ }
541
1313
  }
542
- else if (b == 0xdd) {
543
- partialPacket.push(0xdb);
1314
+ else if (b == 0xdb) {
1315
+ // start of escape sequence
1316
+ inEscape = true;
1317
+ }
1318
+ else if (b == 0xc0) {
1319
+ // end of packet
1320
+ if (this.debug)
1321
+ this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
1322
+ // Compact buffer periodically to prevent memory growth
1323
+ this._compactInputBuffer();
1324
+ return partialPacket;
544
1325
  }
545
1326
  else {
546
- if (this.debug) {
547
- this.logger.debug("Read invalid data: " + toHex(b));
548
- this.logger.debug("Remaining data in serial buffer: " +
549
- hexFormatter(this._inputBuffer));
550
- }
551
- throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
1327
+ // normal byte in packet
1328
+ partialPacket.push(b);
552
1329
  }
553
1330
  }
554
- else if (b == 0xdb) {
555
- // start of escape sequence
556
- inEscape = true;
1331
+ }
1332
+ }
1333
+ else {
1334
+ // Byte-by-byte version: Stable for non CDC USB-Serial adapters (CH340, CP2102, etc.)
1335
+ let readBytes = [];
1336
+ while (true) {
1337
+ // Check abandon flag (for reset strategy timeout)
1338
+ if (this._abandonCurrentOperation) {
1339
+ throw new SlipReadError("Operation abandoned (reset strategy timeout)");
557
1340
  }
558
- else if (b == 0xc0) {
559
- // end of packet
560
- if (this.debug)
561
- this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
562
- return partialPacket;
1341
+ const stamp = Date.now();
1342
+ readBytes = [];
1343
+ while (Date.now() - stamp < timeout) {
1344
+ if (this._inputBufferAvailable > 0) {
1345
+ readBytes.push(this._readByte());
1346
+ break;
1347
+ }
1348
+ else {
1349
+ // Reduced sleep time for faster response during high-speed transfers
1350
+ await sleep(1);
1351
+ }
563
1352
  }
564
- else {
565
- // normal byte in packet
566
- partialPacket.push(b);
1353
+ if (readBytes.length == 0) {
1354
+ const waitingFor = partialPacket === null ? "header" : "content";
1355
+ throw new SlipReadError("Timed out waiting for packet " + waitingFor);
1356
+ }
1357
+ if (this.debug)
1358
+ this.logger.debug("Read " + readBytes.length + " bytes: " + hexFormatter(readBytes));
1359
+ for (const b of readBytes) {
1360
+ if (partialPacket === null) {
1361
+ // waiting for packet header
1362
+ if (b == 0xc0) {
1363
+ partialPacket = [];
1364
+ }
1365
+ else {
1366
+ if (this.debug) {
1367
+ this.logger.debug("Read invalid data: " + toHex(b));
1368
+ this.logger.debug("Remaining data in serial buffer: " +
1369
+ hexFormatter(this._inputBuffer));
1370
+ }
1371
+ throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
1372
+ }
1373
+ }
1374
+ else if (inEscape) {
1375
+ // part-way through escape sequence
1376
+ inEscape = false;
1377
+ if (b == 0xdc) {
1378
+ partialPacket.push(0xc0);
1379
+ }
1380
+ else if (b == 0xdd) {
1381
+ partialPacket.push(0xdb);
1382
+ }
1383
+ else {
1384
+ if (this.debug) {
1385
+ this.logger.debug("Read invalid data: " + toHex(b));
1386
+ this.logger.debug("Remaining data in serial buffer: " +
1387
+ hexFormatter(this._inputBuffer));
1388
+ }
1389
+ throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
1390
+ }
1391
+ }
1392
+ else if (b == 0xdb) {
1393
+ // start of escape sequence
1394
+ inEscape = true;
1395
+ }
1396
+ else if (b == 0xc0) {
1397
+ // end of packet
1398
+ if (this.debug)
1399
+ this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
1400
+ // Compact buffer periodically to prevent memory growth
1401
+ this._compactInputBuffer();
1402
+ return partialPacket;
1403
+ }
1404
+ else {
1405
+ // normal byte in packet
1406
+ partialPacket.push(b);
1407
+ }
567
1408
  }
568
1409
  }
569
1410
  }
@@ -595,7 +1436,7 @@ export class ESPLoader extends EventTarget {
595
1436
  throw new Error(`Invalid (unsupported) command ${toHex(opcode)}`);
596
1437
  }
597
1438
  }
598
- throw "Response doesn't match request";
1439
+ throw new Error("Response doesn't match request");
599
1440
  }
600
1441
  /**
601
1442
  * @name checksum
@@ -647,8 +1488,9 @@ export class ESPLoader extends EventTarget {
647
1488
  }
648
1489
  async reconfigurePort(baud) {
649
1490
  var _a;
1491
+ // Block new writes during the entire reconfiguration (all paths)
1492
+ this._isReconfiguring = true;
650
1493
  try {
651
- this._isReconfiguring = true;
652
1494
  // Wait for pending writes to complete
653
1495
  try {
654
1496
  await this._writeChain;
@@ -656,6 +1498,30 @@ export class ESPLoader extends EventTarget {
656
1498
  catch (err) {
657
1499
  this.logger.debug(`Pending write error during reconfigure: ${err}`);
658
1500
  }
1501
+ // WebUSB: Check if we should use setBaudRate() or close/reopen
1502
+ if (this.isWebUSB()) {
1503
+ const portInfo = this.port.getInfo();
1504
+ const isCH343 = portInfo.usbVendorId === 0x1a86 && portInfo.usbProductId === 0x55d3;
1505
+ // CH343 is a CDC device and MUST use close/reopen
1506
+ // Other chips (CH340, CP2102, FTDI) MUST use setBaudRate()
1507
+ if (!isCH343 &&
1508
+ typeof this.port.setBaudRate === "function") {
1509
+ // this.logger.log(
1510
+ // `[WebUSB] Changing baudrate to ${baud} using setBaudRate()...`,
1511
+ // );
1512
+ await this.port.setBaudRate(baud);
1513
+ // this.logger.log(`[WebUSB] Baudrate changed to ${baud}`);
1514
+ // Give the chip time to adjust to new baudrate
1515
+ await sleep(100);
1516
+ return;
1517
+ }
1518
+ else if (isCH343) {
1519
+ // this.logger.log(
1520
+ // `[WebUSB] CH343 detected - using close/reopen for baudrate change`,
1521
+ // );
1522
+ }
1523
+ }
1524
+ // Web Serial or CH343: Close and reopen port
659
1525
  // Release persistent writer before closing
660
1526
  if (this._writer) {
661
1527
  try {
@@ -684,110 +1550,44 @@ export class ESPLoader extends EventTarget {
684
1550
  throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
685
1551
  }
686
1552
  finally {
1553
+ // Always reset flag, even on error or early return
687
1554
  this._isReconfiguring = false;
688
1555
  }
689
1556
  }
690
1557
  /**
691
- * @name connectWithResetStrategies
692
- * Try different reset strategies to enter bootloader mode
693
- * Similar to esptool.py's connect() method with multiple reset strategies
1558
+ * @name syncWithTimeout
1559
+ * Sync with timeout that can be abandoned (for reset strategy loop)
1560
+ * This is internally time-bounded and checks the abandon flag
694
1561
  */
695
- async connectWithResetStrategies() {
696
- var _a, _b;
697
- const portInfo = this.port.getInfo();
698
- const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
699
- const isEspressifUSB = portInfo.usbVendorId === 0x303a;
700
- 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"}`);
701
- // Define reset strategies to try in order
702
- const resetStrategies = [];
703
- // Strategy 1: USB-JTAG/Serial reset (for ESP32-C3, C6, S3, etc.)
704
- // Try this first if we detect Espressif USB VID or the specific PID
705
- if (isUSBJTAGSerial || isEspressifUSB) {
706
- resetStrategies.push({
707
- name: "USB-JTAG/Serial",
708
- fn: async () => await this.hardResetUSBJTAGSerial(),
709
- });
710
- }
711
- // Strategy 2: Classic reset (for USB-to-Serial bridges)
712
- resetStrategies.push({
713
- name: "Classic",
714
- fn: async () => await this.hardResetClassic(),
715
- });
716
- // Strategy 3: If USB-JTAG/Serial was not tried yet, try it as fallback
717
- if (!isUSBJTAGSerial && !isEspressifUSB) {
718
- resetStrategies.push({
719
- name: "USB-JTAG/Serial (fallback)",
720
- fn: async () => await this.hardResetUSBJTAGSerial(),
721
- });
722
- }
723
- let lastError = null;
724
- // Try each reset strategy
725
- for (const strategy of resetStrategies) {
1562
+ async syncWithTimeout(timeoutMs) {
1563
+ const startTime = Date.now();
1564
+ for (let i = 0; i < 5; i++) {
1565
+ // Check if we've exceeded the timeout
1566
+ if (Date.now() - startTime > timeoutMs) {
1567
+ return false;
1568
+ }
1569
+ // Check abandon flag
1570
+ if (this._abandonCurrentOperation) {
1571
+ return false;
1572
+ }
1573
+ this._clearInputBuffer();
726
1574
  try {
727
- this.logger.log(`Trying ${strategy.name} reset...`);
728
- // Check if port is still open, if not, skip this strategy
729
- if (!this.connected || !this.port.writable) {
730
- this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
731
- continue;
1575
+ const response = await this._sync();
1576
+ if (response) {
1577
+ await sleep(SYNC_TIMEOUT);
1578
+ return true;
732
1579
  }
733
- await strategy.fn();
734
- // Try to sync after reset
735
- await this.sync();
736
- // If we get here, sync succeeded
737
- this.logger.log(`Connected successfully with ${strategy.name} reset.`);
738
- return;
1580
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
739
1581
  }
740
- catch (error) {
741
- lastError = error;
742
- this.logger.log(`${strategy.name} reset failed: ${error.message}`);
743
- // If port got disconnected, we can't try more strategies
744
- if (!this.connected || !this.port.writable) {
745
- this.logger.log(`Port disconnected during reset attempt`);
746
- break;
1582
+ catch (e) {
1583
+ // Check abandon flag after error
1584
+ if (this._abandonCurrentOperation) {
1585
+ return false;
747
1586
  }
748
- // Clear buffers before trying next strategy
749
- this._inputBuffer.length = 0;
750
- await this.drainInputBuffer(200);
751
- await this.flushSerialBuffers();
752
1587
  }
1588
+ await sleep(SYNC_TIMEOUT);
753
1589
  }
754
- // All strategies failed
755
- throw new Error(`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
756
- }
757
- /**
758
- * @name hardResetUSBJTAGSerial
759
- * USB-JTAG/Serial reset sequence for ESP32-C3, ESP32-S3, ESP32-C6, etc.
760
- */
761
- async hardResetUSBJTAGSerial() {
762
- await this.setRTS(false);
763
- await this.setDTR(false); // Idle
764
- await this.sleep(100);
765
- await this.setDTR(true); // Set IO0
766
- await this.setRTS(false);
767
- await this.sleep(100);
768
- await this.setRTS(true); // Reset. Calls inverted to go through (1,1) instead of (0,0)
769
- await this.setDTR(false);
770
- await this.setRTS(true); // RTS set as Windows only propagates DTR on RTS setting
771
- await this.sleep(100);
772
- await this.setDTR(false);
773
- await this.setRTS(false); // Chip out of reset
774
- // Wait for chip to boot into bootloader
775
- await this.sleep(200);
776
- }
777
- /**
778
- * @name hardResetClassic
779
- * Classic reset sequence for USB-to-Serial bridge chips (CH340, CP2102, etc.)
780
- */
781
- async hardResetClassic() {
782
- await this.setDTR(false); // IO0=HIGH
783
- await this.setRTS(true); // EN=LOW, chip in reset
784
- await this.sleep(100);
785
- await this.setDTR(true); // IO0=LOW
786
- await this.setRTS(false); // EN=HIGH, chip out of reset
787
- await this.sleep(50);
788
- await this.setDTR(false); // IO0=HIGH, done
789
- // Wait for chip to boot into bootloader
790
- await this.sleep(200);
1590
+ return false;
791
1591
  }
792
1592
  /**
793
1593
  * @name sync
@@ -796,7 +1596,7 @@ export class ESPLoader extends EventTarget {
796
1596
  */
797
1597
  async sync() {
798
1598
  for (let i = 0; i < 5; i++) {
799
- this._inputBuffer.length = 0;
1599
+ this._clearInputBuffer();
800
1600
  const response = await this._sync();
801
1601
  if (response) {
802
1602
  await sleep(SYNC_TIMEOUT);
@@ -820,8 +1620,10 @@ export class ESPLoader extends EventTarget {
820
1620
  return true;
821
1621
  }
822
1622
  }
823
- catch {
824
- // If read packet fails.
1623
+ catch (e) {
1624
+ if (this.debug) {
1625
+ this.logger.debug(`Sync attempt ${i + 1} failed: ${e}`);
1626
+ }
825
1627
  }
826
1628
  }
827
1629
  return false;
@@ -1296,12 +2098,19 @@ export class ESPLoader extends EventTarget {
1296
2098
  await this._writer.write(new Uint8Array(data));
1297
2099
  }, async () => {
1298
2100
  // Previous write failed, but still attempt this write
2101
+ this.logger.debug("Previous write failed, attempting recovery for current write");
1299
2102
  if (!this.port.writable) {
1300
2103
  throw new Error("Port became unavailable during write");
1301
2104
  }
1302
2105
  // Writer was likely cleaned up by previous error, create new one
1303
2106
  if (!this._writer) {
1304
- this._writer = this.port.writable.getWriter();
2107
+ try {
2108
+ this._writer = this.port.writable.getWriter();
2109
+ }
2110
+ catch (err) {
2111
+ this.logger.debug(`Failed to get writer in recovery: ${err}`);
2112
+ throw new Error("Cannot acquire writer lock");
2113
+ }
1305
2114
  }
1306
2115
  await this._writer.write(new Uint8Array(data));
1307
2116
  })
@@ -1312,7 +2121,7 @@ export class ESPLoader extends EventTarget {
1312
2121
  try {
1313
2122
  this._writer.releaseLock();
1314
2123
  }
1315
- catch (e) {
2124
+ catch {
1316
2125
  // Ignore release errors
1317
2126
  }
1318
2127
  this._writer = undefined;
@@ -1332,49 +2141,69 @@ export class ESPLoader extends EventTarget {
1332
2141
  this.logger.debug("Port already closed, skipping disconnect");
1333
2142
  return;
1334
2143
  }
2144
+ // Wait for pending writes to complete
1335
2145
  try {
1336
- this._isReconfiguring = true;
1337
- // Wait for pending writes to complete
2146
+ await this._writeChain;
2147
+ }
2148
+ catch (err) {
2149
+ this.logger.debug(`Pending write error during disconnect: ${err}`);
2150
+ }
2151
+ // Release persistent writer before closing
2152
+ if (this._writer) {
1338
2153
  try {
1339
- await this._writeChain;
2154
+ await this._writer.close();
2155
+ this._writer.releaseLock();
1340
2156
  }
1341
2157
  catch (err) {
1342
- this.logger.debug(`Pending write error during disconnect: ${err}`);
2158
+ this.logger.debug(`Writer close/release error: ${err}`);
1343
2159
  }
1344
- // Release persistent writer before closing
1345
- if (this._writer) {
1346
- try {
1347
- await this._writer.close();
1348
- this._writer.releaseLock();
1349
- }
1350
- catch (err) {
1351
- this.logger.debug(`Writer close/release error: ${err}`);
1352
- }
1353
- this._writer = undefined;
2160
+ this._writer = undefined;
2161
+ }
2162
+ else {
2163
+ // No persistent writer exists, close stream directly
2164
+ // This path is taken when no writes have been queued
2165
+ try {
2166
+ const writer = this.port.writable.getWriter();
2167
+ await writer.close();
2168
+ writer.releaseLock();
1354
2169
  }
1355
- else {
1356
- // No persistent writer exists, close stream directly
1357
- // This path is taken when no writes have been queued
1358
- try {
1359
- const writer = this.port.writable.getWriter();
1360
- await writer.close();
1361
- writer.releaseLock();
1362
- }
1363
- catch (err) {
1364
- this.logger.debug(`Direct writer close error: ${err}`);
1365
- }
2170
+ catch (err) {
2171
+ this.logger.debug(`Direct writer close error: ${err}`);
1366
2172
  }
1367
- await new Promise((resolve) => {
1368
- if (!this._reader) {
1369
- resolve(undefined);
1370
- }
1371
- this.addEventListener("disconnect", resolve, { once: true });
2173
+ }
2174
+ await new Promise((resolve) => {
2175
+ if (!this._reader) {
2176
+ resolve(undefined);
2177
+ return;
2178
+ }
2179
+ // Set a timeout to prevent hanging (important for node-usb)
2180
+ const timeout = setTimeout(() => {
2181
+ this.logger.debug("Disconnect timeout - forcing resolution");
2182
+ resolve(undefined);
2183
+ }, 1000);
2184
+ this.addEventListener("disconnect", () => {
2185
+ clearTimeout(timeout);
2186
+ resolve(undefined);
2187
+ }, { once: true });
2188
+ // Only cancel if reader is still active
2189
+ try {
1372
2190
  this._reader.cancel();
1373
- });
1374
- this.connected = false;
2191
+ }
2192
+ catch (err) {
2193
+ this.logger.debug(`Reader cancel error: ${err}`);
2194
+ // Reader already released, resolve immediately
2195
+ clearTimeout(timeout);
2196
+ resolve(undefined);
2197
+ }
2198
+ });
2199
+ this.connected = false;
2200
+ // Close the port (important for node-usb adapter)
2201
+ try {
2202
+ await this.port.close();
2203
+ this.logger.debug("Port closed successfully");
1375
2204
  }
1376
- finally {
1377
- this._isReconfiguring = false;
2205
+ catch (err) {
2206
+ this.logger.debug(`Port close error: ${err}`);
1378
2207
  }
1379
2208
  }
1380
2209
  /**
@@ -1387,10 +2216,10 @@ export class ESPLoader extends EventTarget {
1387
2216
  return;
1388
2217
  }
1389
2218
  try {
1390
- this._isReconfiguring = true;
1391
2219
  this.logger.log("Reconnecting serial port...");
1392
2220
  this.connected = false;
1393
2221
  this.__inputBuffer = [];
2222
+ this.__inputBufferReadIndex = 0;
1394
2223
  // Wait for pending writes to complete
1395
2224
  try {
1396
2225
  await this._writeChain;
@@ -1398,6 +2227,8 @@ export class ESPLoader extends EventTarget {
1398
2227
  catch (err) {
1399
2228
  this.logger.debug(`Pending write error during reconnect: ${err}`);
1400
2229
  }
2230
+ // Block new writes during port close/open
2231
+ this._isReconfiguring = true;
1401
2232
  // Release persistent writer
1402
2233
  if (this._writer) {
1403
2234
  try {
@@ -1439,6 +2270,8 @@ export class ESPLoader extends EventTarget {
1439
2270
  if (!this.port.readable || !this.port.writable) {
1440
2271
  throw new Error(`Port streams not available after open (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
1441
2272
  }
2273
+ // Port is now open and ready - allow writes for initialization
2274
+ this._isReconfiguring = false;
1442
2275
  // Save chip info and flash size (no need to detect again)
1443
2276
  const savedChipFamily = this.chipFamily;
1444
2277
  const savedChipName = this.chipName;
@@ -1449,6 +2282,7 @@ export class ESPLoader extends EventTarget {
1449
2282
  await this.hardReset(true);
1450
2283
  if (!this._parent) {
1451
2284
  this.__inputBuffer = [];
2285
+ this.__inputBufferReadIndex = 0;
1452
2286
  this.__totalBytesRead = 0;
1453
2287
  this.readLoop();
1454
2288
  }
@@ -1476,14 +2310,16 @@ export class ESPLoader extends EventTarget {
1476
2310
  throw new Error(`Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
1477
2311
  }
1478
2312
  }
1479
- // Copy stub state to this instance if we're a stub loader
1480
- if (this.IS_STUB) {
1481
- Object.assign(this, stubLoader);
1482
- }
2313
+ // The stub is now running on the chip
2314
+ // stubLoader has this instance as _parent, so all operations go through this
2315
+ // We just need to mark this instance as running stub code
2316
+ this.IS_STUB = true;
1483
2317
  this.logger.debug("Reconnection successful");
1484
2318
  }
1485
- finally {
2319
+ catch (err) {
2320
+ // Ensure flag is reset on error
1486
2321
  this._isReconfiguring = false;
2322
+ throw err;
1487
2323
  }
1488
2324
  }
1489
2325
  /**
@@ -1509,8 +2345,8 @@ export class ESPLoader extends EventTarget {
1509
2345
  const drainStart = Date.now();
1510
2346
  const drainTimeout = 100; // Short timeout for draining
1511
2347
  while (drained < bytesToDrain && Date.now() - drainStart < drainTimeout) {
1512
- if (this._inputBuffer.length > 0) {
1513
- const byte = this._inputBuffer.shift();
2348
+ if (this._inputBufferAvailable > 0) {
2349
+ const byte = this._readByte();
1514
2350
  if (byte !== undefined) {
1515
2351
  drained++;
1516
2352
  }
@@ -1526,6 +2362,7 @@ export class ESPLoader extends EventTarget {
1526
2362
  // Final clear of application buffer
1527
2363
  if (!this._parent) {
1528
2364
  this.__inputBuffer = [];
2365
+ this.__inputBufferReadIndex = 0;
1529
2366
  }
1530
2367
  }
1531
2368
  /**
@@ -1537,12 +2374,14 @@ export class ESPLoader extends EventTarget {
1537
2374
  // Clear application buffer
1538
2375
  if (!this._parent) {
1539
2376
  this.__inputBuffer = [];
2377
+ this.__inputBufferReadIndex = 0;
1540
2378
  }
1541
2379
  // Wait for any pending data
1542
2380
  await sleep(SYNC_TIMEOUT);
1543
2381
  // Final clear
1544
2382
  if (!this._parent) {
1545
2383
  this.__inputBuffer = [];
2384
+ this.__inputBufferReadIndex = 0;
1546
2385
  }
1547
2386
  this.logger.debug("Serial buffers flushed");
1548
2387
  }
@@ -1561,7 +2400,35 @@ export class ESPLoader extends EventTarget {
1561
2400
  // Flush serial buffers before flash read operation
1562
2401
  await this.flushSerialBuffers();
1563
2402
  this.logger.log(`Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`);
1564
- const CHUNK_SIZE = 0x10000; // 64KB chunks
2403
+ // Initialize adaptive speed multipliers for WebUSB devices
2404
+ if (this.isWebUSB()) {
2405
+ if (this._isCDCDevice) {
2406
+ // CDC devices (CH343): Start with maximum, adaptive adjustment enabled
2407
+ this._adaptiveBlockMultiplier = 8; // blockSize = 248 bytes
2408
+ this._adaptiveMaxInFlightMultiplier = 8; // maxInFlight = 248 bytes
2409
+ this._consecutiveSuccessfulChunks = 0;
2410
+ this.logger.debug(`CDC device - Initialized: blockMultiplier=${this._adaptiveBlockMultiplier}, maxInFlightMultiplier=${this._adaptiveMaxInFlightMultiplier}`);
2411
+ }
2412
+ else {
2413
+ // Non-CDC devices (CH340, CP2102): Fixed values, no adaptive adjustment
2414
+ this._adaptiveBlockMultiplier = 1; // blockSize = 31 bytes (fixed)
2415
+ this._adaptiveMaxInFlightMultiplier = 1; // maxInFlight = 31 bytes (fixed)
2416
+ this._consecutiveSuccessfulChunks = 0;
2417
+ this.logger.debug(`Non-CDC device - Fixed values: blockSize=31, maxInFlight=31`);
2418
+ }
2419
+ }
2420
+ // Chunk size: Amount of data to request from ESP in one command
2421
+ // For WebUSB (Android), use smaller chunks to avoid timeouts and buffer issues
2422
+ // For Web Serial (Desktop), use larger chunks for better performance
2423
+ let CHUNK_SIZE;
2424
+ if (this.isWebUSB()) {
2425
+ // WebUSB: Use smaller chunks to avoid SLIP timeout issues
2426
+ CHUNK_SIZE = 0x4 * 0x1000; // 4KB = 16384 bytes
2427
+ }
2428
+ else {
2429
+ // Web Serial: Use larger chunks for better performance
2430
+ CHUNK_SIZE = 0x40 * 0x1000;
2431
+ }
1565
2432
  let allData = new Uint8Array(0);
1566
2433
  let currentAddr = addr;
1567
2434
  let remainingSize = size;
@@ -1569,18 +2436,35 @@ export class ESPLoader extends EventTarget {
1569
2436
  const chunkSize = Math.min(CHUNK_SIZE, remainingSize);
1570
2437
  let chunkSuccess = false;
1571
2438
  let retryCount = 0;
1572
- const MAX_RETRIES = 15;
2439
+ const MAX_RETRIES = 5;
2440
+ let deepRecoveryAttempted = false;
1573
2441
  // Retry loop for this chunk
1574
2442
  while (!chunkSuccess && retryCount <= MAX_RETRIES) {
1575
2443
  let resp = new Uint8Array(0);
2444
+ let lastAckedLength = 0; // Track last acknowledged length
1576
2445
  try {
1577
2446
  // Only log on first attempt or retries
1578
2447
  if (retryCount === 0) {
1579
2448
  this.logger.debug(`Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`);
1580
2449
  }
1581
- // Send read flash command for this chunk
1582
- // This must be inside the retry loop so we send a fresh command after errors
1583
- const pkt = pack("<IIII", currentAddr, chunkSize, 0x1000, 1024);
2450
+ let blockSize;
2451
+ let maxInFlight;
2452
+ if (this.isWebUSB()) {
2453
+ // WebUSB (Android): All devices use adaptive speed
2454
+ // All have maxTransferSize=64, baseBlockSize=31
2455
+ const maxTransferSize = this.port.maxTransferSize || 64;
2456
+ const baseBlockSize = Math.floor((maxTransferSize - 2) / 2); // 31 bytes
2457
+ // Use current adaptive multipliers (initialized at start of readFlash)
2458
+ blockSize = baseBlockSize * this._adaptiveBlockMultiplier;
2459
+ maxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
2460
+ }
2461
+ else {
2462
+ // Web Serial (Desktop): Use multiples of 63 for consistency
2463
+ const base = 63;
2464
+ blockSize = base * 65; // 63 * 65 = 4095 (close to 0x1000)
2465
+ maxInFlight = base * 130; // 63 * 130 = 8190 (close to blockSize * 2)
2466
+ }
2467
+ const pkt = pack("<IIII", currentAddr, chunkSize, blockSize, maxInFlight);
1584
2468
  const [res] = await this.checkCommand(ESP_READ_FLASH, pkt);
1585
2469
  if (res != 0) {
1586
2470
  throw new Error("Failed to read memory: " + res);
@@ -1623,10 +2507,19 @@ export class ESPLoader extends EventTarget {
1623
2507
  newResp.set(resp);
1624
2508
  newResp.set(packetData, resp.length);
1625
2509
  resp = newResp;
1626
- // Send acknowledgment
1627
- const ackData = pack("<I", resp.length);
1628
- const slipEncodedAck = slipEncode(ackData);
1629
- await this.writeToStream(slipEncodedAck);
2510
+ // Send acknowledgment when we've received maxInFlight bytes
2511
+ // The stub sends packets until (num_sent - num_acked) >= max_in_flight
2512
+ // We MUST wait for all packets before sending ACK
2513
+ const shouldAck = resp.length >= chunkSize || // End of chunk
2514
+ resp.length >= lastAckedLength + maxInFlight; // Received all packets
2515
+ if (shouldAck) {
2516
+ const ackData = pack("<I", resp.length);
2517
+ const slipEncodedAck = slipEncode(ackData);
2518
+ await this.writeToStream(slipEncodedAck);
2519
+ // Update lastAckedLength to current response length
2520
+ // This ensures next ACK is sent at the right time
2521
+ lastAckedLength = resp.length;
2522
+ }
1630
2523
  }
1631
2524
  }
1632
2525
  // Chunk read successfully - append to all data
@@ -1635,9 +2528,62 @@ export class ESPLoader extends EventTarget {
1635
2528
  newAllData.set(resp, allData.length);
1636
2529
  allData = newAllData;
1637
2530
  chunkSuccess = true;
2531
+ // ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
2532
+ // Non-CDC devices (CH340, CP2102) stay at fixed blockSize=31, maxInFlight=31
2533
+ if (this.isWebUSB() && this._isCDCDevice && retryCount === 0) {
2534
+ this._consecutiveSuccessfulChunks++;
2535
+ // After 2 consecutive successful chunks, increase speed gradually
2536
+ if (this._consecutiveSuccessfulChunks >= 2) {
2537
+ const maxTransferSize = this.port.maxTransferSize || 64;
2538
+ const baseBlockSize = Math.floor((maxTransferSize - 2) / 2); // 31 bytes
2539
+ // Maximum: blockSize=248 (8 * 31), maxInFlight=248 (8 * 31)
2540
+ const MAX_BLOCK_MULTIPLIER = 8; // 248 bytes - tested stable
2541
+ const MAX_INFLIGHT_MULTIPLIER = 8; // 248 bytes - tested stable
2542
+ let adjusted = false;
2543
+ // Increase blockSize first (up to 248), then maxInFlight
2544
+ if (this._adaptiveBlockMultiplier < MAX_BLOCK_MULTIPLIER) {
2545
+ this._adaptiveBlockMultiplier = Math.min(this._adaptiveBlockMultiplier * 2, MAX_BLOCK_MULTIPLIER);
2546
+ adjusted = true;
2547
+ }
2548
+ // Once blockSize is at maximum, increase maxInFlight
2549
+ else if (this._adaptiveMaxInFlightMultiplier < MAX_INFLIGHT_MULTIPLIER) {
2550
+ this._adaptiveMaxInFlightMultiplier = Math.min(this._adaptiveMaxInFlightMultiplier * 2, MAX_INFLIGHT_MULTIPLIER);
2551
+ adjusted = true;
2552
+ }
2553
+ if (adjusted) {
2554
+ const newBlockSize = baseBlockSize * this._adaptiveBlockMultiplier;
2555
+ const newMaxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
2556
+ this.logger.debug(`Speed increased: blockSize=${newBlockSize}, maxInFlight=${newMaxInFlight}`);
2557
+ this._lastAdaptiveAdjustment = Date.now();
2558
+ }
2559
+ // Reset counter
2560
+ this._consecutiveSuccessfulChunks = 0;
2561
+ }
2562
+ }
1638
2563
  }
1639
2564
  catch (err) {
1640
2565
  retryCount++;
2566
+ // ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
2567
+ // Non-CDC devices stay at fixed values
2568
+ if (this.isWebUSB() && this._isCDCDevice && retryCount === 1) {
2569
+ // Only reduce if we're above minimum
2570
+ if (this._adaptiveBlockMultiplier > 1 ||
2571
+ this._adaptiveMaxInFlightMultiplier > 1) {
2572
+ // Reduce to minimum on error
2573
+ this._adaptiveBlockMultiplier = 1; // 31 bytes (for CH343)
2574
+ this._adaptiveMaxInFlightMultiplier = 1; // 31 bytes
2575
+ this._consecutiveSuccessfulChunks = 0; // Reset success counter
2576
+ const maxTransferSize = this.port.maxTransferSize || 64;
2577
+ const baseBlockSize = Math.floor((maxTransferSize - 2) / 2);
2578
+ const newBlockSize = baseBlockSize * this._adaptiveBlockMultiplier;
2579
+ const newMaxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
2580
+ this.logger.debug(`Error at higher speed - reduced to minimum: blockSize=${newBlockSize}, maxInFlight=${newMaxInFlight}`);
2581
+ }
2582
+ else {
2583
+ // Already at minimum and still failing - this is a real error
2584
+ this.logger.debug(`Error at minimum speed (blockSize=31, maxInFlight=31) - not a speed issue`);
2585
+ }
2586
+ }
1641
2587
  // Check if it's a timeout error or SLIP error
1642
2588
  if (err instanceof SlipReadError) {
1643
2589
  if (retryCount <= MAX_RETRIES) {
@@ -1655,7 +2601,27 @@ export class ESPLoader extends EventTarget {
1655
2601
  }
1656
2602
  }
1657
2603
  else {
1658
- throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries: ${err}`);
2604
+ // All retries exhausted - attempt recovery by reloading stub
2605
+ // IMPORTANT: Do NOT close port to keep ESP32 in bootloader mode
2606
+ if (!deepRecoveryAttempted) {
2607
+ deepRecoveryAttempted = true;
2608
+ this.logger.log(`All retries exhausted at 0x${currentAddr.toString(16)}. Attempting recovery (close and reopen port)...`);
2609
+ try {
2610
+ // Reconnect will close port, reopen, and reload stub
2611
+ await this.reconnect();
2612
+ this.logger.log("Deep recovery successful. Resuming read from current position...");
2613
+ // Reset retry counter to give it another chance after recovery
2614
+ retryCount = 0;
2615
+ continue;
2616
+ }
2617
+ catch (recoveryErr) {
2618
+ throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery failed: ${recoveryErr}`);
2619
+ }
2620
+ }
2621
+ else {
2622
+ // Recovery already attempted, give up
2623
+ throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery attempt`);
2624
+ }
1659
2625
  }
1660
2626
  }
1661
2627
  else {
@@ -1672,7 +2638,6 @@ export class ESPLoader extends EventTarget {
1672
2638
  remainingSize -= chunkSize;
1673
2639
  this.logger.debug(`Total progress: 0x${allData.length.toString(16)} from 0x${size.toString(16)} bytes`);
1674
2640
  }
1675
- this.logger.debug(`Successfully read ${allData.length} bytes from flash`);
1676
2641
  return allData;
1677
2642
  }
1678
2643
  }
@@ -1720,10 +2685,50 @@ class EspStubLoader extends ESPLoader {
1720
2685
  return [0, []];
1721
2686
  }
1722
2687
  /**
1723
- * @name getEraseSize
1724
- * depending on flash chip model the erase may take this long (maybe longer!)
2688
+ * @name eraseFlash
2689
+ * Erase entire flash chip
1725
2690
  */
1726
2691
  async eraseFlash() {
1727
2692
  await this.checkCommand(ESP_ERASE_FLASH, [], 0, CHIP_ERASE_TIMEOUT);
1728
2693
  }
2694
+ /**
2695
+ * @name eraseRegion
2696
+ * Erase a specific region of flash
2697
+ */
2698
+ async eraseRegion(offset, size) {
2699
+ // Validate inputs
2700
+ if (offset < 0) {
2701
+ throw new Error(`Invalid offset: ${offset} (must be non-negative)`);
2702
+ }
2703
+ if (size < 0) {
2704
+ throw new Error(`Invalid size: ${size} (must be non-negative)`);
2705
+ }
2706
+ // No-op for zero size
2707
+ if (size === 0) {
2708
+ this.logger.log("eraseRegion: size is 0, skipping erase");
2709
+ return;
2710
+ }
2711
+ // Check for sector alignment
2712
+ if (offset % FLASH_SECTOR_SIZE !== 0) {
2713
+ throw new Error(`Offset ${offset} (0x${offset.toString(16)}) is not aligned to flash sector size ${FLASH_SECTOR_SIZE} (0x${FLASH_SECTOR_SIZE.toString(16)})`);
2714
+ }
2715
+ if (size % FLASH_SECTOR_SIZE !== 0) {
2716
+ throw new Error(`Size ${size} (0x${size.toString(16)}) is not aligned to flash sector size ${FLASH_SECTOR_SIZE} (0x${FLASH_SECTOR_SIZE.toString(16)})`);
2717
+ }
2718
+ // Check for reasonable bounds (prevent wrapping in pack)
2719
+ const maxValue = 0xffffffff; // 32-bit unsigned max
2720
+ if (offset > maxValue) {
2721
+ throw new Error(`Offset ${offset} exceeds maximum value ${maxValue}`);
2722
+ }
2723
+ if (size > maxValue) {
2724
+ throw new Error(`Size ${size} exceeds maximum value ${maxValue}`);
2725
+ }
2726
+ // Check for wrap-around
2727
+ if (offset + size > maxValue) {
2728
+ throw new Error(`Region end (offset + size = ${offset + size}) exceeds maximum addressable range ${maxValue}`);
2729
+ }
2730
+ const timeout = timeoutPerMb(ERASE_REGION_TIMEOUT_PER_MB, size);
2731
+ const buffer = pack("<II", offset, size);
2732
+ await this.checkCommand(ESP_ERASE_REGION, buffer, 0, timeout);
2733
+ }
1729
2734
  }