tasmota-webserial-esptool 7.3.4 → 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.
package/src/esp_loader.ts CHANGED
@@ -62,10 +62,20 @@ import {
62
62
  } from "./const";
63
63
  import { getStubCode } from "./stubs";
64
64
  import { hexFormatter, sleep, slipEncode, toHex } from "./util";
65
- // @ts-expect-error pako ESM module doesn't have proper type definitions
66
- import { deflate } from "pako/dist/pako.esm.mjs";
65
+ import { deflate } from "pako";
67
66
  import { pack, unpack } from "./struct";
68
67
 
68
+ // Interface for WebUSB Serial Port (extends SerialPort with WebUSB-specific methods)
69
+ interface WebUSBSerialPort extends SerialPort {
70
+ isWebUSB?: boolean;
71
+ maxTransferSize?: number;
72
+ setSignals(signals: {
73
+ dataTerminalReady?: boolean;
74
+ requestToSend?: boolean;
75
+ }): Promise<void>;
76
+ setBaudRate(baudRate: number): Promise<void>;
77
+ }
78
+
69
79
  export class ESPLoader extends EventTarget {
70
80
  chipFamily!: ChipFamily;
71
81
  chipName: string | null = null;
@@ -79,6 +89,7 @@ export class ESPLoader extends EventTarget {
79
89
  flashSize: string | null = null;
80
90
 
81
91
  __inputBuffer?: number[];
92
+ __inputBufferReadIndex?: number;
82
93
  __totalBytesRead?: number;
83
94
  private _currentBaudRate: number = ESP_ROM_BAUD;
84
95
  private _maxUSBSerialBaudrate?: number;
@@ -87,6 +98,15 @@ export class ESPLoader extends EventTarget {
87
98
  private _initializationSucceeded: boolean = false;
88
99
  private __commandLock: Promise<[number, number[]]> = Promise.resolve([0, []]);
89
100
  private __isReconfiguring: boolean = false;
101
+ private __abandonCurrentOperation: boolean = false;
102
+
103
+ // Adaptive speed adjustment for flash read operations - DISABLED
104
+ // Using fixed conservative values that work reliably
105
+ private __adaptiveBlockMultiplier: number = 1;
106
+ private __adaptiveMaxInFlightMultiplier: number = 1;
107
+ private __consecutiveSuccessfulChunks: number = 0;
108
+ private __lastAdaptiveAdjustment: number = 0;
109
+ private __isCDCDevice: boolean = false;
90
110
 
91
111
  constructor(
92
112
  public port: SerialPort,
@@ -100,6 +120,51 @@ export class ESPLoader extends EventTarget {
100
120
  return this._parent ? this._parent._inputBuffer : this.__inputBuffer!;
101
121
  }
102
122
 
123
+ private get _inputBufferReadIndex(): number {
124
+ return this._parent
125
+ ? this._parent._inputBufferReadIndex
126
+ : this.__inputBufferReadIndex || 0;
127
+ }
128
+
129
+ private set _inputBufferReadIndex(value: number) {
130
+ if (this._parent) {
131
+ this._parent._inputBufferReadIndex = value;
132
+ } else {
133
+ this.__inputBufferReadIndex = value;
134
+ }
135
+ }
136
+
137
+ // Get available bytes in buffer (from read index to end)
138
+ private get _inputBufferAvailable(): number {
139
+ return this._inputBuffer.length - this._inputBufferReadIndex;
140
+ }
141
+
142
+ // Read one byte from buffer (ring-buffer style with index pointer)
143
+ private _readByte(): number | undefined {
144
+ if (this._inputBufferReadIndex >= this._inputBuffer.length) {
145
+ return undefined;
146
+ }
147
+ return this._inputBuffer[this._inputBufferReadIndex++];
148
+ }
149
+
150
+ // Clear input buffer and reset read index
151
+ private _clearInputBuffer(): void {
152
+ this._inputBuffer.length = 0;
153
+ this._inputBufferReadIndex = 0;
154
+ }
155
+
156
+ // Compact buffer when read index gets too large (prevent memory growth)
157
+ private _compactInputBuffer(): void {
158
+ if (
159
+ this._inputBufferReadIndex > 1000 &&
160
+ this._inputBufferReadIndex > this._inputBuffer.length / 2
161
+ ) {
162
+ // Remove already-read bytes and reset index
163
+ this._inputBuffer.splice(0, this._inputBufferReadIndex);
164
+ this._inputBufferReadIndex = 0;
165
+ }
166
+ }
167
+
103
168
  private get _totalBytesRead(): number {
104
169
  return this._parent
105
170
  ? this._parent._totalBytesRead
@@ -140,6 +205,88 @@ export class ESPLoader extends EventTarget {
140
205
  }
141
206
  }
142
207
 
208
+ private get _abandonCurrentOperation(): boolean {
209
+ return this._parent
210
+ ? this._parent._abandonCurrentOperation
211
+ : this.__abandonCurrentOperation;
212
+ }
213
+
214
+ private set _abandonCurrentOperation(value: boolean) {
215
+ if (this._parent) {
216
+ this._parent._abandonCurrentOperation = value;
217
+ } else {
218
+ this.__abandonCurrentOperation = value;
219
+ }
220
+ }
221
+
222
+ private get _adaptiveBlockMultiplier(): number {
223
+ return this._parent
224
+ ? this._parent._adaptiveBlockMultiplier
225
+ : this.__adaptiveBlockMultiplier;
226
+ }
227
+
228
+ private set _adaptiveBlockMultiplier(value: number) {
229
+ if (this._parent) {
230
+ this._parent._adaptiveBlockMultiplier = value;
231
+ } else {
232
+ this.__adaptiveBlockMultiplier = value;
233
+ }
234
+ }
235
+
236
+ private get _adaptiveMaxInFlightMultiplier(): number {
237
+ return this._parent
238
+ ? this._parent._adaptiveMaxInFlightMultiplier
239
+ : this.__adaptiveMaxInFlightMultiplier;
240
+ }
241
+
242
+ private set _adaptiveMaxInFlightMultiplier(value: number) {
243
+ if (this._parent) {
244
+ this._parent._adaptiveMaxInFlightMultiplier = value;
245
+ } else {
246
+ this.__adaptiveMaxInFlightMultiplier = value;
247
+ }
248
+ }
249
+
250
+ private get _consecutiveSuccessfulChunks(): number {
251
+ return this._parent
252
+ ? this._parent._consecutiveSuccessfulChunks
253
+ : this.__consecutiveSuccessfulChunks;
254
+ }
255
+
256
+ private set _consecutiveSuccessfulChunks(value: number) {
257
+ if (this._parent) {
258
+ this._parent._consecutiveSuccessfulChunks = value;
259
+ } else {
260
+ this.__consecutiveSuccessfulChunks = value;
261
+ }
262
+ }
263
+
264
+ private get _lastAdaptiveAdjustment(): number {
265
+ return this._parent
266
+ ? this._parent._lastAdaptiveAdjustment
267
+ : this.__lastAdaptiveAdjustment;
268
+ }
269
+
270
+ private set _lastAdaptiveAdjustment(value: number) {
271
+ if (this._parent) {
272
+ this._parent._lastAdaptiveAdjustment = value;
273
+ } else {
274
+ this.__lastAdaptiveAdjustment = value;
275
+ }
276
+ }
277
+
278
+ private get _isCDCDevice(): boolean {
279
+ return this._parent ? this._parent._isCDCDevice : this.__isCDCDevice;
280
+ }
281
+
282
+ private set _isCDCDevice(value: boolean) {
283
+ if (this._parent) {
284
+ this._parent._isCDCDevice = value;
285
+ } else {
286
+ this.__isCDCDevice = value;
287
+ }
288
+ }
289
+
143
290
  private detectUSBSerialChip(
144
291
  vendorId: number,
145
292
  productId: number,
@@ -196,6 +343,7 @@ export class ESPLoader extends EventTarget {
196
343
  async initialize() {
197
344
  if (!this._parent) {
198
345
  this.__inputBuffer = [];
346
+ this.__inputBufferReadIndex = 0;
199
347
  this.__totalBytesRead = 0;
200
348
 
201
349
  // Detect and log USB-Serial chip info
@@ -216,6 +364,15 @@ export class ESPLoader extends EventTarget {
216
364
  if (portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x2) {
217
365
  this._isESP32S2NativeUSB = true;
218
366
  }
367
+
368
+ // Detect CDC devices for adaptive speed adjustment
369
+ // Espressif Native USB (VID: 0x303a) or CH343 (VID: 0x1a86, PID: 0x55d3)
370
+ if (
371
+ portInfo.usbVendorId === 0x303a ||
372
+ (portInfo.usbVendorId === 0x1a86 && portInfo.usbProductId === 0x55d3)
373
+ ) {
374
+ this._isCDCDevice = true;
375
+ }
219
376
  }
220
377
 
221
378
  // Don't await this promise so it doesn't block rest of method.
@@ -291,7 +448,7 @@ export class ESPLoader extends EventTarget {
291
448
  await this.drainInputBuffer(200);
292
449
 
293
450
  // Clear input buffer and re-sync to recover from failed command
294
- this._inputBuffer.length = 0;
451
+ this._clearInputBuffer();
295
452
  await sleep(SYNC_TIMEOUT);
296
453
 
297
454
  // Re-sync with the chip to ensure clean communication
@@ -467,6 +624,11 @@ export class ESPLoader extends EventTarget {
467
624
  }
468
625
 
469
626
  state_DTR = false;
627
+
628
+ // ============================================================================
629
+ // Web Serial (Desktop) - DTR/RTS Signal Handling & Reset Strategies
630
+ // ============================================================================
631
+
470
632
  async setRTS(state: boolean) {
471
633
  await this.port.setSignals({ requestToSend: state });
472
634
  // Work-around for adapters on Windows using the usbser.sys driver:
@@ -481,6 +643,576 @@ export class ESPLoader extends EventTarget {
481
643
  await this.port.setSignals({ dataTerminalReady: state });
482
644
  }
483
645
 
646
+ /**
647
+ * @name hardResetUSBJTAGSerial
648
+ * USB-JTAG/Serial reset for Web Serial (Desktop)
649
+ */
650
+ async hardResetUSBJTAGSerial() {
651
+ await this.setRTS(false);
652
+ await this.setDTR(false); // Idle
653
+ await this.sleep(100);
654
+
655
+ await this.setDTR(true); // Set IO0
656
+ await this.setRTS(false);
657
+ await this.sleep(100);
658
+
659
+ await this.setRTS(true); // Reset
660
+ await this.setDTR(false);
661
+ await this.setRTS(true);
662
+ await this.sleep(100);
663
+
664
+ await this.setDTR(false);
665
+ await this.setRTS(false); // Chip out of reset
666
+
667
+ await this.sleep(200);
668
+ }
669
+
670
+ /**
671
+ * @name hardResetClassic
672
+ * Classic reset for Web Serial (Desktop)
673
+ */
674
+ async hardResetClassic() {
675
+ await this.setDTR(false); // IO0=HIGH
676
+ await this.setRTS(true); // EN=LOW, chip in reset
677
+ await this.sleep(100);
678
+ await this.setDTR(true); // IO0=LOW
679
+ await this.setRTS(false); // EN=HIGH, chip out of reset
680
+ await this.sleep(50);
681
+ await this.setDTR(false); // IO0=HIGH, done
682
+
683
+ await this.sleep(200);
684
+ }
685
+
686
+ // ============================================================================
687
+ // WebUSB (Android) - DTR/RTS Signal Handling & Reset Strategies
688
+ // ============================================================================
689
+
690
+ async setRTSWebUSB(state: boolean) {
691
+ // Always specify both signals to avoid flipping the other line
692
+ // The WebUSB setSignals() now preserves unspecified signals, but being explicit is safer
693
+ await (this.port as WebUSBSerialPort).setSignals({
694
+ requestToSend: state,
695
+ dataTerminalReady: this.state_DTR,
696
+ });
697
+ }
698
+
699
+ async setDTRWebUSB(state: boolean) {
700
+ this.state_DTR = state;
701
+ // Always specify both signals to avoid flipping the other line
702
+ await (this.port as WebUSBSerialPort).setSignals({
703
+ dataTerminalReady: state,
704
+ requestToSend: undefined, // Let setSignals preserve current RTS state
705
+ });
706
+ }
707
+
708
+ async setDTRandRTSWebUSB(dtr: boolean, rts: boolean) {
709
+ this.state_DTR = dtr;
710
+ await (this.port as WebUSBSerialPort).setSignals({
711
+ dataTerminalReady: dtr,
712
+ requestToSend: rts,
713
+ });
714
+ }
715
+
716
+ /**
717
+ * @name hardResetUSBJTAGSerialWebUSB
718
+ * USB-JTAG/Serial reset for WebUSB (Android)
719
+ */
720
+ async hardResetUSBJTAGSerialWebUSB() {
721
+ await this.setRTSWebUSB(false);
722
+ await this.setDTRWebUSB(false); // Idle
723
+ await this.sleep(100);
724
+
725
+ await this.setDTRWebUSB(true); // Set IO0
726
+ await this.setRTSWebUSB(false);
727
+ await this.sleep(100);
728
+
729
+ await this.setRTSWebUSB(true); // Reset
730
+ await this.setDTRWebUSB(false);
731
+ await this.setRTSWebUSB(true);
732
+ await this.sleep(100);
733
+
734
+ await this.setDTRWebUSB(false);
735
+ await this.setRTSWebUSB(false); // Chip out of reset
736
+
737
+ await this.sleep(200);
738
+ }
739
+
740
+ /**
741
+ * @name hardResetUSBJTAGSerialInvertedDTRWebUSB
742
+ * USB-JTAG/Serial reset with inverted DTR for WebUSB (Android)
743
+ */
744
+ async hardResetUSBJTAGSerialInvertedDTRWebUSB() {
745
+ await this.setRTSWebUSB(false);
746
+ await this.setDTRWebUSB(true); // Idle (DTR inverted)
747
+ await this.sleep(100);
748
+
749
+ await this.setDTRWebUSB(false); // Set IO0 (DTR inverted)
750
+ await this.setRTSWebUSB(false);
751
+ await this.sleep(100);
752
+
753
+ await this.setRTSWebUSB(true); // Reset
754
+ await this.setDTRWebUSB(true); // (DTR inverted)
755
+ await this.setRTSWebUSB(true);
756
+ await this.sleep(100);
757
+
758
+ await this.setDTRWebUSB(true); // (DTR inverted)
759
+ await this.setRTSWebUSB(false); // Chip out of reset
760
+
761
+ await this.sleep(200);
762
+ }
763
+
764
+ /**
765
+ * @name hardResetClassicWebUSB
766
+ * Classic reset for WebUSB (Android)
767
+ */
768
+ async hardResetClassicWebUSB() {
769
+ await this.setDTRWebUSB(false); // IO0=HIGH
770
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
771
+ await this.sleep(100);
772
+ await this.setDTRWebUSB(true); // IO0=LOW
773
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
774
+ await this.sleep(50);
775
+ await this.setDTRWebUSB(false); // IO0=HIGH, done
776
+ await this.sleep(200);
777
+ }
778
+
779
+ /**
780
+ * @name hardResetUnixTightWebUSB
781
+ * Unix Tight reset for WebUSB (Android) - sets DTR and RTS simultaneously
782
+ */
783
+ async hardResetUnixTightWebUSB() {
784
+ await this.setDTRandRTSWebUSB(false, false);
785
+ await this.setDTRandRTSWebUSB(true, true);
786
+ await this.setDTRandRTSWebUSB(false, true); // IO0=HIGH & EN=LOW, chip in reset
787
+ await this.sleep(100);
788
+ await this.setDTRandRTSWebUSB(true, false); // IO0=LOW & EN=HIGH, chip out of reset
789
+ await this.sleep(50);
790
+ await this.setDTRandRTSWebUSB(false, false); // IO0=HIGH, done
791
+ await this.setDTRWebUSB(false); // Ensure IO0=HIGH
792
+ await this.sleep(200);
793
+ }
794
+
795
+ /**
796
+ * @name hardResetClassicLongDelayWebUSB
797
+ * Classic reset with longer delays for WebUSB (Android)
798
+ * Specifically for CP2102/CH340 which may need more time
799
+ */
800
+ async hardResetClassicLongDelayWebUSB() {
801
+ await this.setDTRWebUSB(false); // IO0=HIGH
802
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
803
+ await this.sleep(500); // Extra long delay
804
+ await this.setDTRWebUSB(true); // IO0=LOW
805
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
806
+ await this.sleep(200);
807
+ await this.setDTRWebUSB(false); // IO0=HIGH, done
808
+ await this.sleep(500); // Extra long delay
809
+ }
810
+
811
+ /**
812
+ * @name hardResetClassicShortDelayWebUSB
813
+ * Classic reset with shorter delays for WebUSB (Android)
814
+ */
815
+ async hardResetClassicShortDelayWebUSB() {
816
+ await this.setDTRWebUSB(false); // IO0=HIGH
817
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
818
+ await this.sleep(50);
819
+ await this.setDTRWebUSB(true); // IO0=LOW
820
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
821
+ await this.sleep(25);
822
+ await this.setDTRWebUSB(false); // IO0=HIGH, done
823
+ await this.sleep(100);
824
+ }
825
+
826
+ /**
827
+ * @name hardResetInvertedWebUSB
828
+ * Inverted reset sequence for WebUSB (Android) - both signals inverted
829
+ */
830
+ async hardResetInvertedWebUSB() {
831
+ await this.setDTRWebUSB(true); // IO0=HIGH (inverted)
832
+ await this.setRTSWebUSB(false); // EN=LOW, chip in reset (inverted)
833
+ await this.sleep(100);
834
+ await this.setDTRWebUSB(false); // IO0=LOW (inverted)
835
+ await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (inverted)
836
+ await this.sleep(50);
837
+ await this.setDTRWebUSB(true); // IO0=HIGH, done (inverted)
838
+ await this.sleep(200);
839
+ }
840
+
841
+ /**
842
+ * @name hardResetInvertedDTRWebUSB
843
+ * Only DTR inverted for WebUSB (Android)
844
+ */
845
+ async hardResetInvertedDTRWebUSB() {
846
+ await this.setDTRWebUSB(true); // IO0=HIGH (DTR inverted)
847
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset (RTS normal)
848
+ await this.sleep(100);
849
+ await this.setDTRWebUSB(false); // IO0=LOW (DTR inverted)
850
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset (RTS normal)
851
+ await this.sleep(50);
852
+ await this.setDTRWebUSB(true); // IO0=HIGH, done (DTR inverted)
853
+ await this.sleep(200);
854
+ }
855
+
856
+ /**
857
+ * @name hardResetInvertedRTSWebUSB
858
+ * Only RTS inverted for WebUSB (Android)
859
+ */
860
+ async hardResetInvertedRTSWebUSB() {
861
+ await this.setDTRWebUSB(false); // IO0=HIGH (DTR normal)
862
+ await this.setRTSWebUSB(false); // EN=LOW, chip in reset (RTS inverted)
863
+ await this.sleep(100);
864
+ await this.setDTRWebUSB(true); // IO0=LOW (DTR normal)
865
+ await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (RTS inverted)
866
+ await this.sleep(50);
867
+ await this.setDTRWebUSB(false); // IO0=HIGH, done (DTR normal)
868
+ await this.sleep(200);
869
+ }
870
+
871
+ /**
872
+ * Check if we're using WebUSB (Android) or Web Serial (Desktop)
873
+ */
874
+ private isWebUSB(): boolean {
875
+ // WebUSBSerial class has isWebUSB flag - this is the most reliable check
876
+ return (this.port as WebUSBSerialPort).isWebUSB === true;
877
+ }
878
+
879
+ /**
880
+ * @name connectWithResetStrategies
881
+ * Try different reset strategies to enter bootloader mode
882
+ * Similar to esptool.py's connect() method with multiple reset strategies
883
+ */
884
+ async connectWithResetStrategies() {
885
+ const portInfo = this.port.getInfo();
886
+ const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
887
+ const isEspressifUSB = portInfo.usbVendorId === 0x303a;
888
+
889
+ // this.logger.log(
890
+ // `Detected USB: VID=0x${portInfo.usbVendorId?.toString(16) || "unknown"}, PID=0x${portInfo.usbProductId?.toString(16) || "unknown"}`,
891
+ // );
892
+
893
+ // Define reset strategies to try in order
894
+ const resetStrategies: Array<{ name: string; fn: () => Promise<void> }> =
895
+ [];
896
+
897
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
898
+ const self = this;
899
+
900
+ // WebUSB (Android) uses different reset methods than Web Serial (Desktop)
901
+ if (this.isWebUSB()) {
902
+ // For USB-Serial chips (CP2102, CH340, etc.), try inverted strategies first
903
+ const isUSBSerialChip = !isUSBJTAGSerial && !isEspressifUSB;
904
+
905
+ // Detect specific chip types once
906
+ const isCP2102 = portInfo.usbVendorId === 0x10c4;
907
+ const isCH34x = portInfo.usbVendorId === 0x1a86;
908
+
909
+ // Check for ESP32-S2 Native USB (VID: 0x303a, PID: 0x0002)
910
+ const isESP32S2NativeUSB =
911
+ portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x0002;
912
+
913
+ // WebUSB Strategy 1: USB-JTAG/Serial reset (for Native USB only)
914
+ if (isUSBJTAGSerial || isEspressifUSB) {
915
+ if (isESP32S2NativeUSB) {
916
+ // ESP32-S2 Native USB: Try multiple strategies
917
+ // The device might be in JTAG mode OR CDC mode
918
+
919
+ // Strategy 1: USB-JTAG/Serial (works in CDC mode on Desktop)
920
+ resetStrategies.push({
921
+ name: "USB-JTAG/Serial (WebUSB) - ESP32-S2",
922
+ fn: async function () {
923
+ return await self.hardResetUSBJTAGSerialWebUSB();
924
+ },
925
+ });
926
+
927
+ // Strategy 2: USB-JTAG/Serial Inverted DTR (works in JTAG mode)
928
+ resetStrategies.push({
929
+ name: "USB-JTAG/Serial Inverted DTR (WebUSB) - ESP32-S2",
930
+ fn: async function () {
931
+ return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
932
+ },
933
+ });
934
+
935
+ // Strategy 3: UnixTight (CDC fallback)
936
+ resetStrategies.push({
937
+ name: "UnixTight (WebUSB) - ESP32-S2 CDC",
938
+ fn: async function () {
939
+ return await self.hardResetUnixTightWebUSB();
940
+ },
941
+ });
942
+
943
+ // Strategy 4: Classic reset (CDC fallback)
944
+ resetStrategies.push({
945
+ name: "Classic (WebUSB) - ESP32-S2 CDC",
946
+ fn: async function () {
947
+ return await self.hardResetClassicWebUSB();
948
+ },
949
+ });
950
+ } else {
951
+ // Other USB-JTAG chips: Try Inverted DTR first - works best for ESP32-H2 and other JTAG chips
952
+ resetStrategies.push({
953
+ name: "USB-JTAG/Serial Inverted DTR (WebUSB)",
954
+ fn: async function () {
955
+ return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
956
+ },
957
+ });
958
+ resetStrategies.push({
959
+ name: "USB-JTAG/Serial (WebUSB)",
960
+ fn: async function () {
961
+ return await self.hardResetUSBJTAGSerialWebUSB();
962
+ },
963
+ });
964
+ resetStrategies.push({
965
+ name: "Inverted DTR Classic (WebUSB)",
966
+ fn: async function () {
967
+ return await self.hardResetInvertedDTRWebUSB();
968
+ },
969
+ });
970
+ }
971
+ }
972
+
973
+ // For USB-Serial chips, try inverted strategies first
974
+ if (isUSBSerialChip) {
975
+ if (isCH34x) {
976
+ // CH340/CH343: UnixTight works best (like CP2102)
977
+ resetStrategies.push({
978
+ name: "UnixTight (WebUSB) - CH34x",
979
+ fn: async function () {
980
+ return await self.hardResetUnixTightWebUSB();
981
+ },
982
+ });
983
+ resetStrategies.push({
984
+ name: "Classic (WebUSB) - CH34x",
985
+ fn: async function () {
986
+ return await self.hardResetClassicWebUSB();
987
+ },
988
+ });
989
+ resetStrategies.push({
990
+ name: "Inverted Both (WebUSB) - CH34x",
991
+ fn: async function () {
992
+ return await self.hardResetInvertedWebUSB();
993
+ },
994
+ });
995
+ resetStrategies.push({
996
+ name: "Inverted RTS (WebUSB) - CH34x",
997
+ fn: async function () {
998
+ return await self.hardResetInvertedRTSWebUSB();
999
+ },
1000
+ });
1001
+ resetStrategies.push({
1002
+ name: "Inverted DTR (WebUSB) - CH34x",
1003
+ fn: async function () {
1004
+ return await self.hardResetInvertedDTRWebUSB();
1005
+ },
1006
+ });
1007
+ } else if (isCP2102) {
1008
+ // CP2102: UnixTight works best (tested and confirmed)
1009
+ // Try it first, then fallback to other strategies
1010
+
1011
+ resetStrategies.push({
1012
+ name: "UnixTight (WebUSB) - CP2102",
1013
+ fn: async function () {
1014
+ return await self.hardResetUnixTightWebUSB();
1015
+ },
1016
+ });
1017
+
1018
+ resetStrategies.push({
1019
+ name: "Classic (WebUSB) - CP2102",
1020
+ fn: async function () {
1021
+ return await self.hardResetClassicWebUSB();
1022
+ },
1023
+ });
1024
+
1025
+ resetStrategies.push({
1026
+ name: "Inverted Both (WebUSB) - CP2102",
1027
+ fn: async function () {
1028
+ return await self.hardResetInvertedWebUSB();
1029
+ },
1030
+ });
1031
+
1032
+ resetStrategies.push({
1033
+ name: "Inverted RTS (WebUSB) - CP2102",
1034
+ fn: async function () {
1035
+ return await self.hardResetInvertedRTSWebUSB();
1036
+ },
1037
+ });
1038
+
1039
+ resetStrategies.push({
1040
+ name: "Inverted DTR (WebUSB) - CP2102",
1041
+ fn: async function () {
1042
+ return await self.hardResetInvertedDTRWebUSB();
1043
+ },
1044
+ });
1045
+ } else {
1046
+ // For other USB-Serial chips, try UnixTight first, then multiple strategies
1047
+ resetStrategies.push({
1048
+ name: "UnixTight (WebUSB)",
1049
+ fn: async function () {
1050
+ return await self.hardResetUnixTightWebUSB();
1051
+ },
1052
+ });
1053
+ resetStrategies.push({
1054
+ name: "Classic (WebUSB)",
1055
+ fn: async function () {
1056
+ return await self.hardResetClassicWebUSB();
1057
+ },
1058
+ });
1059
+ resetStrategies.push({
1060
+ name: "Inverted Both (WebUSB)",
1061
+ fn: async function () {
1062
+ return await self.hardResetInvertedWebUSB();
1063
+ },
1064
+ });
1065
+ resetStrategies.push({
1066
+ name: "Inverted RTS (WebUSB)",
1067
+ fn: async function () {
1068
+ return await self.hardResetInvertedRTSWebUSB();
1069
+ },
1070
+ });
1071
+ resetStrategies.push({
1072
+ name: "Inverted DTR (WebUSB)",
1073
+ fn: async function () {
1074
+ return await self.hardResetInvertedDTRWebUSB();
1075
+ },
1076
+ });
1077
+ }
1078
+ }
1079
+
1080
+ // Add general fallback strategies only for non-CP2102 and non-ESP32-S2 Native USB chips
1081
+ if (!isCP2102 && !isESP32S2NativeUSB) {
1082
+ // Classic reset (for chips not handled above)
1083
+ if (portInfo.usbVendorId !== 0x1a86) {
1084
+ resetStrategies.push({
1085
+ name: "Classic (WebUSB)",
1086
+ fn: async function () {
1087
+ return await self.hardResetClassicWebUSB();
1088
+ },
1089
+ });
1090
+ }
1091
+
1092
+ // UnixTight reset (sets DTR/RTS simultaneously)
1093
+ resetStrategies.push({
1094
+ name: "UnixTight (WebUSB)",
1095
+ fn: async function () {
1096
+ return await self.hardResetUnixTightWebUSB();
1097
+ },
1098
+ });
1099
+
1100
+ // WebUSB Strategy: Classic with long delays
1101
+ resetStrategies.push({
1102
+ name: "Classic Long Delay (WebUSB)",
1103
+ fn: async function () {
1104
+ return await self.hardResetClassicLongDelayWebUSB();
1105
+ },
1106
+ });
1107
+
1108
+ // WebUSB Strategy: Classic with short delays
1109
+ resetStrategies.push({
1110
+ name: "Classic Short Delay (WebUSB)",
1111
+ fn: async function () {
1112
+ return await self.hardResetClassicShortDelayWebUSB();
1113
+ },
1114
+ });
1115
+
1116
+ // WebUSB Strategy: USB-JTAG/Serial fallback
1117
+ if (!isUSBJTAGSerial && !isEspressifUSB) {
1118
+ resetStrategies.push({
1119
+ name: "USB-JTAG/Serial fallback (WebUSB)",
1120
+ fn: async function () {
1121
+ return await self.hardResetUSBJTAGSerialWebUSB();
1122
+ },
1123
+ });
1124
+ }
1125
+ }
1126
+ } else {
1127
+ // Web Serial (Desktop) strategies
1128
+ // Strategy: USB-JTAG/Serial reset
1129
+ if (isUSBJTAGSerial || isEspressifUSB) {
1130
+ resetStrategies.push({
1131
+ name: "USB-JTAG/Serial",
1132
+ fn: async function () {
1133
+ return await self.hardResetUSBJTAGSerial();
1134
+ },
1135
+ });
1136
+ }
1137
+
1138
+ // Strategy: Classic reset
1139
+ resetStrategies.push({
1140
+ name: "Classic",
1141
+ fn: async function () {
1142
+ return await self.hardResetClassic();
1143
+ },
1144
+ });
1145
+
1146
+ // Strategy: USB-JTAG/Serial fallback
1147
+ if (!isUSBJTAGSerial && !isEspressifUSB) {
1148
+ resetStrategies.push({
1149
+ name: "USB-JTAG/Serial (fallback)",
1150
+ fn: async function () {
1151
+ return await self.hardResetUSBJTAGSerial();
1152
+ },
1153
+ });
1154
+ }
1155
+ }
1156
+
1157
+ let lastError: Error | null = null;
1158
+
1159
+ // Try each reset strategy with timeout
1160
+ for (const strategy of resetStrategies) {
1161
+ try {
1162
+ // Check if port is still open, if not, skip this strategy
1163
+ if (!this.connected || !this.port.writable) {
1164
+ this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
1165
+ continue;
1166
+ }
1167
+
1168
+ // Clear abandon flag before starting new strategy
1169
+ this._abandonCurrentOperation = false;
1170
+
1171
+ await strategy.fn();
1172
+
1173
+ // Try to sync after reset with internally time-bounded sync (3 seconds per strategy)
1174
+ const syncSuccess = await this.syncWithTimeout(3000);
1175
+
1176
+ if (syncSuccess) {
1177
+ // Sync succeeded
1178
+ this.logger.log(
1179
+ `Connected successfully with ${strategy.name} reset.`,
1180
+ );
1181
+ return;
1182
+ } else {
1183
+ throw new Error("Sync timeout or abandoned");
1184
+ }
1185
+ } catch (error) {
1186
+ lastError = error as Error;
1187
+ this.logger.log(
1188
+ `${strategy.name} reset failed: ${(error as Error).message}`,
1189
+ );
1190
+
1191
+ // Set abandon flag to stop any in-flight operations
1192
+ this._abandonCurrentOperation = true;
1193
+
1194
+ // Wait a bit for in-flight operations to abort
1195
+ await sleep(100);
1196
+
1197
+ // If port got disconnected, we can't try more strategies
1198
+ if (!this.connected || !this.port.writable) {
1199
+ this.logger.log(`Port disconnected during reset attempt`);
1200
+ break;
1201
+ }
1202
+
1203
+ // Clear buffers before trying next strategy
1204
+ this._clearInputBuffer();
1205
+ await this.drainInputBuffer(200);
1206
+ await this.flushSerialBuffers();
1207
+ }
1208
+ }
1209
+
1210
+ // All strategies failed
1211
+ throw new Error(
1212
+ `Couldn't sync to ESP. Try resetting manually. Last error: ${lastError?.message}`,
1213
+ );
1214
+ }
1215
+
484
1216
  async hardReset(bootloader = false) {
485
1217
  if (bootloader) {
486
1218
  // enter flash mode
@@ -488,15 +1220,31 @@ export class ESPLoader extends EventTarget {
488
1220
  await this.hardResetUSBJTAGSerial();
489
1221
  this.logger.log("USB-JTAG/Serial reset.");
490
1222
  } else {
491
- await this.hardResetClassic();
492
- this.logger.log("Classic reset.");
1223
+ // Use different reset strategy for WebUSB (Android) vs Web Serial (Desktop)
1224
+ if (this.isWebUSB()) {
1225
+ await this.hardResetClassicWebUSB();
1226
+ this.logger.log("Classic reset (WebUSB/Android).");
1227
+ } else {
1228
+ await this.hardResetClassic();
1229
+ this.logger.log("Classic reset.");
1230
+ }
493
1231
  }
494
1232
  } else {
495
- // just reset
496
- await this.setRTS(true); // EN->LOW
497
- await this.sleep(100);
498
- await this.setRTS(false);
499
- this.logger.log("Hard reset.");
1233
+ // just reset (no bootloader mode)
1234
+ if (this.isWebUSB()) {
1235
+ // WebUSB: Use longer delays for better compatibility
1236
+ await this.setRTS(true); // EN->LOW
1237
+ await this.sleep(200);
1238
+ await this.setRTS(false);
1239
+ await this.sleep(200);
1240
+ this.logger.log("Hard reset (WebUSB).");
1241
+ } else {
1242
+ // Web Serial: Standard reset
1243
+ await this.setRTS(true); // EN->LOW
1244
+ await this.sleep(100);
1245
+ await this.setRTS(false);
1246
+ this.logger.log("Hard reset.");
1247
+ }
500
1248
  }
501
1249
  await new Promise((resolve) => setTimeout(resolve, 1000));
502
1250
  }
@@ -627,6 +1375,13 @@ export class ESPLoader extends EventTarget {
627
1375
  statusLen = 4;
628
1376
  } else if ([2, 4].includes(data.length)) {
629
1377
  statusLen = data.length;
1378
+ } else {
1379
+ // Default to 2-byte status if we can't determine
1380
+ // This prevents silent data corruption when statusLen would be 0
1381
+ statusLen = 2;
1382
+ this.logger.debug(
1383
+ `Unknown chip family, defaulting to 2-byte status (opcode: ${toHex(opcode)}, data.length: ${data.length})`,
1384
+ );
630
1385
  }
631
1386
  }
632
1387
 
@@ -670,6 +1425,7 @@ export class ESPLoader extends EventTarget {
670
1425
  ...pack("<BBHI", 0x00, opcode, buffer.length, checksum),
671
1426
  ...buffer,
672
1427
  ]);
1428
+
673
1429
  if (this.debug) {
674
1430
  this.logger.debug(
675
1431
  `Writing ${packet.length} byte${packet.length == 1 ? "" : "s"}:`,
@@ -683,81 +1439,181 @@ export class ESPLoader extends EventTarget {
683
1439
  * @name readPacket
684
1440
  * Generator to read SLIP packets from a serial port.
685
1441
  * Yields one full SLIP packet at a time, raises exception on timeout or invalid data.
1442
+ *
1443
+ * Two implementations:
1444
+ * - Burst: CDC devices (Native USB) and CH343 - very fast processing
1445
+ * - Byte-by-byte: CH340, CP2102, and other USB-Serial adapters - stable fast processing
686
1446
  */
687
-
688
1447
  async readPacket(timeout: number): Promise<number[]> {
689
1448
  let partialPacket: number[] | null = null;
690
1449
  let inEscape = false;
691
- let readBytes: number[] = [];
692
- while (true) {
693
- const stamp = Date.now();
694
- readBytes = [];
695
- while (Date.now() - stamp < timeout) {
696
- if (this._inputBuffer.length > 0) {
697
- readBytes.push(this._inputBuffer.shift()!);
698
- break;
699
- } else {
700
- // Reduced sleep time for faster response during high-speed transfers
1450
+
1451
+ // CDC devices use burst processing, non-CDC use byte-by-byte
1452
+ if (this._isCDCDevice) {
1453
+ // Burst version: Process all available bytes in one pass for ultra-high-speed transfers
1454
+ // Used for: CDC devices (all platforms) and CH343
1455
+ const startTime = Date.now();
1456
+
1457
+ while (true) {
1458
+ // Check abandon flag (for reset strategy timeout)
1459
+ if (this._abandonCurrentOperation) {
1460
+ throw new SlipReadError(
1461
+ "Operation abandoned (reset strategy timeout)",
1462
+ );
1463
+ }
1464
+
1465
+ // Check timeout
1466
+ if (Date.now() - startTime > timeout) {
1467
+ const waitingFor = partialPacket === null ? "header" : "content";
1468
+ throw new SlipReadError("Timed out waiting for packet " + waitingFor);
1469
+ }
1470
+
1471
+ // If no data available, wait a bit
1472
+ if (this._inputBufferAvailable === 0) {
701
1473
  await sleep(1);
1474
+ continue;
702
1475
  }
703
- }
704
- if (readBytes.length == 0) {
705
- const waitingFor = partialPacket === null ? "header" : "content";
706
- throw new SlipReadError("Timed out waiting for packet " + waitingFor);
707
- }
708
- if (this.debug)
709
- this.logger.debug(
710
- "Read " + readBytes.length + " bytes: " + hexFormatter(readBytes),
711
- );
712
- for (const b of readBytes) {
713
- if (partialPacket === null) {
714
- // waiting for packet header
715
- if (b == 0xc0) {
716
- partialPacket = [];
717
- } else {
718
- if (this.debug) {
719
- this.logger.debug("Read invalid data: " + toHex(b));
720
- this.logger.debug(
721
- "Remaining data in serial buffer: " +
722
- hexFormatter(this._inputBuffer),
1476
+
1477
+ // Process all available bytes without going back to outer loop
1478
+ // This is critical for handling high-speed burst transfers
1479
+ while (this._inputBufferAvailable > 0) {
1480
+ const b = this._readByte()!;
1481
+
1482
+ if (partialPacket === null) {
1483
+ // waiting for packet header
1484
+ if (b == 0xc0) {
1485
+ partialPacket = [];
1486
+ } else {
1487
+ if (this.debug) {
1488
+ this.logger.debug("Read invalid data: " + toHex(b));
1489
+ this.logger.debug(
1490
+ "Remaining data in serial buffer: " +
1491
+ hexFormatter(this._inputBuffer),
1492
+ );
1493
+ }
1494
+ throw new SlipReadError(
1495
+ "Invalid head of packet (" + toHex(b) + ")",
723
1496
  );
724
1497
  }
725
- throw new SlipReadError(
726
- "Invalid head of packet (" + toHex(b) + ")",
727
- );
1498
+ } else if (inEscape) {
1499
+ // part-way through escape sequence
1500
+ inEscape = false;
1501
+ if (b == 0xdc) {
1502
+ partialPacket.push(0xc0);
1503
+ } else if (b == 0xdd) {
1504
+ partialPacket.push(0xdb);
1505
+ } else {
1506
+ if (this.debug) {
1507
+ this.logger.debug("Read invalid data: " + toHex(b));
1508
+ this.logger.debug(
1509
+ "Remaining data in serial buffer: " +
1510
+ hexFormatter(this._inputBuffer),
1511
+ );
1512
+ }
1513
+ throw new SlipReadError(
1514
+ "Invalid SLIP escape (0xdb, " + toHex(b) + ")",
1515
+ );
1516
+ }
1517
+ } else if (b == 0xdb) {
1518
+ // start of escape sequence
1519
+ inEscape = true;
1520
+ } else if (b == 0xc0) {
1521
+ // end of packet
1522
+ if (this.debug)
1523
+ this.logger.debug(
1524
+ "Received full packet: " + hexFormatter(partialPacket),
1525
+ );
1526
+ // Compact buffer periodically to prevent memory growth
1527
+ this._compactInputBuffer();
1528
+ return partialPacket;
1529
+ } else {
1530
+ // normal byte in packet
1531
+ partialPacket.push(b);
728
1532
  }
729
- } else if (inEscape) {
730
- // part-way through escape sequence
731
- inEscape = false;
732
- if (b == 0xdc) {
733
- partialPacket.push(0xc0);
734
- } else if (b == 0xdd) {
735
- partialPacket.push(0xdb);
1533
+ }
1534
+ }
1535
+ } else {
1536
+ // Byte-by-byte version: Stable for non CDC USB-Serial adapters (CH340, CP2102, etc.)
1537
+ let readBytes: number[] = [];
1538
+ while (true) {
1539
+ // Check abandon flag (for reset strategy timeout)
1540
+ if (this._abandonCurrentOperation) {
1541
+ throw new SlipReadError(
1542
+ "Operation abandoned (reset strategy timeout)",
1543
+ );
1544
+ }
1545
+
1546
+ const stamp = Date.now();
1547
+ readBytes = [];
1548
+ while (Date.now() - stamp < timeout) {
1549
+ if (this._inputBufferAvailable > 0) {
1550
+ readBytes.push(this._readByte()!);
1551
+ break;
736
1552
  } else {
737
- if (this.debug) {
738
- this.logger.debug("Read invalid data: " + toHex(b));
739
- this.logger.debug(
740
- "Remaining data in serial buffer: " +
741
- hexFormatter(this._inputBuffer),
1553
+ // Reduced sleep time for faster response during high-speed transfers
1554
+ await sleep(1);
1555
+ }
1556
+ }
1557
+ if (readBytes.length == 0) {
1558
+ const waitingFor = partialPacket === null ? "header" : "content";
1559
+ throw new SlipReadError("Timed out waiting for packet " + waitingFor);
1560
+ }
1561
+ if (this.debug)
1562
+ this.logger.debug(
1563
+ "Read " + readBytes.length + " bytes: " + hexFormatter(readBytes),
1564
+ );
1565
+ for (const b of readBytes) {
1566
+ if (partialPacket === null) {
1567
+ // waiting for packet header
1568
+ if (b == 0xc0) {
1569
+ partialPacket = [];
1570
+ } else {
1571
+ if (this.debug) {
1572
+ this.logger.debug("Read invalid data: " + toHex(b));
1573
+ this.logger.debug(
1574
+ "Remaining data in serial buffer: " +
1575
+ hexFormatter(this._inputBuffer),
1576
+ );
1577
+ }
1578
+ throw new SlipReadError(
1579
+ "Invalid head of packet (" + toHex(b) + ")",
742
1580
  );
743
1581
  }
744
- throw new SlipReadError(
745
- "Invalid SLIP escape (0xdb, " + toHex(b) + ")",
746
- );
1582
+ } else if (inEscape) {
1583
+ // part-way through escape sequence
1584
+ inEscape = false;
1585
+ if (b == 0xdc) {
1586
+ partialPacket.push(0xc0);
1587
+ } else if (b == 0xdd) {
1588
+ partialPacket.push(0xdb);
1589
+ } else {
1590
+ if (this.debug) {
1591
+ this.logger.debug("Read invalid data: " + toHex(b));
1592
+ this.logger.debug(
1593
+ "Remaining data in serial buffer: " +
1594
+ hexFormatter(this._inputBuffer),
1595
+ );
1596
+ }
1597
+ throw new SlipReadError(
1598
+ "Invalid SLIP escape (0xdb, " + toHex(b) + ")",
1599
+ );
1600
+ }
1601
+ } else if (b == 0xdb) {
1602
+ // start of escape sequence
1603
+ inEscape = true;
1604
+ } else if (b == 0xc0) {
1605
+ // end of packet
1606
+ if (this.debug)
1607
+ this.logger.debug(
1608
+ "Received full packet: " + hexFormatter(partialPacket),
1609
+ );
1610
+ // Compact buffer periodically to prevent memory growth
1611
+ this._compactInputBuffer();
1612
+ return partialPacket;
1613
+ } else {
1614
+ // normal byte in packet
1615
+ partialPacket.push(b);
747
1616
  }
748
- } else if (b == 0xdb) {
749
- // start of escape sequence
750
- inEscape = true;
751
- } else if (b == 0xc0) {
752
- // end of packet
753
- if (this.debug)
754
- this.logger.debug(
755
- "Received full packet: " + hexFormatter(partialPacket),
756
- );
757
- return partialPacket;
758
- } else {
759
- // normal byte in packet
760
- partialPacket.push(b);
761
1617
  }
762
1618
  }
763
1619
  }
@@ -781,6 +1637,7 @@ export class ESPLoader extends EventTarget {
781
1637
  }
782
1638
 
783
1639
  const [resp, opRet, , val] = unpack("<BBHI", packet.slice(0, 8));
1640
+
784
1641
  if (resp != 1) {
785
1642
  continue;
786
1643
  }
@@ -795,7 +1652,7 @@ export class ESPLoader extends EventTarget {
795
1652
  throw new Error(`Invalid (unsupported) command ${toHex(opcode)}`);
796
1653
  }
797
1654
  }
798
- throw "Response doesn't match request";
1655
+ throw new Error("Response doesn't match request");
799
1656
  }
800
1657
 
801
1658
  /**
@@ -858,6 +1715,9 @@ export class ESPLoader extends EventTarget {
858
1715
  }
859
1716
 
860
1717
  async reconfigurePort(baud: number) {
1718
+ // Block new writes during the entire reconfiguration (all paths)
1719
+ this._isReconfiguring = true;
1720
+
861
1721
  try {
862
1722
  // Wait for pending writes to complete
863
1723
  try {
@@ -866,9 +1726,35 @@ export class ESPLoader extends EventTarget {
866
1726
  this.logger.debug(`Pending write error during reconfigure: ${err}`);
867
1727
  }
868
1728
 
869
- // Block new writes during port close/open
870
- this._isReconfiguring = true;
1729
+ // WebUSB: Check if we should use setBaudRate() or close/reopen
1730
+ if (this.isWebUSB()) {
1731
+ const portInfo = this.port.getInfo();
1732
+ const isCH343 =
1733
+ portInfo.usbVendorId === 0x1a86 && portInfo.usbProductId === 0x55d3;
1734
+
1735
+ // CH343 is a CDC device and MUST use close/reopen
1736
+ // Other chips (CH340, CP2102, FTDI) MUST use setBaudRate()
1737
+ if (
1738
+ !isCH343 &&
1739
+ typeof (this.port as WebUSBSerialPort).setBaudRate === "function"
1740
+ ) {
1741
+ // this.logger.log(
1742
+ // `[WebUSB] Changing baudrate to ${baud} using setBaudRate()...`,
1743
+ // );
1744
+ await (this.port as WebUSBSerialPort).setBaudRate(baud);
1745
+ // this.logger.log(`[WebUSB] Baudrate changed to ${baud}`);
1746
+
1747
+ // Give the chip time to adjust to new baudrate
1748
+ await sleep(100);
1749
+ return;
1750
+ } else if (isCH343) {
1751
+ // this.logger.log(
1752
+ // `[WebUSB] CH343 detected - using close/reopen for baudrate change`,
1753
+ // );
1754
+ }
1755
+ }
871
1756
 
1757
+ // Web Serial or CH343: Close and reopen port
872
1758
  // Release persistent writer before closing
873
1759
  if (this._writer) {
874
1760
  try {
@@ -889,148 +1775,59 @@ export class ESPLoader extends EventTarget {
889
1775
  // Reopen Port
890
1776
  await this.port.open({ baudRate: baud });
891
1777
 
892
- // Port is now open - allow writes again
893
- this._isReconfiguring = false;
894
-
895
1778
  // Clear buffer again
896
1779
  await this.flushSerialBuffers();
897
1780
 
898
1781
  // Restart Readloop
899
1782
  this.readLoop();
900
1783
  } catch (e) {
901
- this._isReconfiguring = false;
902
1784
  this.logger.error(`Reconfigure port error: ${e}`);
903
1785
  throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
1786
+ } finally {
1787
+ // Always reset flag, even on error or early return
1788
+ this._isReconfiguring = false;
904
1789
  }
905
1790
  }
906
1791
 
907
1792
  /**
908
- * @name connectWithResetStrategies
909
- * Try different reset strategies to enter bootloader mode
910
- * Similar to esptool.py's connect() method with multiple reset strategies
1793
+ * @name syncWithTimeout
1794
+ * Sync with timeout that can be abandoned (for reset strategy loop)
1795
+ * This is internally time-bounded and checks the abandon flag
911
1796
  */
912
- async connectWithResetStrategies() {
913
- const portInfo = this.port.getInfo();
914
- const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
915
- const isEspressifUSB = portInfo.usbVendorId === 0x303a;
1797
+ async syncWithTimeout(timeoutMs: number): Promise<boolean> {
1798
+ const startTime = Date.now();
916
1799
 
917
- this.logger.log(
918
- `Detected USB: VID=0x${portInfo.usbVendorId?.toString(16) || "unknown"}, PID=0x${portInfo.usbProductId?.toString(16) || "unknown"}`,
919
- );
920
-
921
- // Define reset strategies to try in order
922
- const resetStrategies: Array<{ name: string; fn: () => Promise<void> }> =
923
- [];
924
-
925
- // Strategy 1: USB-JTAG/Serial reset (for ESP32-C3, C6, S3, etc.)
926
- // Try this first if we detect Espressif USB VID or the specific PID
927
- if (isUSBJTAGSerial || isEspressifUSB) {
928
- resetStrategies.push({
929
- name: "USB-JTAG/Serial",
930
- fn: async () => await this.hardResetUSBJTAGSerial(),
931
- });
932
- }
933
-
934
- // Strategy 2: Classic reset (for USB-to-Serial bridges)
935
- resetStrategies.push({
936
- name: "Classic",
937
- fn: async () => await this.hardResetClassic(),
938
- });
1800
+ for (let i = 0; i < 5; i++) {
1801
+ // Check if we've exceeded the timeout
1802
+ if (Date.now() - startTime > timeoutMs) {
1803
+ return false;
1804
+ }
939
1805
 
940
- // Strategy 3: If USB-JTAG/Serial was not tried yet, try it as fallback
941
- if (!isUSBJTAGSerial && !isEspressifUSB) {
942
- resetStrategies.push({
943
- name: "USB-JTAG/Serial (fallback)",
944
- fn: async () => await this.hardResetUSBJTAGSerial(),
945
- });
946
- }
1806
+ // Check abandon flag
1807
+ if (this._abandonCurrentOperation) {
1808
+ return false;
1809
+ }
947
1810
 
948
- let lastError: Error | null = null;
1811
+ this._clearInputBuffer();
949
1812
 
950
- // Try each reset strategy
951
- for (const strategy of resetStrategies) {
952
1813
  try {
953
- this.logger.log(`Trying ${strategy.name} reset...`);
954
-
955
- // Check if port is still open, if not, skip this strategy
956
- if (!this.connected || !this.port.writable) {
957
- this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
958
- continue;
1814
+ const response = await this._sync();
1815
+ if (response) {
1816
+ await sleep(SYNC_TIMEOUT);
1817
+ return true;
959
1818
  }
960
-
961
- await strategy.fn();
962
-
963
- // Try to sync after reset
964
- await this.sync();
965
-
966
- // If we get here, sync succeeded
967
- this.logger.log(`Connected successfully with ${strategy.name} reset.`);
968
- return;
969
- } catch (error) {
970
- lastError = error as Error;
971
- this.logger.log(
972
- `${strategy.name} reset failed: ${(error as Error).message}`,
973
- );
974
-
975
- // If port got disconnected, we can't try more strategies
976
- if (!this.connected || !this.port.writable) {
977
- this.logger.log(`Port disconnected during reset attempt`);
978
- break;
1819
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1820
+ } catch (e) {
1821
+ // Check abandon flag after error
1822
+ if (this._abandonCurrentOperation) {
1823
+ return false;
979
1824
  }
980
-
981
- // Clear buffers before trying next strategy
982
- this._inputBuffer.length = 0;
983
- await this.drainInputBuffer(200);
984
- await this.flushSerialBuffers();
985
1825
  }
986
- }
987
1826
 
988
- // All strategies failed
989
- throw new Error(
990
- `Couldn't sync to ESP. Try resetting manually. Last error: ${lastError?.message}`,
991
- );
992
- }
993
-
994
- /**
995
- * @name hardResetUSBJTAGSerial
996
- * USB-JTAG/Serial reset sequence for ESP32-C3, ESP32-S3, ESP32-C6, etc.
997
- */
998
- async hardResetUSBJTAGSerial() {
999
- await this.setRTS(false);
1000
- await this.setDTR(false); // Idle
1001
- await this.sleep(100);
1002
-
1003
- await this.setDTR(true); // Set IO0
1004
- await this.setRTS(false);
1005
- await this.sleep(100);
1006
-
1007
- await this.setRTS(true); // Reset. Calls inverted to go through (1,1) instead of (0,0)
1008
- await this.setDTR(false);
1009
- await this.setRTS(true); // RTS set as Windows only propagates DTR on RTS setting
1010
- await this.sleep(100);
1011
-
1012
- await this.setDTR(false);
1013
- await this.setRTS(false); // Chip out of reset
1014
-
1015
- // Wait for chip to boot into bootloader
1016
- await this.sleep(200);
1017
- }
1018
-
1019
- /**
1020
- * @name hardResetClassic
1021
- * Classic reset sequence for USB-to-Serial bridge chips (CH340, CP2102, etc.)
1022
- */
1023
- async hardResetClassic() {
1024
- await this.setDTR(false); // IO0=HIGH
1025
- await this.setRTS(true); // EN=LOW, chip in reset
1026
- await this.sleep(100);
1027
- await this.setDTR(true); // IO0=LOW
1028
- await this.setRTS(false); // EN=HIGH, chip out of reset
1029
- await this.sleep(50);
1030
- await this.setDTR(false); // IO0=HIGH, done
1827
+ await sleep(SYNC_TIMEOUT);
1828
+ }
1031
1829
 
1032
- // Wait for chip to boot into bootloader
1033
- await this.sleep(200);
1830
+ return false;
1034
1831
  }
1035
1832
 
1036
1833
  /**
@@ -1040,7 +1837,7 @@ export class ESPLoader extends EventTarget {
1040
1837
  */
1041
1838
  async sync() {
1042
1839
  for (let i = 0; i < 5; i++) {
1043
- this._inputBuffer.length = 0;
1840
+ this._clearInputBuffer();
1044
1841
  const response = await this._sync();
1045
1842
  if (response) {
1046
1843
  await sleep(SYNC_TIMEOUT);
@@ -1059,14 +1856,17 @@ export class ESPLoader extends EventTarget {
1059
1856
  */
1060
1857
  async _sync() {
1061
1858
  await this.sendCommand(ESP_SYNC, SYNC_PACKET);
1859
+
1062
1860
  for (let i = 0; i < 8; i++) {
1063
1861
  try {
1064
1862
  const [, data] = await this.getResponse(ESP_SYNC, SYNC_TIMEOUT);
1065
1863
  if (data.length > 1 && data[0] == 0 && data[1] == 0) {
1066
1864
  return true;
1067
1865
  }
1068
- } catch {
1069
- // If read packet fails.
1866
+ } catch (e) {
1867
+ if (this.debug) {
1868
+ this.logger.debug(`Sync attempt ${i + 1} failed: ${e}`);
1869
+ }
1070
1870
  }
1071
1871
  }
1072
1872
  return false;
@@ -1710,13 +2510,21 @@ export class ESPLoader extends EventTarget {
1710
2510
  },
1711
2511
  async () => {
1712
2512
  // Previous write failed, but still attempt this write
2513
+ this.logger.debug(
2514
+ "Previous write failed, attempting recovery for current write",
2515
+ );
1713
2516
  if (!this.port.writable) {
1714
2517
  throw new Error("Port became unavailable during write");
1715
2518
  }
1716
2519
 
1717
2520
  // Writer was likely cleaned up by previous error, create new one
1718
2521
  if (!this._writer) {
1719
- this._writer = this.port.writable.getWriter();
2522
+ try {
2523
+ this._writer = this.port.writable.getWriter();
2524
+ } catch (err) {
2525
+ this.logger.debug(`Failed to get writer in recovery: ${err}`);
2526
+ throw new Error("Cannot acquire writer lock");
2527
+ }
1720
2528
  }
1721
2529
 
1722
2530
  await this._writer.write(new Uint8Array(data));
@@ -1728,7 +2536,7 @@ export class ESPLoader extends EventTarget {
1728
2536
  if (this._writer) {
1729
2537
  try {
1730
2538
  this._writer.releaseLock();
1731
- } catch (e) {
2539
+ } catch {
1732
2540
  // Ignore release errors
1733
2541
  }
1734
2542
  this._writer = undefined;
@@ -1786,6 +2594,7 @@ export class ESPLoader extends EventTarget {
1786
2594
  await new Promise((resolve) => {
1787
2595
  if (!this._reader) {
1788
2596
  resolve(undefined);
2597
+ return;
1789
2598
  }
1790
2599
  this.addEventListener("disconnect", resolve, { once: true });
1791
2600
  this._reader!.cancel();
@@ -1811,6 +2620,7 @@ export class ESPLoader extends EventTarget {
1811
2620
 
1812
2621
  this.connected = false;
1813
2622
  this.__inputBuffer = [];
2623
+ this.__inputBufferReadIndex = 0;
1814
2624
 
1815
2625
  // Wait for pending writes to complete
1816
2626
  try {
@@ -1881,6 +2691,7 @@ export class ESPLoader extends EventTarget {
1881
2691
 
1882
2692
  if (!this._parent) {
1883
2693
  this.__inputBuffer = [];
2694
+ this.__inputBufferReadIndex = 0;
1884
2695
  this.__totalBytesRead = 0;
1885
2696
  this.readLoop();
1886
2697
  }
@@ -1918,10 +2729,11 @@ export class ESPLoader extends EventTarget {
1918
2729
  }
1919
2730
  }
1920
2731
 
1921
- // Copy stub state to this instance if we're a stub loader
1922
- if (this.IS_STUB) {
1923
- Object.assign(this, stubLoader);
1924
- }
2732
+ // The stub is now running on the chip
2733
+ // stubLoader has this instance as _parent, so all operations go through this
2734
+ // We just need to mark this instance as running stub code
2735
+ this.IS_STUB = true;
2736
+
1925
2737
  this.logger.debug("Reconnection successful");
1926
2738
  } catch (err) {
1927
2739
  // Ensure flag is reset on error
@@ -1956,8 +2768,8 @@ export class ESPLoader extends EventTarget {
1956
2768
  const drainTimeout = 100; // Short timeout for draining
1957
2769
 
1958
2770
  while (drained < bytesToDrain && Date.now() - drainStart < drainTimeout) {
1959
- if (this._inputBuffer.length > 0) {
1960
- const byte = this._inputBuffer.shift();
2771
+ if (this._inputBufferAvailable > 0) {
2772
+ const byte = this._readByte();
1961
2773
  if (byte !== undefined) {
1962
2774
  drained++;
1963
2775
  }
@@ -1974,6 +2786,7 @@ export class ESPLoader extends EventTarget {
1974
2786
  // Final clear of application buffer
1975
2787
  if (!this._parent) {
1976
2788
  this.__inputBuffer = [];
2789
+ this.__inputBufferReadIndex = 0;
1977
2790
  }
1978
2791
  }
1979
2792
 
@@ -1986,6 +2799,7 @@ export class ESPLoader extends EventTarget {
1986
2799
  // Clear application buffer
1987
2800
  if (!this._parent) {
1988
2801
  this.__inputBuffer = [];
2802
+ this.__inputBufferReadIndex = 0;
1989
2803
  }
1990
2804
 
1991
2805
  // Wait for any pending data
@@ -1994,6 +2808,7 @@ export class ESPLoader extends EventTarget {
1994
2808
  // Final clear
1995
2809
  if (!this._parent) {
1996
2810
  this.__inputBuffer = [];
2811
+ this.__inputBufferReadIndex = 0;
1997
2812
  }
1998
2813
 
1999
2814
  this.logger.debug("Serial buffers flushed");
@@ -2029,7 +2844,38 @@ export class ESPLoader extends EventTarget {
2029
2844
  `Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`,
2030
2845
  );
2031
2846
 
2032
- const CHUNK_SIZE = 0x10000; // 64KB chunks
2847
+ // Initialize adaptive speed multipliers for WebUSB devices
2848
+ if (this.isWebUSB()) {
2849
+ if (this._isCDCDevice) {
2850
+ // CDC devices (CH343): Start with maximum, adaptive adjustment enabled
2851
+ this._adaptiveBlockMultiplier = 8; // blockSize = 248 bytes
2852
+ this._adaptiveMaxInFlightMultiplier = 8; // maxInFlight = 248 bytes
2853
+ this._consecutiveSuccessfulChunks = 0;
2854
+ this.logger.debug(
2855
+ `CDC device - Initialized: blockMultiplier=${this._adaptiveBlockMultiplier}, maxInFlightMultiplier=${this._adaptiveMaxInFlightMultiplier}`,
2856
+ );
2857
+ } else {
2858
+ // Non-CDC devices (CH340, CP2102): Fixed values, no adaptive adjustment
2859
+ this._adaptiveBlockMultiplier = 1; // blockSize = 31 bytes (fixed)
2860
+ this._adaptiveMaxInFlightMultiplier = 1; // maxInFlight = 31 bytes (fixed)
2861
+ this._consecutiveSuccessfulChunks = 0;
2862
+ this.logger.debug(
2863
+ `Non-CDC device - Fixed values: blockSize=31, maxInFlight=31`,
2864
+ );
2865
+ }
2866
+ }
2867
+
2868
+ // Chunk size: Amount of data to request from ESP in one command
2869
+ // For WebUSB (Android), use smaller chunks to avoid timeouts and buffer issues
2870
+ // For Web Serial (Desktop), use larger chunks for better performance
2871
+ let CHUNK_SIZE: number;
2872
+ if (this.isWebUSB()) {
2873
+ // WebUSB: Use smaller chunks to avoid SLIP timeout issues
2874
+ CHUNK_SIZE = 0x4 * 0x1000; // 4KB = 16384 bytes
2875
+ } else {
2876
+ // Web Serial: Use larger chunks for better performance
2877
+ CHUNK_SIZE = 0x40 * 0x1000;
2878
+ }
2033
2879
 
2034
2880
  let allData = new Uint8Array(0);
2035
2881
  let currentAddr = addr;
@@ -2045,6 +2891,7 @@ export class ESPLoader extends EventTarget {
2045
2891
  // Retry loop for this chunk
2046
2892
  while (!chunkSuccess && retryCount <= MAX_RETRIES) {
2047
2893
  let resp = new Uint8Array(0);
2894
+ let lastAckedLength = 0; // Track last acknowledged length
2048
2895
 
2049
2896
  try {
2050
2897
  // Only log on first attempt or retries
@@ -2054,9 +2901,34 @@ export class ESPLoader extends EventTarget {
2054
2901
  );
2055
2902
  }
2056
2903
 
2057
- // Send read flash command for this chunk
2058
- // This must be inside the retry loop so we send a fresh command after errors
2059
- const pkt = pack("<IIII", currentAddr, chunkSize, 0x1000, 1024);
2904
+ let blockSize: number;
2905
+ let maxInFlight: number;
2906
+
2907
+ if (this.isWebUSB()) {
2908
+ // WebUSB (Android): All devices use adaptive speed
2909
+ // All have maxTransferSize=64, baseBlockSize=31
2910
+ const maxTransferSize =
2911
+ (this.port as WebUSBSerialPort).maxTransferSize || 64;
2912
+ const baseBlockSize = Math.floor((maxTransferSize - 2) / 2); // 31 bytes
2913
+
2914
+ // Use current adaptive multipliers (initialized at start of readFlash)
2915
+ blockSize = baseBlockSize * this._adaptiveBlockMultiplier;
2916
+ maxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
2917
+ } else {
2918
+ // Web Serial (Desktop): Use multiples of 63 for consistency
2919
+ const base = 63;
2920
+ blockSize = base * 65; // 63 * 65 = 4095 (close to 0x1000)
2921
+ maxInFlight = base * 130; // 63 * 130 = 8190 (close to blockSize * 2)
2922
+ }
2923
+
2924
+ const pkt = pack(
2925
+ "<IIII",
2926
+ currentAddr,
2927
+ chunkSize,
2928
+ blockSize,
2929
+ maxInFlight,
2930
+ );
2931
+
2060
2932
  const [res] = await this.checkCommand(ESP_READ_FLASH, pkt);
2061
2933
 
2062
2934
  if (res != 0) {
@@ -2108,10 +2980,21 @@ export class ESPLoader extends EventTarget {
2108
2980
  newResp.set(packetData, resp.length);
2109
2981
  resp = newResp;
2110
2982
 
2111
- // Send acknowledgment
2112
- const ackData = pack("<I", resp.length);
2113
- const slipEncodedAck = slipEncode(ackData);
2114
- await this.writeToStream(slipEncodedAck);
2983
+ // Send acknowledgment after receiving maxInFlight bytes
2984
+ // This unblocks the stub to send the next batch of packets
2985
+ const shouldAck =
2986
+ resp.length >= chunkSize || // End of chunk
2987
+ resp.length >= lastAckedLength + maxInFlight; // Received all packets
2988
+
2989
+ if (shouldAck) {
2990
+ const ackData = pack("<I", resp.length);
2991
+ const slipEncodedAck = slipEncode(ackData);
2992
+ await this.writeToStream(slipEncodedAck);
2993
+
2994
+ // Update lastAckedLength to current response length
2995
+ // This ensures next ACK is sent at the right time
2996
+ lastAckedLength = resp.length;
2997
+ }
2115
2998
  }
2116
2999
  }
2117
3000
 
@@ -2122,9 +3005,93 @@ export class ESPLoader extends EventTarget {
2122
3005
  allData = newAllData;
2123
3006
 
2124
3007
  chunkSuccess = true;
3008
+
3009
+ // ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
3010
+ // Non-CDC devices (CH340, CP2102) stay at fixed blockSize=31, maxInFlight=31
3011
+ if (this.isWebUSB() && this._isCDCDevice && retryCount === 0) {
3012
+ this._consecutiveSuccessfulChunks++;
3013
+
3014
+ // After 2 consecutive successful chunks, increase speed gradually
3015
+ if (this._consecutiveSuccessfulChunks >= 2) {
3016
+ const maxTransferSize =
3017
+ (this.port as WebUSBSerialPort).maxTransferSize || 64;
3018
+ const baseBlockSize = Math.floor((maxTransferSize - 2) / 2); // 31 bytes
3019
+
3020
+ // Maximum: blockSize=248 (8 * 31), maxInFlight=248 (8 * 31)
3021
+ const MAX_BLOCK_MULTIPLIER = 8; // 248 bytes - tested stable
3022
+ const MAX_INFLIGHT_MULTIPLIER = 8; // 248 bytes - tested stable
3023
+
3024
+ let adjusted = false;
3025
+
3026
+ // Increase blockSize first (up to 248), then maxInFlight
3027
+ if (this._adaptiveBlockMultiplier < MAX_BLOCK_MULTIPLIER) {
3028
+ this._adaptiveBlockMultiplier = Math.min(
3029
+ this._adaptiveBlockMultiplier * 2,
3030
+ MAX_BLOCK_MULTIPLIER,
3031
+ );
3032
+ adjusted = true;
3033
+ }
3034
+ // Once blockSize is at maximum, increase maxInFlight
3035
+ else if (
3036
+ this._adaptiveMaxInFlightMultiplier < MAX_INFLIGHT_MULTIPLIER
3037
+ ) {
3038
+ this._adaptiveMaxInFlightMultiplier = Math.min(
3039
+ this._adaptiveMaxInFlightMultiplier * 2,
3040
+ MAX_INFLIGHT_MULTIPLIER,
3041
+ );
3042
+ adjusted = true;
3043
+ }
3044
+
3045
+ if (adjusted) {
3046
+ const newBlockSize =
3047
+ baseBlockSize * this._adaptiveBlockMultiplier;
3048
+ const newMaxInFlight =
3049
+ baseBlockSize * this._adaptiveMaxInFlightMultiplier;
3050
+ this.logger.debug(
3051
+ `Speed increased: blockSize=${newBlockSize}, maxInFlight=${newMaxInFlight}`,
3052
+ );
3053
+ this._lastAdaptiveAdjustment = Date.now();
3054
+ }
3055
+
3056
+ // Reset counter
3057
+ this._consecutiveSuccessfulChunks = 0;
3058
+ }
3059
+ }
2125
3060
  } catch (err) {
2126
3061
  retryCount++;
2127
3062
 
3063
+ // ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
3064
+ // Non-CDC devices stay at fixed values
3065
+ if (this.isWebUSB() && this._isCDCDevice && retryCount === 1) {
3066
+ // Only reduce if we're above minimum
3067
+ if (
3068
+ this._adaptiveBlockMultiplier > 1 ||
3069
+ this._adaptiveMaxInFlightMultiplier > 1
3070
+ ) {
3071
+ // Reduce to minimum on error
3072
+ this._adaptiveBlockMultiplier = 1; // 31 bytes (for CH343)
3073
+ this._adaptiveMaxInFlightMultiplier = 1; // 31 bytes
3074
+ this._consecutiveSuccessfulChunks = 0; // Reset success counter
3075
+
3076
+ const maxTransferSize =
3077
+ (this.port as WebUSBSerialPort).maxTransferSize || 64;
3078
+ const baseBlockSize = Math.floor((maxTransferSize - 2) / 2);
3079
+ const newBlockSize =
3080
+ baseBlockSize * this._adaptiveBlockMultiplier;
3081
+ const newMaxInFlight =
3082
+ baseBlockSize * this._adaptiveMaxInFlightMultiplier;
3083
+
3084
+ this.logger.debug(
3085
+ `Error at higher speed - reduced to minimum: blockSize=${newBlockSize}, maxInFlight=${newMaxInFlight}`,
3086
+ );
3087
+ } else {
3088
+ // Already at minimum and still failing - this is a real error
3089
+ this.logger.debug(
3090
+ `Error at minimum speed (blockSize=31, maxInFlight=31) - not a speed issue`,
3091
+ );
3092
+ }
3093
+ }
3094
+
2128
3095
  // Check if it's a timeout error or SLIP error
2129
3096
  if (err instanceof SlipReadError) {
2130
3097
  if (retryCount <= MAX_RETRIES) {
@@ -2146,12 +3113,13 @@ export class ESPLoader extends EventTarget {
2146
3113
  this.logger.debug(`Buffer drain error: ${drainErr}`);
2147
3114
  }
2148
3115
  } else {
2149
- // All retries exhausted - attempt deep recovery by reconnecting and reloading stub
3116
+ // All retries exhausted - attempt recovery by reloading stub
3117
+ // IMPORTANT: Do NOT close port to keep ESP32 in bootloader mode
2150
3118
  if (!deepRecoveryAttempted) {
2151
3119
  deepRecoveryAttempted = true;
2152
3120
 
2153
3121
  this.logger.log(
2154
- `All retries exhausted at 0x${currentAddr.toString(16)}. Attempting deep recovery (reconnect + reload stub)...`,
3122
+ `All retries exhausted at 0x${currentAddr.toString(16)}. Attempting recovery (close and reopen port)...`,
2155
3123
  );
2156
3124
 
2157
3125
  try {
@@ -2165,15 +3133,15 @@ export class ESPLoader extends EventTarget {
2165
3133
  // Reset retry counter to give it another chance after recovery
2166
3134
  retryCount = 0;
2167
3135
  continue;
2168
- } catch (reconnectErr) {
3136
+ } catch (recoveryErr) {
2169
3137
  throw new Error(
2170
- `Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and deep recovery failed: ${reconnectErr}`,
3138
+ `Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery failed: ${recoveryErr}`,
2171
3139
  );
2172
3140
  }
2173
3141
  } else {
2174
- // Deep recovery already attempted, give up
3142
+ // Recovery already attempted, give up
2175
3143
  throw new Error(
2176
- `Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and deep recovery attempt`,
3144
+ `Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery attempt`,
2177
3145
  );
2178
3146
  }
2179
3147
  }
@@ -2197,7 +3165,6 @@ export class ESPLoader extends EventTarget {
2197
3165
  );
2198
3166
  }
2199
3167
 
2200
- this.logger.debug(`Successfully read ${allData.length} bytes from flash`);
2201
3168
  return allData;
2202
3169
  }
2203
3170
  }