tasmota-webserial-esptool 7.3.3 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,7 @@
2
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";
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) {
@@ -25,12 +24,58 @@ export class ESPLoader extends EventTarget {
25
24
  this._initializationSucceeded = false;
26
25
  this.__commandLock = Promise.resolve([0, []]);
27
26
  this.__isReconfiguring = false;
27
+ this.__abandonCurrentOperation = false;
28
+ // Adaptive speed adjustment for flash read operations - DISABLED
29
+ // Using fixed conservative values that work reliably
30
+ this.__adaptiveBlockMultiplier = 1;
31
+ this.__adaptiveMaxInFlightMultiplier = 1;
32
+ this.__consecutiveSuccessfulChunks = 0;
33
+ this.__lastAdaptiveAdjustment = 0;
34
+ this.__isCDCDevice = false;
28
35
  this.state_DTR = false;
29
36
  this.__writeChain = Promise.resolve();
30
37
  }
31
38
  get _inputBuffer() {
32
39
  return this._parent ? this._parent._inputBuffer : this.__inputBuffer;
33
40
  }
41
+ get _inputBufferReadIndex() {
42
+ return this._parent
43
+ ? this._parent._inputBufferReadIndex
44
+ : this.__inputBufferReadIndex || 0;
45
+ }
46
+ set _inputBufferReadIndex(value) {
47
+ if (this._parent) {
48
+ this._parent._inputBufferReadIndex = value;
49
+ }
50
+ else {
51
+ this.__inputBufferReadIndex = value;
52
+ }
53
+ }
54
+ // Get available bytes in buffer (from read index to end)
55
+ get _inputBufferAvailable() {
56
+ return this._inputBuffer.length - this._inputBufferReadIndex;
57
+ }
58
+ // Read one byte from buffer (ring-buffer style with index pointer)
59
+ _readByte() {
60
+ if (this._inputBufferReadIndex >= this._inputBuffer.length) {
61
+ return undefined;
62
+ }
63
+ return this._inputBuffer[this._inputBufferReadIndex++];
64
+ }
65
+ // Clear input buffer and reset read index
66
+ _clearInputBuffer() {
67
+ this._inputBuffer.length = 0;
68
+ this._inputBufferReadIndex = 0;
69
+ }
70
+ // Compact buffer when read index gets too large (prevent memory growth)
71
+ _compactInputBuffer() {
72
+ if (this._inputBufferReadIndex > 1000 &&
73
+ this._inputBufferReadIndex > this._inputBuffer.length / 2) {
74
+ // Remove already-read bytes and reset index
75
+ this._inputBuffer.splice(0, this._inputBufferReadIndex);
76
+ this._inputBufferReadIndex = 0;
77
+ }
78
+ }
34
79
  get _totalBytesRead() {
35
80
  return this._parent
36
81
  ? this._parent._totalBytesRead
@@ -68,6 +113,82 @@ export class ESPLoader extends EventTarget {
68
113
  this.__isReconfiguring = value;
69
114
  }
70
115
  }
116
+ get _abandonCurrentOperation() {
117
+ return this._parent
118
+ ? this._parent._abandonCurrentOperation
119
+ : this.__abandonCurrentOperation;
120
+ }
121
+ set _abandonCurrentOperation(value) {
122
+ if (this._parent) {
123
+ this._parent._abandonCurrentOperation = value;
124
+ }
125
+ else {
126
+ this.__abandonCurrentOperation = value;
127
+ }
128
+ }
129
+ get _adaptiveBlockMultiplier() {
130
+ return this._parent
131
+ ? this._parent._adaptiveBlockMultiplier
132
+ : this.__adaptiveBlockMultiplier;
133
+ }
134
+ set _adaptiveBlockMultiplier(value) {
135
+ if (this._parent) {
136
+ this._parent._adaptiveBlockMultiplier = value;
137
+ }
138
+ else {
139
+ this.__adaptiveBlockMultiplier = value;
140
+ }
141
+ }
142
+ get _adaptiveMaxInFlightMultiplier() {
143
+ return this._parent
144
+ ? this._parent._adaptiveMaxInFlightMultiplier
145
+ : this.__adaptiveMaxInFlightMultiplier;
146
+ }
147
+ set _adaptiveMaxInFlightMultiplier(value) {
148
+ if (this._parent) {
149
+ this._parent._adaptiveMaxInFlightMultiplier = value;
150
+ }
151
+ else {
152
+ this.__adaptiveMaxInFlightMultiplier = value;
153
+ }
154
+ }
155
+ get _consecutiveSuccessfulChunks() {
156
+ return this._parent
157
+ ? this._parent._consecutiveSuccessfulChunks
158
+ : this.__consecutiveSuccessfulChunks;
159
+ }
160
+ set _consecutiveSuccessfulChunks(value) {
161
+ if (this._parent) {
162
+ this._parent._consecutiveSuccessfulChunks = value;
163
+ }
164
+ else {
165
+ this.__consecutiveSuccessfulChunks = value;
166
+ }
167
+ }
168
+ get _lastAdaptiveAdjustment() {
169
+ return this._parent
170
+ ? this._parent._lastAdaptiveAdjustment
171
+ : this.__lastAdaptiveAdjustment;
172
+ }
173
+ set _lastAdaptiveAdjustment(value) {
174
+ if (this._parent) {
175
+ this._parent._lastAdaptiveAdjustment = value;
176
+ }
177
+ else {
178
+ this.__lastAdaptiveAdjustment = value;
179
+ }
180
+ }
181
+ get _isCDCDevice() {
182
+ return this._parent ? this._parent._isCDCDevice : this.__isCDCDevice;
183
+ }
184
+ set _isCDCDevice(value) {
185
+ if (this._parent) {
186
+ this._parent._isCDCDevice = value;
187
+ }
188
+ else {
189
+ this.__isCDCDevice = value;
190
+ }
191
+ }
71
192
  detectUSBSerialChip(vendorId, productId) {
72
193
  // Common USB-Serial chip vendors and their products
73
194
  const chips = {
@@ -115,6 +236,7 @@ export class ESPLoader extends EventTarget {
115
236
  async initialize() {
116
237
  if (!this._parent) {
117
238
  this.__inputBuffer = [];
239
+ this.__inputBufferReadIndex = 0;
118
240
  this.__totalBytesRead = 0;
119
241
  // Detect and log USB-Serial chip info
120
242
  const portInfo = this.port.getInfo();
@@ -129,6 +251,12 @@ export class ESPLoader extends EventTarget {
129
251
  if (portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x2) {
130
252
  this._isESP32S2NativeUSB = true;
131
253
  }
254
+ // Detect CDC devices for adaptive speed adjustment
255
+ // Espressif Native USB (VID: 0x303a) or CH343 (VID: 0x1a86, PID: 0x55d3)
256
+ if (portInfo.usbVendorId === 0x303a ||
257
+ (portInfo.usbVendorId === 0x1a86 && portInfo.usbProductId === 0x55d3)) {
258
+ this._isCDCDevice = true;
259
+ }
132
260
  }
133
261
  // Don't await this promise so it doesn't block rest of method.
134
262
  this.readLoop();
@@ -185,7 +313,7 @@ export class ESPLoader extends EventTarget {
185
313
  // This ensures all error responses are cleared before continuing
186
314
  await this.drainInputBuffer(200);
187
315
  // Clear input buffer and re-sync to recover from failed command
188
- this._inputBuffer.length = 0;
316
+ this._clearInputBuffer();
189
317
  await sleep(SYNC_TIMEOUT);
190
318
  // Re-sync with the chip to ensure clean communication
191
319
  try {
@@ -310,6 +438,9 @@ export class ESPLoader extends EventTarget {
310
438
  sleep(ms = 100) {
311
439
  return new Promise((resolve) => setTimeout(resolve, ms));
312
440
  }
441
+ // ============================================================================
442
+ // Web Serial (Desktop) - DTR/RTS Signal Handling & Reset Strategies
443
+ // ============================================================================
313
444
  async setRTS(state) {
314
445
  await this.port.setSignals({ requestToSend: state });
315
446
  // Work-around for adapters on Windows using the usbser.sys driver:
@@ -322,6 +453,509 @@ export class ESPLoader extends EventTarget {
322
453
  this.state_DTR = state;
323
454
  await this.port.setSignals({ dataTerminalReady: state });
324
455
  }
456
+ /**
457
+ * @name hardResetUSBJTAGSerial
458
+ * USB-JTAG/Serial reset for Web Serial (Desktop)
459
+ */
460
+ async hardResetUSBJTAGSerial() {
461
+ await this.setRTS(false);
462
+ await this.setDTR(false); // Idle
463
+ await this.sleep(100);
464
+ await this.setDTR(true); // Set IO0
465
+ await this.setRTS(false);
466
+ await this.sleep(100);
467
+ await this.setRTS(true); // Reset
468
+ await this.setDTR(false);
469
+ await this.setRTS(true);
470
+ await this.sleep(100);
471
+ await this.setDTR(false);
472
+ await this.setRTS(false); // Chip out of reset
473
+ await this.sleep(200);
474
+ }
475
+ /**
476
+ * @name hardResetClassic
477
+ * Classic reset for Web Serial (Desktop)
478
+ */
479
+ async hardResetClassic() {
480
+ await this.setDTR(false); // IO0=HIGH
481
+ await this.setRTS(true); // EN=LOW, chip in reset
482
+ await this.sleep(100);
483
+ await this.setDTR(true); // IO0=LOW
484
+ await this.setRTS(false); // EN=HIGH, chip out of reset
485
+ await this.sleep(50);
486
+ await this.setDTR(false); // IO0=HIGH, done
487
+ await this.sleep(200);
488
+ }
489
+ // ============================================================================
490
+ // WebUSB (Android) - DTR/RTS Signal Handling & Reset Strategies
491
+ // ============================================================================
492
+ async setRTSWebUSB(state) {
493
+ // Always specify both signals to avoid flipping the other line
494
+ // The WebUSB setSignals() now preserves unspecified signals, but being explicit is safer
495
+ await this.port.setSignals({
496
+ requestToSend: state,
497
+ dataTerminalReady: this.state_DTR,
498
+ });
499
+ }
500
+ async setDTRWebUSB(state) {
501
+ this.state_DTR = state;
502
+ // Always specify both signals to avoid flipping the other line
503
+ await this.port.setSignals({
504
+ dataTerminalReady: state,
505
+ requestToSend: undefined, // Let setSignals preserve current RTS state
506
+ });
507
+ }
508
+ async setDTRandRTSWebUSB(dtr, rts) {
509
+ this.state_DTR = dtr;
510
+ await this.port.setSignals({
511
+ dataTerminalReady: dtr,
512
+ requestToSend: rts,
513
+ });
514
+ }
515
+ /**
516
+ * @name hardResetUSBJTAGSerialWebUSB
517
+ * USB-JTAG/Serial reset for WebUSB (Android)
518
+ */
519
+ async hardResetUSBJTAGSerialWebUSB() {
520
+ await this.setRTSWebUSB(false);
521
+ await this.setDTRWebUSB(false); // Idle
522
+ await this.sleep(100);
523
+ await this.setDTRWebUSB(true); // Set IO0
524
+ await this.setRTSWebUSB(false);
525
+ await this.sleep(100);
526
+ await this.setRTSWebUSB(true); // Reset
527
+ await this.setDTRWebUSB(false);
528
+ await this.setRTSWebUSB(true);
529
+ await this.sleep(100);
530
+ await this.setDTRWebUSB(false);
531
+ await this.setRTSWebUSB(false); // Chip out of reset
532
+ await this.sleep(200);
533
+ }
534
+ /**
535
+ * @name hardResetUSBJTAGSerialInvertedDTRWebUSB
536
+ * USB-JTAG/Serial reset with inverted DTR for WebUSB (Android)
537
+ */
538
+ async hardResetUSBJTAGSerialInvertedDTRWebUSB() {
539
+ await this.setRTSWebUSB(false);
540
+ await this.setDTRWebUSB(true); // Idle (DTR inverted)
541
+ await this.sleep(100);
542
+ await this.setDTRWebUSB(false); // Set IO0 (DTR inverted)
543
+ await this.setRTSWebUSB(false);
544
+ await this.sleep(100);
545
+ await this.setRTSWebUSB(true); // Reset
546
+ await this.setDTRWebUSB(true); // (DTR inverted)
547
+ await this.setRTSWebUSB(true);
548
+ await this.sleep(100);
549
+ await this.setDTRWebUSB(true); // (DTR inverted)
550
+ await this.setRTSWebUSB(false); // Chip out of reset
551
+ await this.sleep(200);
552
+ }
553
+ /**
554
+ * @name hardResetClassicWebUSB
555
+ * Classic reset for WebUSB (Android)
556
+ */
557
+ async hardResetClassicWebUSB() {
558
+ await this.setDTRWebUSB(false); // IO0=HIGH
559
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
560
+ await this.sleep(100);
561
+ await this.setDTRWebUSB(true); // IO0=LOW
562
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
563
+ await this.sleep(50);
564
+ await this.setDTRWebUSB(false); // IO0=HIGH, done
565
+ await this.sleep(200);
566
+ }
567
+ /**
568
+ * @name hardResetUnixTightWebUSB
569
+ * Unix Tight reset for WebUSB (Android) - sets DTR and RTS simultaneously
570
+ */
571
+ async hardResetUnixTightWebUSB() {
572
+ await this.setDTRandRTSWebUSB(false, false);
573
+ await this.setDTRandRTSWebUSB(true, true);
574
+ await this.setDTRandRTSWebUSB(false, true); // IO0=HIGH & EN=LOW, chip in reset
575
+ await this.sleep(100);
576
+ await this.setDTRandRTSWebUSB(true, false); // IO0=LOW & EN=HIGH, chip out of reset
577
+ await this.sleep(50);
578
+ await this.setDTRandRTSWebUSB(false, false); // IO0=HIGH, done
579
+ await this.setDTRWebUSB(false); // Ensure IO0=HIGH
580
+ await this.sleep(200);
581
+ }
582
+ /**
583
+ * @name hardResetClassicLongDelayWebUSB
584
+ * Classic reset with longer delays for WebUSB (Android)
585
+ * Specifically for CP2102/CH340 which may need more time
586
+ */
587
+ async hardResetClassicLongDelayWebUSB() {
588
+ await this.setDTRWebUSB(false); // IO0=HIGH
589
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
590
+ await this.sleep(500); // Extra long delay
591
+ await this.setDTRWebUSB(true); // IO0=LOW
592
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
593
+ await this.sleep(200);
594
+ await this.setDTRWebUSB(false); // IO0=HIGH, done
595
+ await this.sleep(500); // Extra long delay
596
+ }
597
+ /**
598
+ * @name hardResetClassicShortDelayWebUSB
599
+ * Classic reset with shorter delays for WebUSB (Android)
600
+ */
601
+ async hardResetClassicShortDelayWebUSB() {
602
+ await this.setDTRWebUSB(false); // IO0=HIGH
603
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
604
+ await this.sleep(50);
605
+ await this.setDTRWebUSB(true); // IO0=LOW
606
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
607
+ await this.sleep(25);
608
+ await this.setDTRWebUSB(false); // IO0=HIGH, done
609
+ await this.sleep(100);
610
+ }
611
+ /**
612
+ * @name hardResetInvertedWebUSB
613
+ * Inverted reset sequence for WebUSB (Android) - both signals inverted
614
+ */
615
+ async hardResetInvertedWebUSB() {
616
+ await this.setDTRWebUSB(true); // IO0=HIGH (inverted)
617
+ await this.setRTSWebUSB(false); // EN=LOW, chip in reset (inverted)
618
+ await this.sleep(100);
619
+ await this.setDTRWebUSB(false); // IO0=LOW (inverted)
620
+ await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (inverted)
621
+ await this.sleep(50);
622
+ await this.setDTRWebUSB(true); // IO0=HIGH, done (inverted)
623
+ await this.sleep(200);
624
+ }
625
+ /**
626
+ * @name hardResetInvertedDTRWebUSB
627
+ * Only DTR inverted for WebUSB (Android)
628
+ */
629
+ async hardResetInvertedDTRWebUSB() {
630
+ await this.setDTRWebUSB(true); // IO0=HIGH (DTR inverted)
631
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset (RTS normal)
632
+ await this.sleep(100);
633
+ await this.setDTRWebUSB(false); // IO0=LOW (DTR inverted)
634
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset (RTS normal)
635
+ await this.sleep(50);
636
+ await this.setDTRWebUSB(true); // IO0=HIGH, done (DTR inverted)
637
+ await this.sleep(200);
638
+ }
639
+ /**
640
+ * @name hardResetInvertedRTSWebUSB
641
+ * Only RTS inverted for WebUSB (Android)
642
+ */
643
+ async hardResetInvertedRTSWebUSB() {
644
+ await this.setDTRWebUSB(false); // IO0=HIGH (DTR normal)
645
+ await this.setRTSWebUSB(false); // EN=LOW, chip in reset (RTS inverted)
646
+ await this.sleep(100);
647
+ await this.setDTRWebUSB(true); // IO0=LOW (DTR normal)
648
+ await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (RTS inverted)
649
+ await this.sleep(50);
650
+ await this.setDTRWebUSB(false); // IO0=HIGH, done (DTR normal)
651
+ await this.sleep(200);
652
+ }
653
+ /**
654
+ * Check if we're using WebUSB (Android) or Web Serial (Desktop)
655
+ */
656
+ isWebUSB() {
657
+ // WebUSBSerial class has isWebUSB flag - this is the most reliable check
658
+ return this.port.isWebUSB === true;
659
+ }
660
+ /**
661
+ * @name connectWithResetStrategies
662
+ * Try different reset strategies to enter bootloader mode
663
+ * Similar to esptool.py's connect() method with multiple reset strategies
664
+ */
665
+ async connectWithResetStrategies() {
666
+ const portInfo = this.port.getInfo();
667
+ const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
668
+ const isEspressifUSB = portInfo.usbVendorId === 0x303a;
669
+ // this.logger.log(
670
+ // `Detected USB: VID=0x${portInfo.usbVendorId?.toString(16) || "unknown"}, PID=0x${portInfo.usbProductId?.toString(16) || "unknown"}`,
671
+ // );
672
+ // Define reset strategies to try in order
673
+ const resetStrategies = [];
674
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
675
+ const self = this;
676
+ // WebUSB (Android) uses different reset methods than Web Serial (Desktop)
677
+ if (this.isWebUSB()) {
678
+ // For USB-Serial chips (CP2102, CH340, etc.), try inverted strategies first
679
+ const isUSBSerialChip = !isUSBJTAGSerial && !isEspressifUSB;
680
+ // Detect specific chip types once
681
+ const isCP2102 = portInfo.usbVendorId === 0x10c4;
682
+ const isCH34x = portInfo.usbVendorId === 0x1a86;
683
+ // Check for ESP32-S2 Native USB (VID: 0x303a, PID: 0x0002)
684
+ const isESP32S2NativeUSB = portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x0002;
685
+ // WebUSB Strategy 1: USB-JTAG/Serial reset (for Native USB only)
686
+ if (isUSBJTAGSerial || isEspressifUSB) {
687
+ if (isESP32S2NativeUSB) {
688
+ // ESP32-S2 Native USB: Try multiple strategies
689
+ // The device might be in JTAG mode OR CDC mode
690
+ // Strategy 1: USB-JTAG/Serial (works in CDC mode on Desktop)
691
+ resetStrategies.push({
692
+ name: "USB-JTAG/Serial (WebUSB) - ESP32-S2",
693
+ fn: async function () {
694
+ return await self.hardResetUSBJTAGSerialWebUSB();
695
+ },
696
+ });
697
+ // Strategy 2: USB-JTAG/Serial Inverted DTR (works in JTAG mode)
698
+ resetStrategies.push({
699
+ name: "USB-JTAG/Serial Inverted DTR (WebUSB) - ESP32-S2",
700
+ fn: async function () {
701
+ return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
702
+ },
703
+ });
704
+ // Strategy 3: UnixTight (CDC fallback)
705
+ resetStrategies.push({
706
+ name: "UnixTight (WebUSB) - ESP32-S2 CDC",
707
+ fn: async function () {
708
+ return await self.hardResetUnixTightWebUSB();
709
+ },
710
+ });
711
+ // Strategy 4: Classic reset (CDC fallback)
712
+ resetStrategies.push({
713
+ name: "Classic (WebUSB) - ESP32-S2 CDC",
714
+ fn: async function () {
715
+ return await self.hardResetClassicWebUSB();
716
+ },
717
+ });
718
+ }
719
+ else {
720
+ // Other USB-JTAG chips: Try Inverted DTR first - works best for ESP32-H2 and other JTAG chips
721
+ resetStrategies.push({
722
+ name: "USB-JTAG/Serial Inverted DTR (WebUSB)",
723
+ fn: async function () {
724
+ return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
725
+ },
726
+ });
727
+ resetStrategies.push({
728
+ name: "USB-JTAG/Serial (WebUSB)",
729
+ fn: async function () {
730
+ return await self.hardResetUSBJTAGSerialWebUSB();
731
+ },
732
+ });
733
+ resetStrategies.push({
734
+ name: "Inverted DTR Classic (WebUSB)",
735
+ fn: async function () {
736
+ return await self.hardResetInvertedDTRWebUSB();
737
+ },
738
+ });
739
+ }
740
+ }
741
+ // For USB-Serial chips, try inverted strategies first
742
+ if (isUSBSerialChip) {
743
+ if (isCH34x) {
744
+ // CH340/CH343: UnixTight works best (like CP2102)
745
+ resetStrategies.push({
746
+ name: "UnixTight (WebUSB) - CH34x",
747
+ fn: async function () {
748
+ return await self.hardResetUnixTightWebUSB();
749
+ },
750
+ });
751
+ resetStrategies.push({
752
+ name: "Classic (WebUSB) - CH34x",
753
+ fn: async function () {
754
+ return await self.hardResetClassicWebUSB();
755
+ },
756
+ });
757
+ resetStrategies.push({
758
+ name: "Inverted Both (WebUSB) - CH34x",
759
+ fn: async function () {
760
+ return await self.hardResetInvertedWebUSB();
761
+ },
762
+ });
763
+ resetStrategies.push({
764
+ name: "Inverted RTS (WebUSB) - CH34x",
765
+ fn: async function () {
766
+ return await self.hardResetInvertedRTSWebUSB();
767
+ },
768
+ });
769
+ resetStrategies.push({
770
+ name: "Inverted DTR (WebUSB) - CH34x",
771
+ fn: async function () {
772
+ return await self.hardResetInvertedDTRWebUSB();
773
+ },
774
+ });
775
+ }
776
+ else if (isCP2102) {
777
+ // CP2102: UnixTight works best (tested and confirmed)
778
+ // Try it first, then fallback to other strategies
779
+ resetStrategies.push({
780
+ name: "UnixTight (WebUSB) - CP2102",
781
+ fn: async function () {
782
+ return await self.hardResetUnixTightWebUSB();
783
+ },
784
+ });
785
+ resetStrategies.push({
786
+ name: "Classic (WebUSB) - CP2102",
787
+ fn: async function () {
788
+ return await self.hardResetClassicWebUSB();
789
+ },
790
+ });
791
+ resetStrategies.push({
792
+ name: "Inverted Both (WebUSB) - CP2102",
793
+ fn: async function () {
794
+ return await self.hardResetInvertedWebUSB();
795
+ },
796
+ });
797
+ resetStrategies.push({
798
+ name: "Inverted RTS (WebUSB) - CP2102",
799
+ fn: async function () {
800
+ return await self.hardResetInvertedRTSWebUSB();
801
+ },
802
+ });
803
+ resetStrategies.push({
804
+ name: "Inverted DTR (WebUSB) - CP2102",
805
+ fn: async function () {
806
+ return await self.hardResetInvertedDTRWebUSB();
807
+ },
808
+ });
809
+ }
810
+ else {
811
+ // For other USB-Serial chips, try UnixTight first, then multiple strategies
812
+ resetStrategies.push({
813
+ name: "UnixTight (WebUSB)",
814
+ fn: async function () {
815
+ return await self.hardResetUnixTightWebUSB();
816
+ },
817
+ });
818
+ resetStrategies.push({
819
+ name: "Classic (WebUSB)",
820
+ fn: async function () {
821
+ return await self.hardResetClassicWebUSB();
822
+ },
823
+ });
824
+ resetStrategies.push({
825
+ name: "Inverted Both (WebUSB)",
826
+ fn: async function () {
827
+ return await self.hardResetInvertedWebUSB();
828
+ },
829
+ });
830
+ resetStrategies.push({
831
+ name: "Inverted RTS (WebUSB)",
832
+ fn: async function () {
833
+ return await self.hardResetInvertedRTSWebUSB();
834
+ },
835
+ });
836
+ resetStrategies.push({
837
+ name: "Inverted DTR (WebUSB)",
838
+ fn: async function () {
839
+ return await self.hardResetInvertedDTRWebUSB();
840
+ },
841
+ });
842
+ }
843
+ }
844
+ // Add general fallback strategies only for non-CP2102 and non-ESP32-S2 Native USB chips
845
+ if (!isCP2102 && !isESP32S2NativeUSB) {
846
+ // Classic reset (for chips not handled above)
847
+ if (portInfo.usbVendorId !== 0x1a86) {
848
+ resetStrategies.push({
849
+ name: "Classic (WebUSB)",
850
+ fn: async function () {
851
+ return await self.hardResetClassicWebUSB();
852
+ },
853
+ });
854
+ }
855
+ // UnixTight reset (sets DTR/RTS simultaneously)
856
+ resetStrategies.push({
857
+ name: "UnixTight (WebUSB)",
858
+ fn: async function () {
859
+ return await self.hardResetUnixTightWebUSB();
860
+ },
861
+ });
862
+ // WebUSB Strategy: Classic with long delays
863
+ resetStrategies.push({
864
+ name: "Classic Long Delay (WebUSB)",
865
+ fn: async function () {
866
+ return await self.hardResetClassicLongDelayWebUSB();
867
+ },
868
+ });
869
+ // WebUSB Strategy: Classic with short delays
870
+ resetStrategies.push({
871
+ name: "Classic Short Delay (WebUSB)",
872
+ fn: async function () {
873
+ return await self.hardResetClassicShortDelayWebUSB();
874
+ },
875
+ });
876
+ // WebUSB Strategy: USB-JTAG/Serial fallback
877
+ if (!isUSBJTAGSerial && !isEspressifUSB) {
878
+ resetStrategies.push({
879
+ name: "USB-JTAG/Serial fallback (WebUSB)",
880
+ fn: async function () {
881
+ return await self.hardResetUSBJTAGSerialWebUSB();
882
+ },
883
+ });
884
+ }
885
+ }
886
+ }
887
+ else {
888
+ // Web Serial (Desktop) strategies
889
+ // Strategy: USB-JTAG/Serial reset
890
+ if (isUSBJTAGSerial || isEspressifUSB) {
891
+ resetStrategies.push({
892
+ name: "USB-JTAG/Serial",
893
+ fn: async function () {
894
+ return await self.hardResetUSBJTAGSerial();
895
+ },
896
+ });
897
+ }
898
+ // Strategy: Classic reset
899
+ resetStrategies.push({
900
+ name: "Classic",
901
+ fn: async function () {
902
+ return await self.hardResetClassic();
903
+ },
904
+ });
905
+ // Strategy: USB-JTAG/Serial fallback
906
+ if (!isUSBJTAGSerial && !isEspressifUSB) {
907
+ resetStrategies.push({
908
+ name: "USB-JTAG/Serial (fallback)",
909
+ fn: async function () {
910
+ return await self.hardResetUSBJTAGSerial();
911
+ },
912
+ });
913
+ }
914
+ }
915
+ let lastError = null;
916
+ // Try each reset strategy with timeout
917
+ for (const strategy of resetStrategies) {
918
+ try {
919
+ // Check if port is still open, if not, skip this strategy
920
+ if (!this.connected || !this.port.writable) {
921
+ this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
922
+ continue;
923
+ }
924
+ // Clear abandon flag before starting new strategy
925
+ this._abandonCurrentOperation = false;
926
+ await strategy.fn();
927
+ // Try to sync after reset with internally time-bounded sync (3 seconds per strategy)
928
+ const syncSuccess = await this.syncWithTimeout(3000);
929
+ if (syncSuccess) {
930
+ // Sync succeeded
931
+ this.logger.log(`Connected successfully with ${strategy.name} reset.`);
932
+ return;
933
+ }
934
+ else {
935
+ throw new Error("Sync timeout or abandoned");
936
+ }
937
+ }
938
+ catch (error) {
939
+ lastError = error;
940
+ this.logger.log(`${strategy.name} reset failed: ${error.message}`);
941
+ // Set abandon flag to stop any in-flight operations
942
+ this._abandonCurrentOperation = true;
943
+ // Wait a bit for in-flight operations to abort
944
+ await sleep(100);
945
+ // If port got disconnected, we can't try more strategies
946
+ if (!this.connected || !this.port.writable) {
947
+ this.logger.log(`Port disconnected during reset attempt`);
948
+ break;
949
+ }
950
+ // Clear buffers before trying next strategy
951
+ this._clearInputBuffer();
952
+ await this.drainInputBuffer(200);
953
+ await this.flushSerialBuffers();
954
+ }
955
+ }
956
+ // All strategies failed
957
+ throw new Error(`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
958
+ }
325
959
  async hardReset(bootloader = false) {
326
960
  if (bootloader) {
327
961
  // enter flash mode
@@ -330,16 +964,34 @@ export class ESPLoader extends EventTarget {
330
964
  this.logger.log("USB-JTAG/Serial reset.");
331
965
  }
332
966
  else {
333
- await this.hardResetClassic();
334
- this.logger.log("Classic reset.");
967
+ // Use different reset strategy for WebUSB (Android) vs Web Serial (Desktop)
968
+ if (this.isWebUSB()) {
969
+ await this.hardResetClassicWebUSB();
970
+ this.logger.log("Classic reset (WebUSB/Android).");
971
+ }
972
+ else {
973
+ await this.hardResetClassic();
974
+ this.logger.log("Classic reset.");
975
+ }
335
976
  }
336
977
  }
337
978
  else {
338
- // just reset
339
- await this.setRTS(true); // EN->LOW
340
- await this.sleep(100);
341
- await this.setRTS(false);
342
- this.logger.log("Hard reset.");
979
+ // just reset (no bootloader mode)
980
+ if (this.isWebUSB()) {
981
+ // WebUSB: Use longer delays for better compatibility
982
+ await this.setRTS(true); // EN->LOW
983
+ await this.sleep(200);
984
+ await this.setRTS(false);
985
+ await this.sleep(200);
986
+ this.logger.log("Hard reset (WebUSB).");
987
+ }
988
+ else {
989
+ // Web Serial: Standard reset
990
+ await this.setRTS(true); // EN->LOW
991
+ await this.sleep(100);
992
+ await this.setRTS(false);
993
+ this.logger.log("Hard reset.");
994
+ }
343
995
  }
344
996
  await new Promise((resolve) => setTimeout(resolve, 1000));
345
997
  }
@@ -464,6 +1116,12 @@ export class ESPLoader extends EventTarget {
464
1116
  else if ([2, 4].includes(data.length)) {
465
1117
  statusLen = data.length;
466
1118
  }
1119
+ else {
1120
+ // Default to 2-byte status if we can't determine
1121
+ // This prevents silent data corruption when statusLen would be 0
1122
+ statusLen = 2;
1123
+ this.logger.debug(`Unknown chip family, defaulting to 2-byte status (opcode: ${toHex(opcode)}, data.length: ${data.length})`);
1124
+ }
467
1125
  }
468
1126
  if (data.length < statusLen) {
469
1127
  throw new Error("Didn't get enough status bytes");
@@ -512,71 +1170,164 @@ export class ESPLoader extends EventTarget {
512
1170
  * @name readPacket
513
1171
  * Generator to read SLIP packets from a serial port.
514
1172
  * Yields one full SLIP packet at a time, raises exception on timeout or invalid data.
1173
+ *
1174
+ * Two implementations:
1175
+ * - Burst: CDC devices (Native USB) and CH343 - very fast processing
1176
+ * - Byte-by-byte: CH340, CP2102, and other USB-Serial adapters - stable fast processing
515
1177
  */
516
1178
  async readPacket(timeout) {
517
1179
  let partialPacket = null;
518
1180
  let inEscape = false;
519
- const startTime = Date.now();
520
- while (true) {
521
- // Check timeout
522
- if (Date.now() - startTime > timeout) {
523
- const waitingFor = partialPacket === null ? "header" : "content";
524
- throw new SlipReadError("Timed out waiting for packet " + waitingFor);
525
- }
526
- // If no data available, wait a bit
527
- if (this._inputBuffer.length === 0) {
528
- await sleep(1);
529
- continue;
530
- }
531
- // Process all available bytes without going back to outer loop
532
- // This is critical for handling high-speed burst transfers
533
- while (this._inputBuffer.length > 0) {
534
- const b = this._inputBuffer.shift();
535
- if (partialPacket === null) {
536
- // waiting for packet header
537
- if (b == 0xc0) {
538
- partialPacket = [];
1181
+ // CDC devices use burst processing, non-CDC use byte-by-byte
1182
+ if (this._isCDCDevice) {
1183
+ // Burst version: Process all available bytes in one pass for ultra-high-speed transfers
1184
+ // Used for: CDC devices (all platforms) and CH343
1185
+ const startTime = Date.now();
1186
+ while (true) {
1187
+ // Check abandon flag (for reset strategy timeout)
1188
+ if (this._abandonCurrentOperation) {
1189
+ throw new SlipReadError("Operation abandoned (reset strategy timeout)");
1190
+ }
1191
+ // Check timeout
1192
+ if (Date.now() - startTime > timeout) {
1193
+ const waitingFor = partialPacket === null ? "header" : "content";
1194
+ throw new SlipReadError("Timed out waiting for packet " + waitingFor);
1195
+ }
1196
+ // If no data available, wait a bit
1197
+ if (this._inputBufferAvailable === 0) {
1198
+ await sleep(1);
1199
+ continue;
1200
+ }
1201
+ // Process all available bytes without going back to outer loop
1202
+ // This is critical for handling high-speed burst transfers
1203
+ while (this._inputBufferAvailable > 0) {
1204
+ const b = this._readByte();
1205
+ if (partialPacket === null) {
1206
+ // waiting for packet header
1207
+ if (b == 0xc0) {
1208
+ partialPacket = [];
1209
+ }
1210
+ else {
1211
+ if (this.debug) {
1212
+ this.logger.debug("Read invalid data: " + toHex(b));
1213
+ this.logger.debug("Remaining data in serial buffer: " +
1214
+ hexFormatter(this._inputBuffer));
1215
+ }
1216
+ throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
1217
+ }
539
1218
  }
540
- else {
541
- if (this.debug) {
542
- this.logger.debug("Read invalid data: " + toHex(b));
543
- this.logger.debug("Remaining data in serial buffer: " +
544
- hexFormatter(this._inputBuffer));
1219
+ else if (inEscape) {
1220
+ // part-way through escape sequence
1221
+ inEscape = false;
1222
+ if (b == 0xdc) {
1223
+ partialPacket.push(0xc0);
1224
+ }
1225
+ else if (b == 0xdd) {
1226
+ partialPacket.push(0xdb);
1227
+ }
1228
+ else {
1229
+ if (this.debug) {
1230
+ this.logger.debug("Read invalid data: " + toHex(b));
1231
+ this.logger.debug("Remaining data in serial buffer: " +
1232
+ hexFormatter(this._inputBuffer));
1233
+ }
1234
+ throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
545
1235
  }
546
- throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
547
1236
  }
548
- }
549
- else if (inEscape) {
550
- // part-way through escape sequence
551
- inEscape = false;
552
- if (b == 0xdc) {
553
- partialPacket.push(0xc0);
1237
+ else if (b == 0xdb) {
1238
+ // start of escape sequence
1239
+ inEscape = true;
554
1240
  }
555
- else if (b == 0xdd) {
556
- partialPacket.push(0xdb);
1241
+ else if (b == 0xc0) {
1242
+ // end of packet
1243
+ if (this.debug)
1244
+ this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
1245
+ // Compact buffer periodically to prevent memory growth
1246
+ this._compactInputBuffer();
1247
+ return partialPacket;
557
1248
  }
558
1249
  else {
559
- if (this.debug) {
560
- this.logger.debug("Read invalid data: " + toHex(b));
561
- this.logger.debug("Remaining data in serial buffer: " +
562
- hexFormatter(this._inputBuffer));
563
- }
564
- throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
1250
+ // normal byte in packet
1251
+ partialPacket.push(b);
565
1252
  }
566
1253
  }
567
- else if (b == 0xdb) {
568
- // start of escape sequence
569
- inEscape = true;
1254
+ }
1255
+ }
1256
+ else {
1257
+ // Byte-by-byte version: Stable for non CDC USB-Serial adapters (CH340, CP2102, etc.)
1258
+ let readBytes = [];
1259
+ while (true) {
1260
+ // Check abandon flag (for reset strategy timeout)
1261
+ if (this._abandonCurrentOperation) {
1262
+ throw new SlipReadError("Operation abandoned (reset strategy timeout)");
1263
+ }
1264
+ const stamp = Date.now();
1265
+ readBytes = [];
1266
+ while (Date.now() - stamp < timeout) {
1267
+ if (this._inputBufferAvailable > 0) {
1268
+ readBytes.push(this._readByte());
1269
+ break;
1270
+ }
1271
+ else {
1272
+ // Reduced sleep time for faster response during high-speed transfers
1273
+ await sleep(1);
1274
+ }
570
1275
  }
571
- else if (b == 0xc0) {
572
- // end of packet
573
- if (this.debug)
574
- this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
575
- return partialPacket;
1276
+ if (readBytes.length == 0) {
1277
+ const waitingFor = partialPacket === null ? "header" : "content";
1278
+ throw new SlipReadError("Timed out waiting for packet " + waitingFor);
576
1279
  }
577
- else {
578
- // normal byte in packet
579
- partialPacket.push(b);
1280
+ if (this.debug)
1281
+ this.logger.debug("Read " + readBytes.length + " bytes: " + hexFormatter(readBytes));
1282
+ for (const b of readBytes) {
1283
+ if (partialPacket === null) {
1284
+ // waiting for packet header
1285
+ if (b == 0xc0) {
1286
+ partialPacket = [];
1287
+ }
1288
+ else {
1289
+ if (this.debug) {
1290
+ this.logger.debug("Read invalid data: " + toHex(b));
1291
+ this.logger.debug("Remaining data in serial buffer: " +
1292
+ hexFormatter(this._inputBuffer));
1293
+ }
1294
+ throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
1295
+ }
1296
+ }
1297
+ else if (inEscape) {
1298
+ // part-way through escape sequence
1299
+ inEscape = false;
1300
+ if (b == 0xdc) {
1301
+ partialPacket.push(0xc0);
1302
+ }
1303
+ else if (b == 0xdd) {
1304
+ partialPacket.push(0xdb);
1305
+ }
1306
+ else {
1307
+ if (this.debug) {
1308
+ this.logger.debug("Read invalid data: " + toHex(b));
1309
+ this.logger.debug("Remaining data in serial buffer: " +
1310
+ hexFormatter(this._inputBuffer));
1311
+ }
1312
+ throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
1313
+ }
1314
+ }
1315
+ else if (b == 0xdb) {
1316
+ // start of escape sequence
1317
+ inEscape = true;
1318
+ }
1319
+ else if (b == 0xc0) {
1320
+ // end of packet
1321
+ if (this.debug)
1322
+ this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
1323
+ // Compact buffer periodically to prevent memory growth
1324
+ this._compactInputBuffer();
1325
+ return partialPacket;
1326
+ }
1327
+ else {
1328
+ // normal byte in packet
1329
+ partialPacket.push(b);
1330
+ }
580
1331
  }
581
1332
  }
582
1333
  }
@@ -608,7 +1359,7 @@ export class ESPLoader extends EventTarget {
608
1359
  throw new Error(`Invalid (unsupported) command ${toHex(opcode)}`);
609
1360
  }
610
1361
  }
611
- throw "Response doesn't match request";
1362
+ throw new Error("Response doesn't match request");
612
1363
  }
613
1364
  /**
614
1365
  * @name checksum
@@ -660,6 +1411,8 @@ export class ESPLoader extends EventTarget {
660
1411
  }
661
1412
  async reconfigurePort(baud) {
662
1413
  var _a;
1414
+ // Block new writes during the entire reconfiguration (all paths)
1415
+ this._isReconfiguring = true;
663
1416
  try {
664
1417
  // Wait for pending writes to complete
665
1418
  try {
@@ -668,8 +1421,30 @@ export class ESPLoader extends EventTarget {
668
1421
  catch (err) {
669
1422
  this.logger.debug(`Pending write error during reconfigure: ${err}`);
670
1423
  }
671
- // Block new writes during port close/open
672
- this._isReconfiguring = true;
1424
+ // WebUSB: Check if we should use setBaudRate() or close/reopen
1425
+ if (this.isWebUSB()) {
1426
+ const portInfo = this.port.getInfo();
1427
+ const isCH343 = portInfo.usbVendorId === 0x1a86 && portInfo.usbProductId === 0x55d3;
1428
+ // CH343 is a CDC device and MUST use close/reopen
1429
+ // Other chips (CH340, CP2102, FTDI) MUST use setBaudRate()
1430
+ if (!isCH343 &&
1431
+ typeof this.port.setBaudRate === "function") {
1432
+ // this.logger.log(
1433
+ // `[WebUSB] Changing baudrate to ${baud} using setBaudRate()...`,
1434
+ // );
1435
+ await this.port.setBaudRate(baud);
1436
+ // this.logger.log(`[WebUSB] Baudrate changed to ${baud}`);
1437
+ // Give the chip time to adjust to new baudrate
1438
+ await sleep(100);
1439
+ return;
1440
+ }
1441
+ else if (isCH343) {
1442
+ // this.logger.log(
1443
+ // `[WebUSB] CH343 detected - using close/reopen for baudrate change`,
1444
+ // );
1445
+ }
1446
+ }
1447
+ // Web Serial or CH343: Close and reopen port
673
1448
  // Release persistent writer before closing
674
1449
  if (this._writer) {
675
1450
  try {
@@ -688,120 +1463,54 @@ export class ESPLoader extends EventTarget {
688
1463
  await this.port.close();
689
1464
  // Reopen Port
690
1465
  await this.port.open({ baudRate: baud });
691
- // Port is now open - allow writes again
692
- this._isReconfiguring = false;
693
1466
  // Clear buffer again
694
1467
  await this.flushSerialBuffers();
695
1468
  // Restart Readloop
696
1469
  this.readLoop();
697
1470
  }
698
1471
  catch (e) {
699
- this._isReconfiguring = false;
700
1472
  this.logger.error(`Reconfigure port error: ${e}`);
701
1473
  throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
702
1474
  }
1475
+ finally {
1476
+ // Always reset flag, even on error or early return
1477
+ this._isReconfiguring = false;
1478
+ }
703
1479
  }
704
1480
  /**
705
- * @name connectWithResetStrategies
706
- * Try different reset strategies to enter bootloader mode
707
- * Similar to esptool.py's connect() method with multiple reset strategies
1481
+ * @name syncWithTimeout
1482
+ * Sync with timeout that can be abandoned (for reset strategy loop)
1483
+ * This is internally time-bounded and checks the abandon flag
708
1484
  */
709
- async connectWithResetStrategies() {
710
- var _a, _b;
711
- const portInfo = this.port.getInfo();
712
- const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
713
- const isEspressifUSB = portInfo.usbVendorId === 0x303a;
714
- 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"}`);
715
- // Define reset strategies to try in order
716
- const resetStrategies = [];
717
- // Strategy 1: USB-JTAG/Serial reset (for ESP32-C3, C6, S3, etc.)
718
- // Try this first if we detect Espressif USB VID or the specific PID
719
- if (isUSBJTAGSerial || isEspressifUSB) {
720
- resetStrategies.push({
721
- name: "USB-JTAG/Serial",
722
- fn: async () => await this.hardResetUSBJTAGSerial(),
723
- });
724
- }
725
- // Strategy 2: Classic reset (for USB-to-Serial bridges)
726
- resetStrategies.push({
727
- name: "Classic",
728
- fn: async () => await this.hardResetClassic(),
729
- });
730
- // Strategy 3: If USB-JTAG/Serial was not tried yet, try it as fallback
731
- if (!isUSBJTAGSerial && !isEspressifUSB) {
732
- resetStrategies.push({
733
- name: "USB-JTAG/Serial (fallback)",
734
- fn: async () => await this.hardResetUSBJTAGSerial(),
735
- });
736
- }
737
- let lastError = null;
738
- // Try each reset strategy
739
- for (const strategy of resetStrategies) {
1485
+ async syncWithTimeout(timeoutMs) {
1486
+ const startTime = Date.now();
1487
+ for (let i = 0; i < 5; i++) {
1488
+ // Check if we've exceeded the timeout
1489
+ if (Date.now() - startTime > timeoutMs) {
1490
+ return false;
1491
+ }
1492
+ // Check abandon flag
1493
+ if (this._abandonCurrentOperation) {
1494
+ return false;
1495
+ }
1496
+ this._clearInputBuffer();
740
1497
  try {
741
- this.logger.log(`Trying ${strategy.name} reset...`);
742
- // Check if port is still open, if not, skip this strategy
743
- if (!this.connected || !this.port.writable) {
744
- this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
745
- continue;
1498
+ const response = await this._sync();
1499
+ if (response) {
1500
+ await sleep(SYNC_TIMEOUT);
1501
+ return true;
746
1502
  }
747
- await strategy.fn();
748
- // Try to sync after reset
749
- await this.sync();
750
- // If we get here, sync succeeded
751
- this.logger.log(`Connected successfully with ${strategy.name} reset.`);
752
- return;
1503
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
753
1504
  }
754
- catch (error) {
755
- lastError = error;
756
- this.logger.log(`${strategy.name} reset failed: ${error.message}`);
757
- // If port got disconnected, we can't try more strategies
758
- if (!this.connected || !this.port.writable) {
759
- this.logger.log(`Port disconnected during reset attempt`);
760
- break;
1505
+ catch (e) {
1506
+ // Check abandon flag after error
1507
+ if (this._abandonCurrentOperation) {
1508
+ return false;
761
1509
  }
762
- // Clear buffers before trying next strategy
763
- this._inputBuffer.length = 0;
764
- await this.drainInputBuffer(200);
765
- await this.flushSerialBuffers();
766
1510
  }
1511
+ await sleep(SYNC_TIMEOUT);
767
1512
  }
768
- // All strategies failed
769
- throw new Error(`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
770
- }
771
- /**
772
- * @name hardResetUSBJTAGSerial
773
- * USB-JTAG/Serial reset sequence for ESP32-C3, ESP32-S3, ESP32-C6, etc.
774
- */
775
- async hardResetUSBJTAGSerial() {
776
- await this.setRTS(false);
777
- await this.setDTR(false); // Idle
778
- await this.sleep(100);
779
- await this.setDTR(true); // Set IO0
780
- await this.setRTS(false);
781
- await this.sleep(100);
782
- await this.setRTS(true); // Reset. Calls inverted to go through (1,1) instead of (0,0)
783
- await this.setDTR(false);
784
- await this.setRTS(true); // RTS set as Windows only propagates DTR on RTS setting
785
- await this.sleep(100);
786
- await this.setDTR(false);
787
- await this.setRTS(false); // Chip out of reset
788
- // Wait for chip to boot into bootloader
789
- await this.sleep(200);
790
- }
791
- /**
792
- * @name hardResetClassic
793
- * Classic reset sequence for USB-to-Serial bridge chips (CH340, CP2102, etc.)
794
- */
795
- async hardResetClassic() {
796
- await this.setDTR(false); // IO0=HIGH
797
- await this.setRTS(true); // EN=LOW, chip in reset
798
- await this.sleep(100);
799
- await this.setDTR(true); // IO0=LOW
800
- await this.setRTS(false); // EN=HIGH, chip out of reset
801
- await this.sleep(50);
802
- await this.setDTR(false); // IO0=HIGH, done
803
- // Wait for chip to boot into bootloader
804
- await this.sleep(200);
1513
+ return false;
805
1514
  }
806
1515
  /**
807
1516
  * @name sync
@@ -810,7 +1519,7 @@ export class ESPLoader extends EventTarget {
810
1519
  */
811
1520
  async sync() {
812
1521
  for (let i = 0; i < 5; i++) {
813
- this._inputBuffer.length = 0;
1522
+ this._clearInputBuffer();
814
1523
  const response = await this._sync();
815
1524
  if (response) {
816
1525
  await sleep(SYNC_TIMEOUT);
@@ -834,8 +1543,10 @@ export class ESPLoader extends EventTarget {
834
1543
  return true;
835
1544
  }
836
1545
  }
837
- catch {
838
- // If read packet fails.
1546
+ catch (e) {
1547
+ if (this.debug) {
1548
+ this.logger.debug(`Sync attempt ${i + 1} failed: ${e}`);
1549
+ }
839
1550
  }
840
1551
  }
841
1552
  return false;
@@ -1310,12 +2021,19 @@ export class ESPLoader extends EventTarget {
1310
2021
  await this._writer.write(new Uint8Array(data));
1311
2022
  }, async () => {
1312
2023
  // Previous write failed, but still attempt this write
2024
+ this.logger.debug("Previous write failed, attempting recovery for current write");
1313
2025
  if (!this.port.writable) {
1314
2026
  throw new Error("Port became unavailable during write");
1315
2027
  }
1316
2028
  // Writer was likely cleaned up by previous error, create new one
1317
2029
  if (!this._writer) {
1318
- this._writer = this.port.writable.getWriter();
2030
+ try {
2031
+ this._writer = this.port.writable.getWriter();
2032
+ }
2033
+ catch (err) {
2034
+ this.logger.debug(`Failed to get writer in recovery: ${err}`);
2035
+ throw new Error("Cannot acquire writer lock");
2036
+ }
1319
2037
  }
1320
2038
  await this._writer.write(new Uint8Array(data));
1321
2039
  })
@@ -1326,7 +2044,7 @@ export class ESPLoader extends EventTarget {
1326
2044
  try {
1327
2045
  this._writer.releaseLock();
1328
2046
  }
1329
- catch (e) {
2047
+ catch {
1330
2048
  // Ignore release errors
1331
2049
  }
1332
2050
  this._writer = undefined;
@@ -1382,6 +2100,7 @@ export class ESPLoader extends EventTarget {
1382
2100
  await new Promise((resolve) => {
1383
2101
  if (!this._reader) {
1384
2102
  resolve(undefined);
2103
+ return;
1385
2104
  }
1386
2105
  this.addEventListener("disconnect", resolve, { once: true });
1387
2106
  this._reader.cancel();
@@ -1405,6 +2124,7 @@ export class ESPLoader extends EventTarget {
1405
2124
  this.logger.log("Reconnecting serial port...");
1406
2125
  this.connected = false;
1407
2126
  this.__inputBuffer = [];
2127
+ this.__inputBufferReadIndex = 0;
1408
2128
  // Wait for pending writes to complete
1409
2129
  try {
1410
2130
  await this._writeChain;
@@ -1467,6 +2187,7 @@ export class ESPLoader extends EventTarget {
1467
2187
  await this.hardReset(true);
1468
2188
  if (!this._parent) {
1469
2189
  this.__inputBuffer = [];
2190
+ this.__inputBufferReadIndex = 0;
1470
2191
  this.__totalBytesRead = 0;
1471
2192
  this.readLoop();
1472
2193
  }
@@ -1494,10 +2215,10 @@ export class ESPLoader extends EventTarget {
1494
2215
  throw new Error(`Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
1495
2216
  }
1496
2217
  }
1497
- // Copy stub state to this instance if we're a stub loader
1498
- if (this.IS_STUB) {
1499
- Object.assign(this, stubLoader);
1500
- }
2218
+ // The stub is now running on the chip
2219
+ // stubLoader has this instance as _parent, so all operations go through this
2220
+ // We just need to mark this instance as running stub code
2221
+ this.IS_STUB = true;
1501
2222
  this.logger.debug("Reconnection successful");
1502
2223
  }
1503
2224
  catch (err) {
@@ -1529,8 +2250,8 @@ export class ESPLoader extends EventTarget {
1529
2250
  const drainStart = Date.now();
1530
2251
  const drainTimeout = 100; // Short timeout for draining
1531
2252
  while (drained < bytesToDrain && Date.now() - drainStart < drainTimeout) {
1532
- if (this._inputBuffer.length > 0) {
1533
- const byte = this._inputBuffer.shift();
2253
+ if (this._inputBufferAvailable > 0) {
2254
+ const byte = this._readByte();
1534
2255
  if (byte !== undefined) {
1535
2256
  drained++;
1536
2257
  }
@@ -1546,6 +2267,7 @@ export class ESPLoader extends EventTarget {
1546
2267
  // Final clear of application buffer
1547
2268
  if (!this._parent) {
1548
2269
  this.__inputBuffer = [];
2270
+ this.__inputBufferReadIndex = 0;
1549
2271
  }
1550
2272
  }
1551
2273
  /**
@@ -1557,12 +2279,14 @@ export class ESPLoader extends EventTarget {
1557
2279
  // Clear application buffer
1558
2280
  if (!this._parent) {
1559
2281
  this.__inputBuffer = [];
2282
+ this.__inputBufferReadIndex = 0;
1560
2283
  }
1561
2284
  // Wait for any pending data
1562
2285
  await sleep(SYNC_TIMEOUT);
1563
2286
  // Final clear
1564
2287
  if (!this._parent) {
1565
2288
  this.__inputBuffer = [];
2289
+ this.__inputBufferReadIndex = 0;
1566
2290
  }
1567
2291
  this.logger.debug("Serial buffers flushed");
1568
2292
  }
@@ -1581,7 +2305,35 @@ export class ESPLoader extends EventTarget {
1581
2305
  // Flush serial buffers before flash read operation
1582
2306
  await this.flushSerialBuffers();
1583
2307
  this.logger.log(`Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`);
1584
- const CHUNK_SIZE = 0x10000; // 64KB chunks
2308
+ // Initialize adaptive speed multipliers for WebUSB devices
2309
+ if (this.isWebUSB()) {
2310
+ if (this._isCDCDevice) {
2311
+ // CDC devices (CH343): Start with maximum, adaptive adjustment enabled
2312
+ this._adaptiveBlockMultiplier = 8; // blockSize = 248 bytes
2313
+ this._adaptiveMaxInFlightMultiplier = 8; // maxInFlight = 248 bytes
2314
+ this._consecutiveSuccessfulChunks = 0;
2315
+ this.logger.debug(`CDC device - Initialized: blockMultiplier=${this._adaptiveBlockMultiplier}, maxInFlightMultiplier=${this._adaptiveMaxInFlightMultiplier}`);
2316
+ }
2317
+ else {
2318
+ // Non-CDC devices (CH340, CP2102): Fixed values, no adaptive adjustment
2319
+ this._adaptiveBlockMultiplier = 1; // blockSize = 31 bytes (fixed)
2320
+ this._adaptiveMaxInFlightMultiplier = 1; // maxInFlight = 31 bytes (fixed)
2321
+ this._consecutiveSuccessfulChunks = 0;
2322
+ this.logger.debug(`Non-CDC device - Fixed values: blockSize=31, maxInFlight=31`);
2323
+ }
2324
+ }
2325
+ // Chunk size: Amount of data to request from ESP in one command
2326
+ // For WebUSB (Android), use smaller chunks to avoid timeouts and buffer issues
2327
+ // For Web Serial (Desktop), use larger chunks for better performance
2328
+ let CHUNK_SIZE;
2329
+ if (this.isWebUSB()) {
2330
+ // WebUSB: Use smaller chunks to avoid SLIP timeout issues
2331
+ CHUNK_SIZE = 0x4 * 0x1000; // 4KB = 16384 bytes
2332
+ }
2333
+ else {
2334
+ // Web Serial: Use larger chunks for better performance
2335
+ CHUNK_SIZE = 0x40 * 0x1000;
2336
+ }
1585
2337
  let allData = new Uint8Array(0);
1586
2338
  let currentAddr = addr;
1587
2339
  let remainingSize = size;
@@ -1594,14 +2346,30 @@ export class ESPLoader extends EventTarget {
1594
2346
  // Retry loop for this chunk
1595
2347
  while (!chunkSuccess && retryCount <= MAX_RETRIES) {
1596
2348
  let resp = new Uint8Array(0);
2349
+ let lastAckedLength = 0; // Track last acknowledged length
1597
2350
  try {
1598
2351
  // Only log on first attempt or retries
1599
2352
  if (retryCount === 0) {
1600
2353
  this.logger.debug(`Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`);
1601
2354
  }
1602
- // Send read flash command for this chunk
1603
- // This must be inside the retry loop so we send a fresh command after errors
1604
- const pkt = pack("<IIII", currentAddr, chunkSize, 0x1000, 1024);
2355
+ let blockSize;
2356
+ let maxInFlight;
2357
+ if (this.isWebUSB()) {
2358
+ // WebUSB (Android): All devices use adaptive speed
2359
+ // All have maxTransferSize=64, baseBlockSize=31
2360
+ const maxTransferSize = this.port.maxTransferSize || 64;
2361
+ const baseBlockSize = Math.floor((maxTransferSize - 2) / 2); // 31 bytes
2362
+ // Use current adaptive multipliers (initialized at start of readFlash)
2363
+ blockSize = baseBlockSize * this._adaptiveBlockMultiplier;
2364
+ maxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
2365
+ }
2366
+ else {
2367
+ // Web Serial (Desktop): Use multiples of 63 for consistency
2368
+ const base = 63;
2369
+ blockSize = base * 65; // 63 * 65 = 4095 (close to 0x1000)
2370
+ maxInFlight = base * 130; // 63 * 130 = 8190 (close to blockSize * 2)
2371
+ }
2372
+ const pkt = pack("<IIII", currentAddr, chunkSize, blockSize, maxInFlight);
1605
2373
  const [res] = await this.checkCommand(ESP_READ_FLASH, pkt);
1606
2374
  if (res != 0) {
1607
2375
  throw new Error("Failed to read memory: " + res);
@@ -1644,10 +2412,18 @@ export class ESPLoader extends EventTarget {
1644
2412
  newResp.set(resp);
1645
2413
  newResp.set(packetData, resp.length);
1646
2414
  resp = newResp;
1647
- // Send acknowledgment
1648
- const ackData = pack("<I", resp.length);
1649
- const slipEncodedAck = slipEncode(ackData);
1650
- await this.writeToStream(slipEncodedAck);
2415
+ // Send acknowledgment after receiving maxInFlight bytes
2416
+ // This unblocks the stub to send the next batch of packets
2417
+ const shouldAck = resp.length >= chunkSize || // End of chunk
2418
+ resp.length >= lastAckedLength + maxInFlight; // Received all packets
2419
+ if (shouldAck) {
2420
+ const ackData = pack("<I", resp.length);
2421
+ const slipEncodedAck = slipEncode(ackData);
2422
+ await this.writeToStream(slipEncodedAck);
2423
+ // Update lastAckedLength to current response length
2424
+ // This ensures next ACK is sent at the right time
2425
+ lastAckedLength = resp.length;
2426
+ }
1651
2427
  }
1652
2428
  }
1653
2429
  // Chunk read successfully - append to all data
@@ -1656,9 +2432,62 @@ export class ESPLoader extends EventTarget {
1656
2432
  newAllData.set(resp, allData.length);
1657
2433
  allData = newAllData;
1658
2434
  chunkSuccess = true;
2435
+ // ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
2436
+ // Non-CDC devices (CH340, CP2102) stay at fixed blockSize=31, maxInFlight=31
2437
+ if (this.isWebUSB() && this._isCDCDevice && retryCount === 0) {
2438
+ this._consecutiveSuccessfulChunks++;
2439
+ // After 2 consecutive successful chunks, increase speed gradually
2440
+ if (this._consecutiveSuccessfulChunks >= 2) {
2441
+ const maxTransferSize = this.port.maxTransferSize || 64;
2442
+ const baseBlockSize = Math.floor((maxTransferSize - 2) / 2); // 31 bytes
2443
+ // Maximum: blockSize=248 (8 * 31), maxInFlight=248 (8 * 31)
2444
+ const MAX_BLOCK_MULTIPLIER = 8; // 248 bytes - tested stable
2445
+ const MAX_INFLIGHT_MULTIPLIER = 8; // 248 bytes - tested stable
2446
+ let adjusted = false;
2447
+ // Increase blockSize first (up to 248), then maxInFlight
2448
+ if (this._adaptiveBlockMultiplier < MAX_BLOCK_MULTIPLIER) {
2449
+ this._adaptiveBlockMultiplier = Math.min(this._adaptiveBlockMultiplier * 2, MAX_BLOCK_MULTIPLIER);
2450
+ adjusted = true;
2451
+ }
2452
+ // Once blockSize is at maximum, increase maxInFlight
2453
+ else if (this._adaptiveMaxInFlightMultiplier < MAX_INFLIGHT_MULTIPLIER) {
2454
+ this._adaptiveMaxInFlightMultiplier = Math.min(this._adaptiveMaxInFlightMultiplier * 2, MAX_INFLIGHT_MULTIPLIER);
2455
+ adjusted = true;
2456
+ }
2457
+ if (adjusted) {
2458
+ const newBlockSize = baseBlockSize * this._adaptiveBlockMultiplier;
2459
+ const newMaxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
2460
+ this.logger.debug(`Speed increased: blockSize=${newBlockSize}, maxInFlight=${newMaxInFlight}`);
2461
+ this._lastAdaptiveAdjustment = Date.now();
2462
+ }
2463
+ // Reset counter
2464
+ this._consecutiveSuccessfulChunks = 0;
2465
+ }
2466
+ }
1659
2467
  }
1660
2468
  catch (err) {
1661
2469
  retryCount++;
2470
+ // ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
2471
+ // Non-CDC devices stay at fixed values
2472
+ if (this.isWebUSB() && this._isCDCDevice && retryCount === 1) {
2473
+ // Only reduce if we're above minimum
2474
+ if (this._adaptiveBlockMultiplier > 1 ||
2475
+ this._adaptiveMaxInFlightMultiplier > 1) {
2476
+ // Reduce to minimum on error
2477
+ this._adaptiveBlockMultiplier = 1; // 31 bytes (for CH343)
2478
+ this._adaptiveMaxInFlightMultiplier = 1; // 31 bytes
2479
+ this._consecutiveSuccessfulChunks = 0; // Reset success counter
2480
+ const maxTransferSize = this.port.maxTransferSize || 64;
2481
+ const baseBlockSize = Math.floor((maxTransferSize - 2) / 2);
2482
+ const newBlockSize = baseBlockSize * this._adaptiveBlockMultiplier;
2483
+ const newMaxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
2484
+ this.logger.debug(`Error at higher speed - reduced to minimum: blockSize=${newBlockSize}, maxInFlight=${newMaxInFlight}`);
2485
+ }
2486
+ else {
2487
+ // Already at minimum and still failing - this is a real error
2488
+ this.logger.debug(`Error at minimum speed (blockSize=31, maxInFlight=31) - not a speed issue`);
2489
+ }
2490
+ }
1662
2491
  // Check if it's a timeout error or SLIP error
1663
2492
  if (err instanceof SlipReadError) {
1664
2493
  if (retryCount <= MAX_RETRIES) {
@@ -1676,10 +2505,11 @@ export class ESPLoader extends EventTarget {
1676
2505
  }
1677
2506
  }
1678
2507
  else {
1679
- // All retries exhausted - attempt deep recovery by reconnecting and reloading stub
2508
+ // All retries exhausted - attempt recovery by reloading stub
2509
+ // IMPORTANT: Do NOT close port to keep ESP32 in bootloader mode
1680
2510
  if (!deepRecoveryAttempted) {
1681
2511
  deepRecoveryAttempted = true;
1682
- this.logger.log(`All retries exhausted at 0x${currentAddr.toString(16)}. Attempting deep recovery (reconnect + reload stub)...`);
2512
+ this.logger.log(`All retries exhausted at 0x${currentAddr.toString(16)}. Attempting recovery (close and reopen port)...`);
1683
2513
  try {
1684
2514
  // Reconnect will close port, reopen, and reload stub
1685
2515
  await this.reconnect();
@@ -1688,13 +2518,13 @@ export class ESPLoader extends EventTarget {
1688
2518
  retryCount = 0;
1689
2519
  continue;
1690
2520
  }
1691
- catch (reconnectErr) {
1692
- throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and deep recovery failed: ${reconnectErr}`);
2521
+ catch (recoveryErr) {
2522
+ throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery failed: ${recoveryErr}`);
1693
2523
  }
1694
2524
  }
1695
2525
  else {
1696
- // Deep recovery already attempted, give up
1697
- throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and deep recovery attempt`);
2526
+ // Recovery already attempted, give up
2527
+ throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery attempt`);
1698
2528
  }
1699
2529
  }
1700
2530
  }
@@ -1712,7 +2542,6 @@ export class ESPLoader extends EventTarget {
1712
2542
  remainingSize -= chunkSize;
1713
2543
  this.logger.debug(`Total progress: 0x${allData.length.toString(16)} from 0x${size.toString(16)} bytes`);
1714
2544
  }
1715
- this.logger.debug(`Successfully read ${allData.length} bytes from flash`);
1716
2545
  return allData;
1717
2546
  }
1718
2547
  }