tasmota-webserial-esptool 7.3.4 → 9.1.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,76 +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
- let readBytes = [];
520
- while (true) {
521
- const stamp = Date.now();
522
- readBytes = [];
523
- while (Date.now() - stamp < timeout) {
524
- if (this._inputBuffer.length > 0) {
525
- readBytes.push(this._inputBuffer.shift());
526
- break;
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)");
527
1190
  }
528
- else {
529
- // Reduced sleep time for faster response during high-speed transfers
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) {
530
1198
  await sleep(1);
1199
+ continue;
531
1200
  }
532
- }
533
- if (readBytes.length == 0) {
534
- const waitingFor = partialPacket === null ? "header" : "content";
535
- throw new SlipReadError("Timed out waiting for packet " + waitingFor);
536
- }
537
- if (this.debug)
538
- this.logger.debug("Read " + readBytes.length + " bytes: " + hexFormatter(readBytes));
539
- for (const b of readBytes) {
540
- if (partialPacket === null) {
541
- // waiting for packet header
542
- if (b == 0xc0) {
543
- partialPacket = [];
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
+ }
544
1218
  }
545
- 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));
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) + ")");
550
1235
  }
551
- throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
552
1236
  }
553
- }
554
- else if (inEscape) {
555
- // part-way through escape sequence
556
- inEscape = false;
557
- if (b == 0xdc) {
558
- partialPacket.push(0xc0);
1237
+ else if (b == 0xdb) {
1238
+ // start of escape sequence
1239
+ inEscape = true;
559
1240
  }
560
- else if (b == 0xdd) {
561
- 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;
562
1248
  }
563
1249
  else {
564
- if (this.debug) {
565
- this.logger.debug("Read invalid data: " + toHex(b));
566
- this.logger.debug("Remaining data in serial buffer: " +
567
- hexFormatter(this._inputBuffer));
568
- }
569
- throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
1250
+ // normal byte in packet
1251
+ partialPacket.push(b);
570
1252
  }
571
1253
  }
572
- else if (b == 0xdb) {
573
- // start of escape sequence
574
- 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)");
575
1263
  }
576
- else if (b == 0xc0) {
577
- // end of packet
578
- if (this.debug)
579
- this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
580
- return partialPacket;
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
+ }
581
1275
  }
582
- else {
583
- // normal byte in packet
584
- partialPacket.push(b);
1276
+ if (readBytes.length == 0) {
1277
+ const waitingFor = partialPacket === null ? "header" : "content";
1278
+ throw new SlipReadError("Timed out waiting for packet " + waitingFor);
1279
+ }
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
+ }
585
1331
  }
586
1332
  }
587
1333
  }
@@ -613,7 +1359,7 @@ export class ESPLoader extends EventTarget {
613
1359
  throw new Error(`Invalid (unsupported) command ${toHex(opcode)}`);
614
1360
  }
615
1361
  }
616
- throw "Response doesn't match request";
1362
+ throw new Error("Response doesn't match request");
617
1363
  }
618
1364
  /**
619
1365
  * @name checksum
@@ -665,6 +1411,8 @@ export class ESPLoader extends EventTarget {
665
1411
  }
666
1412
  async reconfigurePort(baud) {
667
1413
  var _a;
1414
+ // Block new writes during the entire reconfiguration (all paths)
1415
+ this._isReconfiguring = true;
668
1416
  try {
669
1417
  // Wait for pending writes to complete
670
1418
  try {
@@ -673,8 +1421,30 @@ export class ESPLoader extends EventTarget {
673
1421
  catch (err) {
674
1422
  this.logger.debug(`Pending write error during reconfigure: ${err}`);
675
1423
  }
676
- // Block new writes during port close/open
677
- 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
678
1448
  // Release persistent writer before closing
679
1449
  if (this._writer) {
680
1450
  try {
@@ -693,120 +1463,54 @@ export class ESPLoader extends EventTarget {
693
1463
  await this.port.close();
694
1464
  // Reopen Port
695
1465
  await this.port.open({ baudRate: baud });
696
- // Port is now open - allow writes again
697
- this._isReconfiguring = false;
698
1466
  // Clear buffer again
699
1467
  await this.flushSerialBuffers();
700
1468
  // Restart Readloop
701
1469
  this.readLoop();
702
1470
  }
703
1471
  catch (e) {
704
- this._isReconfiguring = false;
705
1472
  this.logger.error(`Reconfigure port error: ${e}`);
706
1473
  throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
707
1474
  }
1475
+ finally {
1476
+ // Always reset flag, even on error or early return
1477
+ this._isReconfiguring = false;
1478
+ }
708
1479
  }
709
1480
  /**
710
- * @name connectWithResetStrategies
711
- * Try different reset strategies to enter bootloader mode
712
- * 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
713
1484
  */
