tasmota-webserial-esptool 7.3.3 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/css/dark.css +74 -0
- package/css/light.css +74 -0
- package/css/style.css +663 -35
- package/dist/esp_loader.d.ts +102 -14
- package/dist/esp_loader.js +1015 -186
- package/dist/index.d.ts +1 -0
- package/dist/index.js +31 -2
- package/dist/stubs/index.d.ts +1 -2
- package/dist/stubs/index.js +4 -0
- package/dist/web/index.js +1 -1
- package/js/modules/esptool.js +1 -1
- package/js/script.js +198 -85
- package/js/webusb-serial.js +1028 -0
- package/package.json +2 -2
- package/src/esp_loader.ts +1184 -216
- package/src/index.ts +38 -2
- package/src/stubs/index.ts +4 -1
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
|
-
|
|
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.
|
|
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
|
-
|
|
492
|
-
this.
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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,80 +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
1450
|
|
|
692
|
-
|
|
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
|
+
}
|
|
693
1464
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
}
|
|
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
|
+
}
|
|
700
1470
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1471
|
+
// If no data available, wait a bit
|
|
1472
|
+
if (this._inputBufferAvailable === 0) {
|
|
1473
|
+
await sleep(1);
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
706
1476
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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()!;
|
|
711
1481
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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) + ")",
|
|
722
1496
|
);
|
|
723
1497
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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);
|
|
727
1532
|
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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;
|
|
735
1552
|
} else {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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) + ")",
|
|
741
1580
|
);
|
|
742
1581
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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);
|
|
746
1616
|
}
|
|
747
|
-
} else if (b == 0xdb) {
|
|
748
|
-
// start of escape sequence
|
|
749
|
-
inEscape = true;
|
|
750
|
-
} else if (b == 0xc0) {
|
|
751
|
-
// end of packet
|
|
752
|
-
if (this.debug)
|
|
753
|
-
this.logger.debug(
|
|
754
|
-
"Received full packet: " + hexFormatter(partialPacket),
|
|
755
|
-
);
|
|
756
|
-
return partialPacket;
|
|
757
|
-
} else {
|
|
758
|
-
// normal byte in packet
|
|
759
|
-
partialPacket.push(b);
|
|
760
1617
|
}
|
|
761
1618
|
}
|
|
762
1619
|
}
|
|
@@ -780,6 +1637,7 @@ export class ESPLoader extends EventTarget {
|
|
|
780
1637
|
}
|
|
781
1638
|
|
|
782
1639
|
const [resp, opRet, , val] = unpack("<BBHI", packet.slice(0, 8));
|
|
1640
|
+
|
|
783
1641
|
if (resp != 1) {
|
|
784
1642
|
continue;
|
|
785
1643
|
}
|
|
@@ -794,7 +1652,7 @@ export class ESPLoader extends EventTarget {
|
|
|
794
1652
|
throw new Error(`Invalid (unsupported) command ${toHex(opcode)}`);
|
|
795
1653
|
}
|
|
796
1654
|
}
|
|
797
|
-
throw "Response doesn't match request";
|
|
1655
|
+
throw new Error("Response doesn't match request");
|
|
798
1656
|
}
|
|
799
1657
|
|
|
800
1658
|
/**
|
|
@@ -857,6 +1715,9 @@ export class ESPLoader extends EventTarget {
|
|
|
857
1715
|
}
|
|
858
1716
|
|
|
859
1717
|
async reconfigurePort(baud: number) {
|
|
1718
|
+
// Block new writes during the entire reconfiguration (all paths)
|
|
1719
|
+
this._isReconfiguring = true;
|
|
1720
|
+
|
|
860
1721
|
try {
|
|
861
1722
|
// Wait for pending writes to complete
|
|
862
1723
|
try {
|
|
@@ -865,9 +1726,35 @@ export class ESPLoader extends EventTarget {
|
|
|
865
1726
|
this.logger.debug(`Pending write error during reconfigure: ${err}`);
|
|
866
1727
|
}
|
|
867
1728
|
|
|
868
|
-
//
|
|
869
|
-
this.
|
|
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
|
+
}
|
|
870
1756
|
|
|
1757
|
+
// Web Serial or CH343: Close and reopen port
|
|
871
1758
|
// Release persistent writer before closing
|
|
872
1759
|
if (this._writer) {
|
|
873
1760
|
try {
|
|
@@ -888,148 +1775,59 @@ export class ESPLoader extends EventTarget {
|
|
|
888
1775
|
// Reopen Port
|
|
889
1776
|
await this.port.open({ baudRate: baud });
|
|
890
1777
|
|
|
891
|
-
// Port is now open - allow writes again
|
|
892
|
-
this._isReconfiguring = false;
|
|
893
|
-
|
|
894
1778
|
// Clear buffer again
|
|
895
1779
|
await this.flushSerialBuffers();
|
|
896
1780
|
|
|
897
1781
|
// Restart Readloop
|
|
898
1782
|
this.readLoop();
|
|
899
1783
|
} catch (e) {
|
|
900
|
-
this._isReconfiguring = false;
|
|
901
1784
|
this.logger.error(`Reconfigure port error: ${e}`);
|
|
902
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;
|
|
903
1789
|
}
|
|
904
1790
|
}
|
|
905
1791
|
|
|
906
1792
|
/**
|
|
907
|
-
* @name
|
|
908
|
-
*
|
|
909
|
-
*
|
|
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
|
|
910
1796
|
*/
|
|
911
|
-
async
|
|
912
|
-
const
|
|
913
|
-
const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
|
|
914
|
-
const isEspressifUSB = portInfo.usbVendorId === 0x303a;
|
|
915
|
-
|
|
916
|
-
this.logger.log(
|
|
917
|
-
`Detected USB: VID=0x${portInfo.usbVendorId?.toString(16) || "unknown"}, PID=0x${portInfo.usbProductId?.toString(16) || "unknown"}`,
|
|
918
|
-
);
|
|
919
|
-
|
|
920
|
-
// Define reset strategies to try in order
|
|
921
|
-
const resetStrategies: Array<{ name: string; fn: () => Promise<void> }> =
|
|
922
|
-
[];
|
|
923
|
-
|
|
924
|
-
// Strategy 1: USB-JTAG/Serial reset (for ESP32-C3, C6, S3, etc.)
|
|
925
|
-
// Try this first if we detect Espressif USB VID or the specific PID
|
|
926
|
-
if (isUSBJTAGSerial || isEspressifUSB) {
|
|
927
|
-
resetStrategies.push({
|
|
928
|
-
name: "USB-JTAG/Serial",
|
|
929
|
-
fn: async () => await this.hardResetUSBJTAGSerial(),
|
|
930
|
-
});
|
|
931
|
-
}
|
|
1797
|
+
async syncWithTimeout(timeoutMs: number): Promise<boolean> {
|
|
1798
|
+
const startTime = Date.now();
|
|
932
1799
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
+
}
|
|
938
1805
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
fn: async () => await this.hardResetUSBJTAGSerial(),
|
|
944
|
-
});
|
|
945
|
-
}
|
|
1806
|
+
// Check abandon flag
|
|
1807
|
+
if (this._abandonCurrentOperation) {
|
|
1808
|
+
return false;
|
|
1809
|
+
}
|
|
946
1810
|
|
|
947
|
-
|
|
1811
|
+
this._clearInputBuffer();
|
|
948
1812
|
|
|
949
|
-
// Try each reset strategy
|
|
950
|
-
for (const strategy of resetStrategies) {
|
|
951
1813
|
try {
|
|
952
|
-
this.
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
|
|
957
|
-
continue;
|
|
1814
|
+
const response = await this._sync();
|
|
1815
|
+
if (response) {
|
|
1816
|
+
await sleep(SYNC_TIMEOUT);
|
|
1817
|
+
return true;
|
|
958
1818
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
// If we get here, sync succeeded
|
|
966
|
-
this.logger.log(`Connected successfully with ${strategy.name} reset.`);
|
|
967
|
-
return;
|
|
968
|
-
} catch (error) {
|
|
969
|
-
lastError = error as Error;
|
|
970
|
-
this.logger.log(
|
|
971
|
-
`${strategy.name} reset failed: ${(error as Error).message}`,
|
|
972
|
-
);
|
|
973
|
-
|
|
974
|
-
// If port got disconnected, we can't try more strategies
|
|
975
|
-
if (!this.connected || !this.port.writable) {
|
|
976
|
-
this.logger.log(`Port disconnected during reset attempt`);
|
|
977
|
-
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;
|
|
978
1824
|
}
|
|
979
|
-
|
|
980
|
-
// Clear buffers before trying next strategy
|
|
981
|
-
this._inputBuffer.length = 0;
|
|
982
|
-
await this.drainInputBuffer(200);
|
|
983
|
-
await this.flushSerialBuffers();
|
|
984
1825
|
}
|
|
985
|
-
}
|
|
986
1826
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError?.message}`,
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
/**
|
|
994
|
-
* @name hardResetUSBJTAGSerial
|
|
995
|
-
* USB-JTAG/Serial reset sequence for ESP32-C3, ESP32-S3, ESP32-C6, etc.
|
|
996
|
-
*/
|
|
997
|
-
async hardResetUSBJTAGSerial() {
|
|
998
|
-
await this.setRTS(false);
|
|
999
|
-
await this.setDTR(false); // Idle
|
|
1000
|
-
await this.sleep(100);
|
|
1001
|
-
|
|
1002
|
-
await this.setDTR(true); // Set IO0
|
|
1003
|
-
await this.setRTS(false);
|
|
1004
|
-
await this.sleep(100);
|
|
1005
|
-
|
|
1006
|
-
await this.setRTS(true); // Reset. Calls inverted to go through (1,1) instead of (0,0)
|
|
1007
|
-
await this.setDTR(false);
|
|
1008
|
-
await this.setRTS(true); // RTS set as Windows only propagates DTR on RTS setting
|
|
1009
|
-
await this.sleep(100);
|
|
1010
|
-
|
|
1011
|
-
await this.setDTR(false);
|
|
1012
|
-
await this.setRTS(false); // Chip out of reset
|
|
1013
|
-
|
|
1014
|
-
// Wait for chip to boot into bootloader
|
|
1015
|
-
await this.sleep(200);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
/**
|
|
1019
|
-
* @name hardResetClassic
|
|
1020
|
-
* Classic reset sequence for USB-to-Serial bridge chips (CH340, CP2102, etc.)
|
|
1021
|
-
*/
|
|
1022
|
-
async hardResetClassic() {
|
|
1023
|
-
await this.setDTR(false); // IO0=HIGH
|
|
1024
|
-
await this.setRTS(true); // EN=LOW, chip in reset
|
|
1025
|
-
await this.sleep(100);
|
|
1026
|
-
await this.setDTR(true); // IO0=LOW
|
|
1027
|
-
await this.setRTS(false); // EN=HIGH, chip out of reset
|
|
1028
|
-
await this.sleep(50);
|
|
1029
|
-
await this.setDTR(false); // IO0=HIGH, done
|
|
1827
|
+
await sleep(SYNC_TIMEOUT);
|
|
1828
|
+
}
|
|
1030
1829
|
|
|
1031
|
-
|
|
1032
|
-
await this.sleep(200);
|
|
1830
|
+
return false;
|
|
1033
1831
|
}
|
|
1034
1832
|
|
|
1035
1833
|
/**
|
|
@@ -1039,7 +1837,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1039
1837
|
*/
|
|
1040
1838
|
async sync() {
|
|
1041
1839
|
for (let i = 0; i < 5; i++) {
|
|
1042
|
-
this.
|
|
1840
|
+
this._clearInputBuffer();
|
|
1043
1841
|
const response = await this._sync();
|
|
1044
1842
|
if (response) {
|
|
1045
1843
|
await sleep(SYNC_TIMEOUT);
|
|
@@ -1058,14 +1856,17 @@ export class ESPLoader extends EventTarget {
|
|
|
1058
1856
|
*/
|
|
1059
1857
|
async _sync() {
|
|
1060
1858
|
await this.sendCommand(ESP_SYNC, SYNC_PACKET);
|
|
1859
|
+
|
|
1061
1860
|
for (let i = 0; i < 8; i++) {
|
|
1062
1861
|
try {
|
|
1063
1862
|
const [, data] = await this.getResponse(ESP_SYNC, SYNC_TIMEOUT);
|
|
1064
1863
|
if (data.length > 1 && data[0] == 0 && data[1] == 0) {
|
|
1065
1864
|
return true;
|
|
1066
1865
|
}
|
|
1067
|
-
} catch {
|
|
1068
|
-
|
|
1866
|
+
} catch (e) {
|
|
1867
|
+
if (this.debug) {
|
|
1868
|
+
this.logger.debug(`Sync attempt ${i + 1} failed: ${e}`);
|
|
1869
|
+
}
|
|
1069
1870
|
}
|
|
1070
1871
|
}
|
|
1071
1872
|
return false;
|
|
@@ -1709,13 +2510,21 @@ export class ESPLoader extends EventTarget {
|
|
|
1709
2510
|
},
|
|
1710
2511
|
async () => {
|
|
1711
2512
|
// Previous write failed, but still attempt this write
|
|
2513
|
+
this.logger.debug(
|
|
2514
|
+
"Previous write failed, attempting recovery for current write",
|
|
2515
|
+
);
|
|
1712
2516
|
if (!this.port.writable) {
|
|
1713
2517
|
throw new Error("Port became unavailable during write");
|
|
1714
2518
|
}
|
|
1715
2519
|
|
|
1716
2520
|
// Writer was likely cleaned up by previous error, create new one
|
|
1717
2521
|
if (!this._writer) {
|
|
1718
|
-
|
|
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
|
+
}
|
|
1719
2528
|
}
|
|
1720
2529
|
|
|
1721
2530
|
await this._writer.write(new Uint8Array(data));
|
|
@@ -1727,7 +2536,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1727
2536
|
if (this._writer) {
|
|
1728
2537
|
try {
|
|
1729
2538
|
this._writer.releaseLock();
|
|
1730
|
-
} catch
|
|
2539
|
+
} catch {
|
|
1731
2540
|
// Ignore release errors
|
|
1732
2541
|
}
|
|
1733
2542
|
this._writer = undefined;
|
|
@@ -1785,6 +2594,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1785
2594
|
await new Promise((resolve) => {
|
|
1786
2595
|
if (!this._reader) {
|
|
1787
2596
|
resolve(undefined);
|
|
2597
|
+
return;
|
|
1788
2598
|
}
|
|
1789
2599
|
this.addEventListener("disconnect", resolve, { once: true });
|
|
1790
2600
|
this._reader!.cancel();
|
|
@@ -1810,6 +2620,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1810
2620
|
|
|
1811
2621
|
this.connected = false;
|
|
1812
2622
|
this.__inputBuffer = [];
|
|
2623
|
+
this.__inputBufferReadIndex = 0;
|
|
1813
2624
|
|
|
1814
2625
|
// Wait for pending writes to complete
|
|
1815
2626
|
try {
|
|
@@ -1880,6 +2691,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1880
2691
|
|
|
1881
2692
|
if (!this._parent) {
|
|
1882
2693
|
this.__inputBuffer = [];
|
|
2694
|
+
this.__inputBufferReadIndex = 0;
|
|
1883
2695
|
this.__totalBytesRead = 0;
|
|
1884
2696
|
this.readLoop();
|
|
1885
2697
|
}
|
|
@@ -1917,10 +2729,11 @@ export class ESPLoader extends EventTarget {
|
|
|
1917
2729
|
}
|
|
1918
2730
|
}
|
|
1919
2731
|
|
|
1920
|
-
//
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
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
|
+
|
|
1924
2737
|
this.logger.debug("Reconnection successful");
|
|
1925
2738
|
} catch (err) {
|
|
1926
2739
|
// Ensure flag is reset on error
|
|
@@ -1955,8 +2768,8 @@ export class ESPLoader extends EventTarget {
|
|
|
1955
2768
|
const drainTimeout = 100; // Short timeout for draining
|
|
1956
2769
|
|
|
1957
2770
|
while (drained < bytesToDrain && Date.now() - drainStart < drainTimeout) {
|
|
1958
|
-
if (this.
|
|
1959
|
-
const byte = this.
|
|
2771
|
+
if (this._inputBufferAvailable > 0) {
|
|
2772
|
+
const byte = this._readByte();
|
|
1960
2773
|
if (byte !== undefined) {
|
|
1961
2774
|
drained++;
|
|
1962
2775
|
}
|
|
@@ -1973,6 +2786,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1973
2786
|
// Final clear of application buffer
|
|
1974
2787
|
if (!this._parent) {
|
|
1975
2788
|
this.__inputBuffer = [];
|
|
2789
|
+
this.__inputBufferReadIndex = 0;
|
|
1976
2790
|
}
|
|
1977
2791
|
}
|
|
1978
2792
|
|
|
@@ -1985,6 +2799,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1985
2799
|
// Clear application buffer
|
|
1986
2800
|
if (!this._parent) {
|
|
1987
2801
|
this.__inputBuffer = [];
|
|
2802
|
+
this.__inputBufferReadIndex = 0;
|
|
1988
2803
|
}
|
|
1989
2804
|
|
|
1990
2805
|
// Wait for any pending data
|
|
@@ -1993,6 +2808,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1993
2808
|
// Final clear
|
|
1994
2809
|
if (!this._parent) {
|
|
1995
2810
|
this.__inputBuffer = [];
|
|
2811
|
+
this.__inputBufferReadIndex = 0;
|
|
1996
2812
|
}
|
|
1997
2813
|
|
|
1998
2814
|
this.logger.debug("Serial buffers flushed");
|
|
@@ -2028,7 +2844,38 @@ export class ESPLoader extends EventTarget {
|
|
|
2028
2844
|
`Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`,
|
|
2029
2845
|
);
|
|
2030
2846
|
|
|
2031
|
-
|
|
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
|
+
}
|
|
2032
2879
|
|
|
2033
2880
|
let allData = new Uint8Array(0);
|
|
2034
2881
|
let currentAddr = addr;
|
|
@@ -2044,6 +2891,7 @@ export class ESPLoader extends EventTarget {
|
|
|
2044
2891
|
// Retry loop for this chunk
|
|
2045
2892
|
while (!chunkSuccess && retryCount <= MAX_RETRIES) {
|
|
2046
2893
|
let resp = new Uint8Array(0);
|
|
2894
|
+
let lastAckedLength = 0; // Track last acknowledged length
|
|
2047
2895
|
|
|
2048
2896
|
try {
|
|
2049
2897
|
// Only log on first attempt or retries
|
|
@@ -2053,9 +2901,34 @@ export class ESPLoader extends EventTarget {
|
|
|
2053
2901
|
);
|
|
2054
2902
|
}
|
|
2055
2903
|
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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
|
+
|
|
2059
2932
|
const [res] = await this.checkCommand(ESP_READ_FLASH, pkt);
|
|
2060
2933
|
|
|
2061
2934
|
if (res != 0) {
|
|
@@ -2107,10 +2980,21 @@ export class ESPLoader extends EventTarget {
|
|
|
2107
2980
|
newResp.set(packetData, resp.length);
|
|
2108
2981
|
resp = newResp;
|
|
2109
2982
|
|
|
2110
|
-
// Send acknowledgment
|
|
2111
|
-
|
|
2112
|
-
const
|
|
2113
|
-
|
|
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
|
+
}
|
|
2114
2998
|
}
|
|
2115
2999
|
}
|
|
2116
3000
|
|
|
@@ -2121,9 +3005,93 @@ export class ESPLoader extends EventTarget {
|
|
|
2121
3005
|
allData = newAllData;
|
|
2122
3006
|
|
|
2123
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
|
+
}
|
|
2124
3060
|
} catch (err) {
|
|
2125
3061
|
retryCount++;
|
|
2126
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
|
+
|
|
2127
3095
|
// Check if it's a timeout error or SLIP error
|
|
2128
3096
|
if (err instanceof SlipReadError) {
|
|
2129
3097
|
if (retryCount <= MAX_RETRIES) {
|
|
@@ -2145,12 +3113,13 @@ export class ESPLoader extends EventTarget {
|
|
|
2145
3113
|
this.logger.debug(`Buffer drain error: ${drainErr}`);
|
|
2146
3114
|
}
|
|
2147
3115
|
} else {
|
|
2148
|
-
// All retries exhausted - attempt
|
|
3116
|
+
// All retries exhausted - attempt recovery by reloading stub
|
|
3117
|
+
// IMPORTANT: Do NOT close port to keep ESP32 in bootloader mode
|
|
2149
3118
|
if (!deepRecoveryAttempted) {
|
|
2150
3119
|
deepRecoveryAttempted = true;
|
|
2151
3120
|
|
|
2152
3121
|
this.logger.log(
|
|
2153
|
-
`All retries exhausted at 0x${currentAddr.toString(16)}. Attempting
|
|
3122
|
+
`All retries exhausted at 0x${currentAddr.toString(16)}. Attempting recovery (close and reopen port)...`,
|
|
2154
3123
|
);
|
|
2155
3124
|
|
|
2156
3125
|
try {
|
|
@@ -2164,15 +3133,15 @@ export class ESPLoader extends EventTarget {
|
|
|
2164
3133
|
// Reset retry counter to give it another chance after recovery
|
|
2165
3134
|
retryCount = 0;
|
|
2166
3135
|
continue;
|
|
2167
|
-
} catch (
|
|
3136
|
+
} catch (recoveryErr) {
|
|
2168
3137
|
throw new Error(
|
|
2169
|
-
`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and
|
|
3138
|
+
`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery failed: ${recoveryErr}`,
|
|
2170
3139
|
);
|
|
2171
3140
|
}
|
|
2172
3141
|
} else {
|
|
2173
|
-
//
|
|
3142
|
+
// Recovery already attempted, give up
|
|
2174
3143
|
throw new Error(
|
|
2175
|
-
`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and
|
|
3144
|
+
`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery attempt`,
|
|
2176
3145
|
);
|
|
2177
3146
|
}
|
|
2178
3147
|
}
|
|
@@ -2196,7 +3165,6 @@ export class ESPLoader extends EventTarget {
|
|
|
2196
3165
|
);
|
|
2197
3166
|
}
|
|
2198
3167
|
|
|
2199
|
-
this.logger.debug(`Successfully read ${allData.length} bytes from flash`);
|
|
2200
3168
|
return allData;
|
|
2201
3169
|
}
|
|
2202
3170
|
}
|