714
- async connectWithResetStrategies() {
715
- var _a, _b;
716
- const portInfo = this.port.getInfo();
717
- const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
718
- const isEspressifUSB = portInfo.usbVendorId === 0x303a;
719
- 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"}`);
720
- // Define reset strategies to try in order
721
- const resetStrategies = [];
722
- // Strategy 1: USB-JTAG/Serial reset (for ESP32-C3, C6, S3, etc.)
723
- // Try this first if we detect Espressif USB VID or the specific PID
724
- if (isUSBJTAGSerial || isEspressifUSB) {
725
- resetStrategies.push({
726
- name: "USB-JTAG/Serial",
727
- fn: async () => await this.hardResetUSBJTAGSerial(),
728
- });
729
- }
730
- // Strategy 2: Classic reset (for USB-to-Serial bridges)
731
- resetStrategies.push({
732
- name: "Classic",
733
- fn: async () => await this.hardResetClassic(),
734
- });
735
- // Strategy 3: If USB-JTAG/Serial was not tried yet, try it as fallback
736
- if (!isUSBJTAGSerial && !isEspressifUSB) {
737
- resetStrategies.push({
738
- name: "USB-JTAG/Serial (fallback)",
739
- fn: async () => await this.hardResetUSBJTAGSerial(),
740
- });
741
- }
742
- let lastError = null;
743
- // Try each reset strategy
744
- 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();
745
1497
  try {
746
- this.logger.log(`Trying ${strategy.name} reset...`);
747
- // Check if port is still open, if not, skip this strategy
748
- if (!this.connected || !this.port.writable) {
749
- this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
750
- continue;
1498
+ const response = await this._sync();
1499
+ if (response) {
1500
+ await sleep(SYNC_TIMEOUT);
1501
+ return true;
751
1502
  }
752
- await strategy.fn();
753
- // Try to sync after reset
754
- await this.sync();
755
- // If we get here, sync succeeded
756
- this.logger.log(`Connected successfully with ${strategy.name} reset.`);
757
- return;
1503
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
758
1504
  }
759
- catch (error) {
760
- lastError = error;
761
- this.logger.log(`${strategy.name} reset failed: ${error.message}`);
762
- // If port got disconnected, we can't try more strategies
763
- if (!this.connected || !this.port.writable) {
764
- this.logger.log(`Port disconnected during reset attempt`);
765
- break;
1505
+ catch (e) {
1506
+ // Check abandon flag after error
1507
+ if (this._abandonCurrentOperation) {
1508
+ return false;
766
1509
  }
767
- // Clear buffers before trying next strategy
768
- this._inputBuffer.length = 0;
769
- await this.drainInputBuffer(200);
770
- await this.flushSerialBuffers();
771
1510
  }
1511
+ await sleep(SYNC_TIMEOUT);
772
1512
  }
773
- // All strategies failed
774
- throw new Error(`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
775
- }
776
- /**
777
- * @name hardResetUSBJTAGSerial
778
- * USB-JTAG/Serial reset sequence for ESP32-C3, ESP32-S3, ESP32-C6, etc.
779
- */
780
- async hardResetUSBJTAGSerial() {
781
- await this.setRTS(false);
782
- await this.setDTR(false); // Idle
783
- await this.sleep(100);
784
- await this.setDTR(true); // Set IO0
785
- await this.setRTS(false);
786
- await this.sleep(100);
787
- await this.setRTS(true); // Reset. Calls inverted to go through (1,1) instead of (0,0)
788
- await this.setDTR(false);
789
- await this.setRTS(true); // RTS set as Windows only propagates DTR on RTS setting
790
- await this.sleep(100);
791
- await this.setDTR(false);
792
- await this.setRTS(false); // Chip out of reset
793
- // Wait for chip to boot into bootloader
794
- await this.sleep(200);
795
- }
796
- /**
797
- * @name hardResetClassic
798
- * Classic reset sequence for USB-to-Serial bridge chips (CH340, CP2102, etc.)
799
- */
800
- async hardResetClassic() {
801
- await this.setDTR(false); // IO0=HIGH
802
- await this.setRTS(true); // EN=LOW, chip in reset
803
- await this.sleep(100);
804
- await this.setDTR(true); // IO0=LOW
805
- await this.setRTS(false); // EN=HIGH, chip out of reset
806
- await this.sleep(50);
807
- await this.setDTR(false); // IO0=HIGH, done
808
- // Wait for chip to boot into bootloader
809
- await this.sleep(200);
1513
+ return false;
810
1514
  }
811
1515
  /**
812
1516
  * @name sync
@@ -815,7 +1519,7 @@ export class ESPLoader extends EventTarget {
815
1519
  */
816
1520
  async sync() {
817
1521
  for (let i = 0; i < 5; i++) {
818
- this._inputBuffer.length = 0;
1522
+ this._clearInputBuffer();
819
1523
  const response = await this._sync();
820
1524
  if (response) {
821
1525
  await sleep(SYNC_TIMEOUT);
@@ -839,8 +1543,10 @@ export class ESPLoader extends EventTarget {
839
1543
  return true;
840
1544
  }
841
1545
  }
842
- catch {
843
- // If read packet fails.
1546
+ catch (e) {
1547
+ if (this.debug) {
1548
+ this.logger.debug(`Sync attempt ${i + 1} failed: ${e}`);
1549
+ }
844
1550
  }
845
1551
  }
846
1552
  return false;
@@ -1315,12 +2021,19 @@ export class ESPLoader extends EventTarget {
1315
2021
  await this._writer.write(new Uint8Array(data));
1316
2022
  }, async () => {
1317
2023
  // Previous write failed, but still attempt this write
2024
+ this.logger.debug("Previous write failed, attempting recovery for current write");
1318
2025
  if (!this.port.writable) {
1319
2026
  throw new Error("Port became unavailable during write");
1320
2027
  }
1321
2028
  // Writer was likely cleaned up by previous error, create new one
1322
2029
  if (!this._writer) {
1323
- 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
+ }
1324
2037
  }
1325
2038
  await this._writer.write(new Uint8Array(data));
1326
2039
  })
@@ -1331,7 +2044,7 @@ export class ESPLoader extends EventTarget {
1331
2044
  try {
1332
2045
  this._writer.releaseLock();
1333
2046
  }
1334
- catch (e) {
2047
+ catch {
1335
2048
  // Ignore release errors
1336
2049
  }
1337
2050
  this._writer = undefined;
@@ -1387,6 +2100,7 @@ export class ESPLoader extends EventTarget {
1387
2100
  await new Promise((resolve) => {
1388
2101
  if (!this._reader) {
1389
2102
  resolve(undefined);
2103
+ return;
1390
2104
  }
1391
2105
  this.addEventListener("disconnect", resolve, { once: true });
1392
2106
  this._reader.cancel();
@@ -1410,6 +2124,7 @@ export class ESPLoader extends EventTarget {
1410
2124
  this.logger.log("Reconnecting serial port...");
1411
2125
  this.connected = false;
1412
2126
  this.__inputBuffer = [];
2127
+ this.__inputBufferReadIndex = 0;
1413
2128
  // Wait for pending writes to complete
1414
2129
  try {
1415
2130
  await this._writeChain;
@@ -1472,6 +2187,7 @@ export class ESPLoader extends EventTarget {
1472
2187
  await this.hardReset(true);
1473
2188
  if (!this._parent) {
1474
2189
  this.__inputBuffer = [];
2190
+ this.__inputBufferReadIndex = 0;
1475
2191
  this.__totalBytesRead = 0;
1476
2192
  this.readLoop();
1477
2193
  }
@@ -1499,10 +2215,10 @@ export class ESPLoader extends EventTarget {
1499
2215
  throw new Error(`Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
1500
2216
  }
1501
2217
  }
1502
- // Copy stub state to this instance if we're a stub loader
1503
- if (this.IS_STUB) {
1504
- Object.assign(this, stubLoader);
1505
- }
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;
1506
2222
  this.logger.debug("Reconnection successful");
1507
2223
  }
1508
2224
  catch (err) {
@@ -1534,8 +2250,8 @@ export class ESPLoader extends EventTarget {
1534
2250
  const drainStart = Date.now();
1535
2251
  const drainTimeout = 100; // Short timeout for draining
1536
2252
  while (drained < bytesToDrain && Date.now() - drainStart < drainTimeout) {
1537
- if (this._inputBuffer.length > 0) {
1538
- const byte = this._inputBuffer.shift();
2253
+ if (this._inputBufferAvailable > 0) {
2254
+ const byte = this._readByte();
1539
2255
  if (byte !== undefined) {
1540
2256
  drained++;
1541
2257
  }
@@ -1551,6 +2267,7 @@ export class ESPLoader extends EventTarget {
1551
2267
  // Final clear of application buffer
1552
2268
  if (!this._parent) {
1553
2269
  this.__inputBuffer = [];
2270
+ this.__inputBufferReadIndex = 0;
1554
2271
  }
1555
2272
  }
1556
2273
  /**
@@ -1562,12 +2279,14 @@ export class ESPLoader extends EventTarget {
1562
2279
  // Clear application buffer
1563
2280
  if (!this._parent) {
1564
2281
  this.__inputBuffer = [];
2282
+ this.__inputBufferReadIndex = 0;
1565
2283
  }
1566
2284
  // Wait for any pending data
1567
2285
  await sleep(SYNC_TIMEOUT);
1568
2286
  // Final clear
1569
2287
  if (!this._parent) {
1570
2288
  this.__inputBuffer = [];
2289
+ this.__inputBufferReadIndex = 0;
1571
2290
  }
1572
2291
  this.logger.debug("Serial buffers flushed");
1573
2292
  }
@@ -1586,7 +2305,35 @@ export class ESPLoader extends EventTarget {
1586
2305
  // Flush serial buffers before flash read operation
1587
2306
  await this.flushSerialBuffers();
1588
2307
  this.logger.log(`Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`);
1589
- 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
+ }
1590
2337
  let allData = new Uint8Array(0);
1591
2338
  let currentAddr = addr;
1592
2339
  let remainingSize = size;
@@ -1599,14 +2346,30 @@ export class ESPLoader extends EventTarget {
1599
2346
  // Retry loop for this chunk
1600
2347
  while (!chunkSuccess && retryCount <= MAX_RETRIES) {
1601
2348
  let resp = new Uint8Array(0);
2349
+ let lastAckedLength = 0; // Track last acknowledged length
1602
2350
  try {
1603
2351
  // Only log on first attempt or retries
1604
2352
  if (retryCount === 0) {
1605
2353
  this.logger.debug(`Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`);
1606
2354
  }
1607
- // Send read flash command for this chunk
1608
- // This must be inside the retry loop so we send a fresh command after errors
1609
- 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);
1610
2373
  const [res] = await this.checkCommand(ESP_READ_FLASH, pkt);
1611
2374
  if (res != 0) {
1612
2375
  throw new Error("Failed to read memory: " + res);
@@ -1649,10 +2412,18 @@ export class ESPLoader extends EventTarget {
1649
2412
  newResp.set(resp);
1650
2413
  newResp.set(packetData, resp.length);
1651
2414
  resp = newResp;
1652
- // Send acknowledgment
1653
- const ackData = pack("<I", resp.length);
1654
- const slipEncodedAck = slipEncode(ackData);
1655
- 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
+ }
1656
2427
  }
1657
2428
  }
1658
2429
  // Chunk read successfully - append to all data
@@ -1661,9 +2432,62 @@ export class ESPLoader extends EventTarget {
1661
2432
  newAllData.set(resp, allData.length);
1662
2433
  allData = newAllData;
1663
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
+ }
1664
2467
  }
1665
2468
  catch (err) {
1666
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
+ }
1667
2491
  // Check if it's a timeout error or SLIP error
1668
2492
  if (err instanceof SlipReadError) {
1669
2493
  if (retryCount <= MAX_RETRIES) {
@@ -1681,10 +2505,11 @@ export class ESPLoader extends EventTarget {
1681
2505
  }
1682
2506
  }
1683
2507
  else {
1684
- // 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
1685
2510
  if (!deepRecoveryAttempted) {
1686
2511
  deepRecoveryAttempted = true;
1687
- 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)...`);
1688
2513
  try {
1689
2514
  // Reconnect will close port, reopen, and reload stub
1690
2515
  await this.reconnect();
@@ -1693,13 +2518,13 @@ export class ESPLoader extends EventTarget {
1693
2518
  retryCount = 0;
1694
2519
  continue;
1695
2520
  }
1696
- catch (reconnectErr) {
1697
- 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}`);
1698
2523
  }
1699
2524
  }
1700
2525
  else {
1701
- // Deep recovery already attempted, give up
1702
- 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`);
1703
2528
  }
1704
2529
  }
1705
2530
  }
@@ -1717,7 +2542,6 @@ export class ESPLoader extends EventTarget {
1717
2542
  remainingSize -= chunkSize;
1718
2543
  this.logger.debug(`Total progress: 0x${allData.length.toString(16)} from 0x${size.toString(16)} bytes`);
1719
2544
  }
1720
- this.logger.debug(`Successfully read ${allData.length} bytes from flash`);
1721
2545
  return allData;
1722
2546
  }
1723
2547
  }