esp32tool 1.1.8 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nojekyll +0 -0
- package/README.md +100 -6
- package/apple-touch-icon.png +0 -0
- package/build-electron-cli.cjs +177 -0
- package/build-single-binary.cjs +295 -0
- package/css/light.css +11 -0
- package/css/style.css +225 -35
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +458 -0
- package/dist/esp_loader.d.ts +129 -21
- package/dist/esp_loader.js +1227 -222
- package/dist/index.d.ts +2 -1
- package/dist/index.js +37 -4
- package/dist/node-usb-adapter.d.ts +47 -0
- package/dist/node-usb-adapter.js +725 -0
- package/dist/stubs/index.d.ts +1 -2
- package/dist/stubs/index.js +4 -0
- package/dist/web/index.js +1 -1
- package/electron/cli-main.cjs +74 -0
- package/electron/main.cjs +338 -0
- package/electron/main.js +7 -2
- package/favicon.ico +0 -0
- package/fix-cli-imports.cjs +127 -0
- package/generate-icons.sh +89 -0
- package/icons/icon-128.png +0 -0
- package/icons/icon-144.png +0 -0
- package/icons/icon-152.png +0 -0
- package/icons/icon-192.png +0 -0
- package/icons/icon-384.png +0 -0
- package/icons/icon-512.png +0 -0
- package/icons/icon-72.png +0 -0
- package/icons/icon-96.png +0 -0
- package/index.html +94 -64
- package/install-android.html +411 -0
- package/js/modules/esptool.js +1 -1
- package/js/script.js +165 -160
- package/js/webusb-serial.js +1017 -0
- package/license.md +1 -1
- package/manifest.json +89 -0
- package/package.cli.json +29 -0
- package/package.json +31 -21
- package/screenshots/desktop.png +0 -0
- package/screenshots/mobile.png +0 -0
- package/src/cli.ts +618 -0
- package/src/esp_loader.ts +1438 -254
- package/src/index.ts +69 -3
- package/src/node-usb-adapter.ts +924 -0
- package/src/stubs/index.ts +4 -1
- package/sw.js +155 -0
package/src/esp_loader.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
USB_RAM_BLOCK,
|
|
43
43
|
ChipFamily,
|
|
44
44
|
ESP_ERASE_FLASH,
|
|
45
|
+
ESP_ERASE_REGION,
|
|
45
46
|
ESP_READ_FLASH,
|
|
46
47
|
CHIP_ERASE_TIMEOUT,
|
|
47
48
|
FLASH_READ_TIMEOUT,
|
|
@@ -62,15 +63,25 @@ import {
|
|
|
62
63
|
} from "./const";
|
|
63
64
|
import { getStubCode } from "./stubs";
|
|
64
65
|
import { hexFormatter, sleep, slipEncode, toHex } from "./util";
|
|
65
|
-
|
|
66
|
-
import { deflate } from "pako/dist/pako.esm.mjs";
|
|
66
|
+
import { deflate } from "pako";
|
|
67
67
|
import { pack, unpack } from "./struct";
|
|
68
68
|
|
|
69
|
+
// Interface for WebUSB Serial Port (extends SerialPort with WebUSB-specific methods)
|
|
70
|
+
interface WebUSBSerialPort extends SerialPort {
|
|
71
|
+
isWebUSB?: boolean;
|
|
72
|
+
maxTransferSize?: number;
|
|
73
|
+
setSignals(signals: {
|
|
74
|
+
dataTerminalReady?: boolean;
|
|
75
|
+
requestToSend?: boolean;
|
|
76
|
+
}): Promise<void>;
|
|
77
|
+
setBaudRate(baudRate: number): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
export class ESPLoader extends EventTarget {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
__chipFamily?: ChipFamily;
|
|
82
|
+
__chipName: string | null = null;
|
|
83
|
+
__chipRevision: number | null = null;
|
|
84
|
+
__chipVariant: string | null = null;
|
|
74
85
|
_efuses = new Array(4).fill(0);
|
|
75
86
|
_flashsize = 4 * 1024 * 1024;
|
|
76
87
|
debug = false;
|
|
@@ -79,6 +90,7 @@ export class ESPLoader extends EventTarget {
|
|
|
79
90
|
flashSize: string | null = null;
|
|
80
91
|
|
|
81
92
|
__inputBuffer?: number[];
|
|
93
|
+
__inputBufferReadIndex?: number;
|
|
82
94
|
__totalBytesRead?: number;
|
|
83
95
|
private _currentBaudRate: number = ESP_ROM_BAUD;
|
|
84
96
|
private _maxUSBSerialBaudrate?: number;
|
|
@@ -86,7 +98,15 @@ export class ESPLoader extends EventTarget {
|
|
|
86
98
|
private _isESP32S2NativeUSB: boolean = false;
|
|
87
99
|
private _initializationSucceeded: boolean = false;
|
|
88
100
|
private __commandLock: Promise<[number, number[]]> = Promise.resolve([0, []]);
|
|
89
|
-
private
|
|
101
|
+
private __isReconfiguring: boolean = false;
|
|
102
|
+
private __abandonCurrentOperation: boolean = false;
|
|
103
|
+
|
|
104
|
+
// Adaptive speed adjustment for flash read operations
|
|
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,
|
|
@@ -96,8 +116,109 @@ export class ESPLoader extends EventTarget {
|
|
|
96
116
|
super();
|
|
97
117
|
}
|
|
98
118
|
|
|
119
|
+
// Chip properties with parent delegation
|
|
120
|
+
// chipFamily accessed before initialization as designed
|
|
121
|
+
get chipFamily(): ChipFamily {
|
|
122
|
+
return this._parent ? this._parent.chipFamily : this.__chipFamily!;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
set chipFamily(value: ChipFamily) {
|
|
126
|
+
if (this._parent) {
|
|
127
|
+
this._parent.chipFamily = value;
|
|
128
|
+
} else {
|
|
129
|
+
this.__chipFamily = value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
get chipName(): string | null {
|
|
134
|
+
return this._parent ? this._parent.chipName : this.__chipName;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
set chipName(value: string | null) {
|
|
138
|
+
if (this._parent) {
|
|
139
|
+
this._parent.chipName = value;
|
|
140
|
+
} else {
|
|
141
|
+
this.__chipName = value;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get chipRevision(): number | null {
|
|
146
|
+
return this._parent ? this._parent.chipRevision : this.__chipRevision;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
set chipRevision(value: number | null) {
|
|
150
|
+
if (this._parent) {
|
|
151
|
+
this._parent.chipRevision = value;
|
|
152
|
+
} else {
|
|
153
|
+
this.__chipRevision = value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
get chipVariant(): string | null {
|
|
158
|
+
return this._parent ? this._parent.chipVariant : this.__chipVariant;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
set chipVariant(value: string | null) {
|
|
162
|
+
if (this._parent) {
|
|
163
|
+
this._parent.chipVariant = value;
|
|
164
|
+
} else {
|
|
165
|
+
this.__chipVariant = value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
99
169
|
private get _inputBuffer(): number[] {
|
|
100
|
-
|
|
170
|
+
if (this._parent) {
|
|
171
|
+
return this._parent._inputBuffer;
|
|
172
|
+
}
|
|
173
|
+
if (this.__inputBuffer === undefined) {
|
|
174
|
+
throw new Error("_inputBuffer accessed before initialization");
|
|
175
|
+
}
|
|
176
|
+
return this.__inputBuffer;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private get _inputBufferReadIndex(): number {
|
|
180
|
+
return this._parent
|
|
181
|
+
? this._parent._inputBufferReadIndex
|
|
182
|
+
: this.__inputBufferReadIndex || 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private set _inputBufferReadIndex(value: number) {
|
|
186
|
+
if (this._parent) {
|
|
187
|
+
this._parent._inputBufferReadIndex = value;
|
|
188
|
+
} else {
|
|
189
|
+
this.__inputBufferReadIndex = value;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Get available bytes in buffer (from read index to end)
|
|
194
|
+
private get _inputBufferAvailable(): number {
|
|
195
|
+
return this._inputBuffer.length - this._inputBufferReadIndex;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Read one byte from buffer (ring-buffer style with index pointer)
|
|
199
|
+
private _readByte(): number | undefined {
|
|
200
|
+
if (this._inputBufferReadIndex >= this._inputBuffer.length) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
return this._inputBuffer[this._inputBufferReadIndex++];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Clear input buffer and reset read index
|
|
207
|
+
private _clearInputBuffer(): void {
|
|
208
|
+
this._inputBuffer.length = 0;
|
|
209
|
+
this._inputBufferReadIndex = 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Compact buffer when read index gets too large (prevent memory growth)
|
|
213
|
+
private _compactInputBuffer(): void {
|
|
214
|
+
if (
|
|
215
|
+
this._inputBufferReadIndex > 1000 &&
|
|
216
|
+
this._inputBufferReadIndex > this._inputBuffer.length / 2
|
|
217
|
+
) {
|
|
218
|
+
// Remove already-read bytes and reset index
|
|
219
|
+
this._inputBuffer.splice(0, this._inputBufferReadIndex);
|
|
220
|
+
this._inputBufferReadIndex = 0;
|
|
221
|
+
}
|
|
101
222
|
}
|
|
102
223
|
|
|
103
224
|
private get _totalBytesRead(): number {
|
|
@@ -126,6 +247,102 @@ export class ESPLoader extends EventTarget {
|
|
|
126
247
|
}
|
|
127
248
|
}
|
|
128
249
|
|
|
250
|
+
private get _isReconfiguring(): boolean {
|
|
251
|
+
return this._parent
|
|
252
|
+
? this._parent._isReconfiguring
|
|
253
|
+
: this.__isReconfiguring;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private set _isReconfiguring(value: boolean) {
|
|
257
|
+
if (this._parent) {
|
|
258
|
+
this._parent._isReconfiguring = value;
|
|
259
|
+
} else {
|
|
260
|
+
this.__isReconfiguring = value;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private get _abandonCurrentOperation(): boolean {
|
|
265
|
+
return this._parent
|
|
266
|
+
? this._parent._abandonCurrentOperation
|
|
267
|
+
: this.__abandonCurrentOperation;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private set _abandonCurrentOperation(value: boolean) {
|
|
271
|
+
if (this._parent) {
|
|
272
|
+
this._parent._abandonCurrentOperation = value;
|
|
273
|
+
} else {
|
|
274
|
+
this.__abandonCurrentOperation = value;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private get _adaptiveBlockMultiplier(): number {
|
|
279
|
+
return this._parent
|
|
280
|
+
? this._parent._adaptiveBlockMultiplier
|
|
281
|
+
: this.__adaptiveBlockMultiplier;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private set _adaptiveBlockMultiplier(value: number) {
|
|
285
|
+
if (this._parent) {
|
|
286
|
+
this._parent._adaptiveBlockMultiplier = value;
|
|
287
|
+
} else {
|
|
288
|
+
this.__adaptiveBlockMultiplier = value;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private get _adaptiveMaxInFlightMultiplier(): number {
|
|
293
|
+
return this._parent
|
|
294
|
+
? this._parent._adaptiveMaxInFlightMultiplier
|
|
295
|
+
: this.__adaptiveMaxInFlightMultiplier;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private set _adaptiveMaxInFlightMultiplier(value: number) {
|
|
299
|
+
if (this._parent) {
|
|
300
|
+
this._parent._adaptiveMaxInFlightMultiplier = value;
|
|
301
|
+
} else {
|
|
302
|
+
this.__adaptiveMaxInFlightMultiplier = value;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private get _consecutiveSuccessfulChunks(): number {
|
|
307
|
+
return this._parent
|
|
308
|
+
? this._parent._consecutiveSuccessfulChunks
|
|
309
|
+
: this.__consecutiveSuccessfulChunks;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private set _consecutiveSuccessfulChunks(value: number) {
|
|
313
|
+
if (this._parent) {
|
|
314
|
+
this._parent._consecutiveSuccessfulChunks = value;
|
|
315
|
+
} else {
|
|
316
|
+
this.__consecutiveSuccessfulChunks = value;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private get _lastAdaptiveAdjustment(): number {
|
|
321
|
+
return this._parent
|
|
322
|
+
? this._parent._lastAdaptiveAdjustment
|
|
323
|
+
: this.__lastAdaptiveAdjustment;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private set _lastAdaptiveAdjustment(value: number) {
|
|
327
|
+
if (this._parent) {
|
|
328
|
+
this._parent._lastAdaptiveAdjustment = value;
|
|
329
|
+
} else {
|
|
330
|
+
this.__lastAdaptiveAdjustment = value;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private get _isCDCDevice(): boolean {
|
|
335
|
+
return this._parent ? this._parent._isCDCDevice : this.__isCDCDevice;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private set _isCDCDevice(value: boolean) {
|
|
339
|
+
if (this._parent) {
|
|
340
|
+
this._parent._isCDCDevice = value;
|
|
341
|
+
} else {
|
|
342
|
+
this.__isCDCDevice = value;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
129
346
|
private detectUSBSerialChip(
|
|
130
347
|
vendorId: number,
|
|
131
348
|
productId: number,
|
|
@@ -182,6 +399,7 @@ export class ESPLoader extends EventTarget {
|
|
|
182
399
|
async initialize() {
|
|
183
400
|
if (!this._parent) {
|
|
184
401
|
this.__inputBuffer = [];
|
|
402
|
+
this.__inputBufferReadIndex = 0;
|
|
185
403
|
this.__totalBytesRead = 0;
|
|
186
404
|
|
|
187
405
|
// Detect and log USB-Serial chip info
|
|
@@ -202,6 +420,15 @@ export class ESPLoader extends EventTarget {
|
|
|
202
420
|
if (portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x2) {
|
|
203
421
|
this._isESP32S2NativeUSB = true;
|
|
204
422
|
}
|
|
423
|
+
|
|
424
|
+
// Detect CDC devices for adaptive speed adjustment
|
|
425
|
+
// Espressif Native USB (VID: 0x303a) or CH343 (VID: 0x1a86, PID: 0x55d3)
|
|
426
|
+
if (
|
|
427
|
+
portInfo.usbVendorId === 0x303a ||
|
|
428
|
+
(portInfo.usbVendorId === 0x1a86 && portInfo.usbProductId === 0x55d3)
|
|
429
|
+
) {
|
|
430
|
+
this._isCDCDevice = true;
|
|
431
|
+
}
|
|
205
432
|
}
|
|
206
433
|
|
|
207
434
|
// Don't await this promise so it doesn't block rest of method.
|
|
@@ -277,7 +504,7 @@ export class ESPLoader extends EventTarget {
|
|
|
277
504
|
await this.drainInputBuffer(200);
|
|
278
505
|
|
|
279
506
|
// Clear input buffer and re-sync to recover from failed command
|
|
280
|
-
this.
|
|
507
|
+
this._clearInputBuffer();
|
|
281
508
|
await sleep(SYNC_TIMEOUT);
|
|
282
509
|
|
|
283
510
|
// Re-sync with the chip to ensure clean communication
|
|
@@ -393,6 +620,21 @@ export class ESPLoader extends EventTarget {
|
|
|
393
620
|
};
|
|
394
621
|
}
|
|
395
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Get MAC address from efuses
|
|
625
|
+
*/
|
|
626
|
+
async getMacAddress(): Promise<string> {
|
|
627
|
+
if (!this._initializationSucceeded) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
"getMacAddress() requires initialize() to have completed successfully",
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
const macBytes = this.macAddr(); // chip-family-aware
|
|
633
|
+
return macBytes
|
|
634
|
+
.map((b) => b.toString(16).padStart(2, "0").toUpperCase())
|
|
635
|
+
.join(":");
|
|
636
|
+
}
|
|
637
|
+
|
|
396
638
|
/**
|
|
397
639
|
* @name readLoop
|
|
398
640
|
* Reads data from the input stream and places it in the inputBuffer
|
|
@@ -427,7 +669,13 @@ export class ESPLoader extends EventTarget {
|
|
|
427
669
|
}
|
|
428
670
|
} catch {
|
|
429
671
|
this.logger.error("Read loop got disconnected");
|
|
672
|
+
} finally {
|
|
673
|
+
// Always reset reconfiguring flag when read loop ends
|
|
674
|
+
// This prevents "Cannot write during port reconfiguration" errors
|
|
675
|
+
// when the read loop dies unexpectedly
|
|
676
|
+
this._isReconfiguring = false;
|
|
430
677
|
}
|
|
678
|
+
|
|
431
679
|
// Disconnected!
|
|
432
680
|
this.connected = false;
|
|
433
681
|
|
|
@@ -453,6 +701,12 @@ export class ESPLoader extends EventTarget {
|
|
|
453
701
|
}
|
|
454
702
|
|
|
455
703
|
state_DTR = false;
|
|
704
|
+
state_RTS = false;
|
|
705
|
+
|
|
706
|
+
// ============================================================================
|
|
707
|
+
// Web Serial (Desktop) - DTR/RTS Signal Handling & Reset Strategies
|
|
708
|
+
// ============================================================================
|
|
709
|
+
|
|
456
710
|
async setRTS(state: boolean) {
|
|
457
711
|
await this.port.setSignals({ requestToSend: state });
|
|
458
712
|
// Work-around for adapters on Windows using the usbser.sys driver:
|
|
@@ -467,6 +721,579 @@ export class ESPLoader extends EventTarget {
|
|
|
467
721
|
await this.port.setSignals({ dataTerminalReady: state });
|
|
468
722
|
}
|
|
469
723
|
|
|
724
|
+
/**
|
|
725
|
+
* @name hardResetUSBJTAGSerial
|
|
726
|
+
* USB-JTAG/Serial reset for Web Serial (Desktop)
|
|
727
|
+
*/
|
|
728
|
+
async hardResetUSBJTAGSerial() {
|
|
729
|
+
await this.setRTS(false);
|
|
730
|
+
await this.setDTR(false); // Idle
|
|
731
|
+
await this.sleep(100);
|
|
732
|
+
|
|
733
|
+
await this.setDTR(true); // Set IO0
|
|
734
|
+
await this.setRTS(false);
|
|
735
|
+
await this.sleep(100);
|
|
736
|
+
|
|
737
|
+
await this.setRTS(true); // Reset
|
|
738
|
+
await this.setDTR(false);
|
|
739
|
+
await this.setRTS(true);
|
|
740
|
+
await this.sleep(100);
|
|
741
|
+
|
|
742
|
+
await this.setDTR(false);
|
|
743
|
+
await this.setRTS(false); // Chip out of reset
|
|
744
|
+
|
|
745
|
+
await this.sleep(200);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* @name hardResetClassic
|
|
750
|
+
* Classic reset for Web Serial (Desktop)
|
|
751
|
+
*/
|
|
752
|
+
async hardResetClassic() {
|
|
753
|
+
await this.setDTR(false); // IO0=HIGH
|
|
754
|
+
await this.setRTS(true); // EN=LOW, chip in reset
|
|
755
|
+
await this.sleep(100);
|
|
756
|
+
await this.setDTR(true); // IO0=LOW
|
|
757
|
+
await this.setRTS(false); // EN=HIGH, chip out of reset
|
|
758
|
+
await this.sleep(50);
|
|
759
|
+
await this.setDTR(false); // IO0=HIGH, done
|
|
760
|
+
|
|
761
|
+
await this.sleep(200);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ============================================================================
|
|
765
|
+
// WebUSB (Android) - DTR/RTS Signal Handling & Reset Strategies
|
|
766
|
+
// ============================================================================
|
|
767
|
+
|
|
768
|
+
async setRTSWebUSB(state: boolean) {
|
|
769
|
+
this.state_RTS = state;
|
|
770
|
+
// Always specify both signals to avoid flipping the other line
|
|
771
|
+
// The WebUSB setSignals() now preserves unspecified signals, but being explicit is safer
|
|
772
|
+
await (this.port as WebUSBSerialPort).setSignals({
|
|
773
|
+
requestToSend: state,
|
|
774
|
+
dataTerminalReady: this.state_DTR,
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async setDTRWebUSB(state: boolean) {
|
|
779
|
+
this.state_DTR = state;
|
|
780
|
+
// Always specify both signals to avoid flipping the other line
|
|
781
|
+
await (this.port as WebUSBSerialPort).setSignals({
|
|
782
|
+
dataTerminalReady: state,
|
|
783
|
+
requestToSend: this.state_RTS, // Explicitly preserve current RTS state
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
async setDTRandRTSWebUSB(dtr: boolean, rts: boolean) {
|
|
788
|
+
this.state_DTR = dtr;
|
|
789
|
+
this.state_RTS = rts;
|
|
790
|
+
await (this.port as WebUSBSerialPort).setSignals({
|
|
791
|
+
dataTerminalReady: dtr,
|
|
792
|
+
requestToSend: rts,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* @name hardResetUSBJTAGSerialWebUSB
|
|
798
|
+
* USB-JTAG/Serial reset for WebUSB (Android)
|
|
799
|
+
*/
|
|
800
|
+
async hardResetUSBJTAGSerialWebUSB() {
|
|
801
|
+
await this.setRTSWebUSB(false);
|
|
802
|
+
await this.setDTRWebUSB(false); // Idle
|
|
803
|
+
await this.sleep(100);
|
|
804
|
+
|
|
805
|
+
await this.setDTRWebUSB(true); // Set IO0
|
|
806
|
+
await this.setRTSWebUSB(false);
|
|
807
|
+
await this.sleep(100);
|
|
808
|
+
|
|
809
|
+
await this.setRTSWebUSB(true); // Reset
|
|
810
|
+
await this.setDTRWebUSB(false);
|
|
811
|
+
await this.setRTSWebUSB(true);
|
|
812
|
+
await this.sleep(100);
|
|
813
|
+
|
|
814
|
+
await this.setDTRWebUSB(false);
|
|
815
|
+
await this.setRTSWebUSB(false); // Chip out of reset
|
|
816
|
+
|
|
817
|
+
await this.sleep(200);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* @name hardResetUSBJTAGSerialInvertedDTRWebUSB
|
|
822
|
+
* USB-JTAG/Serial reset with inverted DTR for WebUSB (Android)
|
|
823
|
+
*/
|
|
824
|
+
async hardResetUSBJTAGSerialInvertedDTRWebUSB() {
|
|
825
|
+
await this.setRTSWebUSB(false);
|
|
826
|
+
await this.setDTRWebUSB(true); // Idle (DTR inverted)
|
|
827
|
+
await this.sleep(100);
|
|
828
|
+
|
|
829
|
+
await this.setDTRWebUSB(false); // Set IO0 (DTR inverted)
|
|
830
|
+
await this.setRTSWebUSB(false);
|
|
831
|
+
await this.sleep(100);
|
|
832
|
+
|
|
833
|
+
await this.setRTSWebUSB(true); // Reset
|
|
834
|
+
await this.setDTRWebUSB(true); // (DTR inverted)
|
|
835
|
+
await this.setRTSWebUSB(true);
|
|
836
|
+
await this.sleep(100);
|
|
837
|
+
|
|
838
|
+
await this.setDTRWebUSB(true); // (DTR inverted)
|
|
839
|
+
await this.setRTSWebUSB(false); // Chip out of reset
|
|
840
|
+
|
|
841
|
+
await this.sleep(200);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* @name hardResetClassicWebUSB
|
|
846
|
+
* Classic reset for WebUSB (Android)
|
|
847
|
+
*/
|
|
848
|
+
async hardResetClassicWebUSB() {
|
|
849
|
+
await this.setDTRWebUSB(false); // IO0=HIGH
|
|
850
|
+
await this.setRTSWebUSB(true); // EN=LOW, chip in reset
|
|
851
|
+
await this.sleep(100);
|
|
852
|
+
await this.setDTRWebUSB(true); // IO0=LOW
|
|
853
|
+
await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
|
|
854
|
+
await this.sleep(50);
|
|
855
|
+
await this.setDTRWebUSB(false); // IO0=HIGH, done
|
|
856
|
+
await this.sleep(200);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* @name hardResetUnixTightWebUSB
|
|
861
|
+
* Unix Tight reset for WebUSB (Android) - sets DTR and RTS simultaneously
|
|
862
|
+
*/
|
|
863
|
+
async hardResetUnixTightWebUSB() {
|
|
864
|
+
await this.setDTRandRTSWebUSB(false, false);
|
|
865
|
+
await this.setDTRandRTSWebUSB(true, true);
|
|
866
|
+
await this.setDTRandRTSWebUSB(false, true); // IO0=HIGH & EN=LOW, chip in reset
|
|
867
|
+
await this.sleep(100);
|
|
868
|
+
await this.setDTRandRTSWebUSB(true, false); // IO0=LOW & EN=HIGH, chip out of reset
|
|
869
|
+
await this.sleep(50);
|
|
870
|
+
await this.setDTRandRTSWebUSB(false, false); // IO0=HIGH, done
|
|
871
|
+
await this.setDTRWebUSB(false); // Ensure IO0=HIGH
|
|
872
|
+
await this.sleep(200);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* @name hardResetClassicLongDelayWebUSB
|
|
877
|
+
* Classic reset with longer delays for WebUSB (Android)
|
|
878
|
+
* Specifically for CP2102/CH340 which may need more time
|
|
879
|
+
*/
|
|
880
|
+
async hardResetClassicLongDelayWebUSB() {
|
|
881
|
+
await this.setDTRWebUSB(false); // IO0=HIGH
|
|
882
|
+
await this.setRTSWebUSB(true); // EN=LOW, chip in reset
|
|
883
|
+
await this.sleep(500); // Extra long delay
|
|
884
|
+
await this.setDTRWebUSB(true); // IO0=LOW
|
|
885
|
+
await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
|
|
886
|
+
await this.sleep(200);
|
|
887
|
+
await this.setDTRWebUSB(false); // IO0=HIGH, done
|
|
888
|
+
await this.sleep(500); // Extra long delay
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* @name hardResetClassicShortDelayWebUSB
|
|
893
|
+
* Classic reset with shorter delays for WebUSB (Android)
|
|
894
|
+
*/
|
|
895
|
+
async hardResetClassicShortDelayWebUSB() {
|
|
896
|
+
await this.setDTRWebUSB(false); // IO0=HIGH
|
|
897
|
+
await this.setRTSWebUSB(true); // EN=LOW, chip in reset
|
|
898
|
+
await this.sleep(50);
|
|
899
|
+
await this.setDTRWebUSB(true); // IO0=LOW
|
|
900
|
+
await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
|
|
901
|
+
await this.sleep(25);
|
|
902
|
+
await this.setDTRWebUSB(false); // IO0=HIGH, done
|
|
903
|
+
await this.sleep(100);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* @name hardResetInvertedWebUSB
|
|
908
|
+
* Inverted reset sequence for WebUSB (Android) - both signals inverted
|
|
909
|
+
*/
|
|
910
|
+
async hardResetInvertedWebUSB() {
|
|
911
|
+
await this.setDTRWebUSB(true); // IO0=HIGH (inverted)
|
|
912
|
+
await this.setRTSWebUSB(false); // EN=LOW, chip in reset (inverted)
|
|
913
|
+
await this.sleep(100);
|
|
914
|
+
await this.setDTRWebUSB(false); // IO0=LOW (inverted)
|
|
915
|
+
await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (inverted)
|
|
916
|
+
await this.sleep(50);
|
|
917
|
+
await this.setDTRWebUSB(true); // IO0=HIGH, done (inverted)
|
|
918
|
+
await this.sleep(200);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* @name hardResetInvertedDTRWebUSB
|
|
923
|
+
* Only DTR inverted for WebUSB (Android)
|
|
924
|
+
*/
|
|
925
|
+
async hardResetInvertedDTRWebUSB() {
|
|
926
|
+
await this.setDTRWebUSB(true); // IO0=HIGH (DTR inverted)
|
|
927
|
+
await this.setRTSWebUSB(true); // EN=LOW, chip in reset (RTS normal)
|
|
928
|
+
await this.sleep(100);
|
|
929
|
+
await this.setDTRWebUSB(false); // IO0=LOW (DTR inverted)
|
|
930
|
+
await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset (RTS normal)
|
|
931
|
+
await this.sleep(50);
|
|
932
|
+
await this.setDTRWebUSB(true); // IO0=HIGH, done (DTR inverted)
|
|
933
|
+
await this.sleep(200);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* @name hardResetInvertedRTSWebUSB
|
|
938
|
+
* Only RTS inverted for WebUSB (Android)
|
|
939
|
+
*/
|
|
940
|
+
async hardResetInvertedRTSWebUSB() {
|
|
941
|
+
await this.setDTRWebUSB(false); // IO0=HIGH (DTR normal)
|
|
942
|
+
await this.setRTSWebUSB(false); // EN=LOW, chip in reset (RTS inverted)
|
|
943
|
+
await this.sleep(100);
|
|
944
|
+
await this.setDTRWebUSB(true); // IO0=LOW (DTR normal)
|
|
945
|
+
await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (RTS inverted)
|
|
946
|
+
await this.sleep(50);
|
|
947
|
+
await this.setDTRWebUSB(false); // IO0=HIGH, done (DTR normal)
|
|
948
|
+
await this.sleep(200);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Check if we're using WebUSB (Android) or Web Serial (Desktop)
|
|
953
|
+
*/
|
|
954
|
+
private isWebUSB(): boolean {
|
|
955
|
+
// WebUSBSerial class has isWebUSB flag - this is the most reliable check
|
|
956
|
+
return (this.port as WebUSBSerialPort).isWebUSB === true;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* @name connectWithResetStrategies
|
|
961
|
+
* Try different reset strategies to enter bootloader mode
|
|
962
|
+
* Similar to esptool.py's connect() method with multiple reset strategies
|
|
963
|
+
*/
|
|
964
|
+
async connectWithResetStrategies() {
|
|
965
|
+
const portInfo = this.port.getInfo();
|
|
966
|
+
const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
|
|
967
|
+
const isEspressifUSB = portInfo.usbVendorId === 0x303a;
|
|
968
|
+
|
|
969
|
+
// this.logger.log(
|
|
970
|
+
// `Detected USB: VID=0x${portInfo.usbVendorId?.toString(16) || "unknown"}, PID=0x${portInfo.usbProductId?.toString(16) || "unknown"}`,
|
|
971
|
+
// );
|
|
972
|
+
|
|
973
|
+
// Define reset strategies to try in order
|
|
974
|
+
const resetStrategies: Array<{ name: string; fn: () => Promise<void> }> =
|
|
975
|
+
[];
|
|
976
|
+
|
|
977
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
978
|
+
const self = this;
|
|
979
|
+
|
|
980
|
+
// WebUSB (Android) uses different reset methods than Web Serial (Desktop)
|
|
981
|
+
if (this.isWebUSB()) {
|
|
982
|
+
// For USB-Serial chips (CP2102, CH340, etc.), try inverted strategies first
|
|
983
|
+
const isUSBSerialChip = !isUSBJTAGSerial && !isEspressifUSB;
|
|
984
|
+
|
|
985
|
+
// Detect specific chip types once
|
|
986
|
+
const isCP2102 = portInfo.usbVendorId === 0x10c4;
|
|
987
|
+
const isCH34x = portInfo.usbVendorId === 0x1a86;
|
|
988
|
+
|
|
989
|
+
// Check for ESP32-S2 Native USB (VID: 0x303a, PID: 0x0002)
|
|
990
|
+
const isESP32S2NativeUSB =
|
|
991
|
+
portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x0002;
|
|
992
|
+
|
|
993
|
+
// WebUSB Strategy 1: USB-JTAG/Serial reset (for Native USB only)
|
|
994
|
+
if (isUSBJTAGSerial || isEspressifUSB) {
|
|
995
|
+
if (isESP32S2NativeUSB) {
|
|
996
|
+
// ESP32-S2 Native USB: Try multiple strategies
|
|
997
|
+
// The device might be in JTAG mode OR CDC mode
|
|
998
|
+
|
|
999
|
+
// Strategy 1: USB-JTAG/Serial (works in CDC mode on Desktop)
|
|
1000
|
+
resetStrategies.push({
|
|
1001
|
+
name: "USB-JTAG/Serial (WebUSB) - ESP32-S2",
|
|
1002
|
+
fn: async () => {
|
|
1003
|
+
return await self.hardResetUSBJTAGSerialWebUSB();
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
// Strategy 2: USB-JTAG/Serial Inverted DTR (works in JTAG mode)
|
|
1008
|
+
resetStrategies.push({
|
|
1009
|
+
name: "USB-JTAG/Serial Inverted DTR (WebUSB) - ESP32-S2",
|
|
1010
|
+
fn: async () => {
|
|
1011
|
+
return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
// Strategy 3: UnixTight (CDC fallback)
|
|
1016
|
+
resetStrategies.push({
|
|
1017
|
+
name: "UnixTight (WebUSB) - ESP32-S2 CDC",
|
|
1018
|
+
fn: async () => {
|
|
1019
|
+
return await self.hardResetUnixTightWebUSB();
|
|
1020
|
+
},
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
// Strategy 4: Classic reset (CDC fallback)
|
|
1024
|
+
resetStrategies.push({
|
|
1025
|
+
name: "Classic (WebUSB) - ESP32-S2 CDC",
|
|
1026
|
+
fn: async () => {
|
|
1027
|
+
return await self.hardResetClassicWebUSB();
|
|
1028
|
+
},
|
|
1029
|
+
});
|
|
1030
|
+
} else {
|
|
1031
|
+
// Other USB-JTAG chips: Try Inverted DTR first - works best for ESP32-H2 and other JTAG chips
|
|
1032
|
+
resetStrategies.push({
|
|
1033
|
+
name: "USB-JTAG/Serial Inverted DTR (WebUSB)",
|
|
1034
|
+
fn: async () => {
|
|
1035
|
+
return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
|
|
1036
|
+
},
|
|
1037
|
+
});
|
|
1038
|
+
resetStrategies.push({
|
|
1039
|
+
name: "USB-JTAG/Serial (WebUSB)",
|
|
1040
|
+
fn: async () => {
|
|
1041
|
+
return await self.hardResetUSBJTAGSerialWebUSB();
|
|
1042
|
+
},
|
|
1043
|
+
});
|
|
1044
|
+
resetStrategies.push({
|
|
1045
|
+
name: "Inverted DTR Classic (WebUSB)",
|
|
1046
|
+
fn: async () => {
|
|
1047
|
+
return await self.hardResetInvertedDTRWebUSB();
|
|
1048
|
+
},
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// For USB-Serial chips, try inverted strategies first
|
|
1054
|
+
if (isUSBSerialChip) {
|
|
1055
|
+
if (isCH34x) {
|
|
1056
|
+
// CH340/CH343: UnixTight works best (like CP2102)
|
|
1057
|
+
resetStrategies.push({
|
|
1058
|
+
name: "UnixTight (WebUSB) - CH34x",
|
|
1059
|
+
fn: async () => {
|
|
1060
|
+
return await self.hardResetUnixTightWebUSB();
|
|
1061
|
+
},
|
|
1062
|
+
});
|
|
1063
|
+
resetStrategies.push({
|
|
1064
|
+
name: "Classic (WebUSB) - CH34x",
|
|
1065
|
+
fn: async () => {
|
|
1066
|
+
return await self.hardResetClassicWebUSB();
|
|
1067
|
+
},
|
|
1068
|
+
});
|
|
1069
|
+
resetStrategies.push({
|
|
1070
|
+
name: "Inverted Both (WebUSB) - CH34x",
|
|
1071
|
+
fn: async () => {
|
|
1072
|
+
return await self.hardResetInvertedWebUSB();
|
|
1073
|
+
},
|
|
1074
|
+
});
|
|
1075
|
+
resetStrategies.push({
|
|
1076
|
+
name: "Inverted RTS (WebUSB) - CH34x",
|
|
1077
|
+
fn: async () => {
|
|
1078
|
+
return await self.hardResetInvertedRTSWebUSB();
|
|
1079
|
+
},
|
|
1080
|
+
});
|
|
1081
|
+
resetStrategies.push({
|
|
1082
|
+
name: "Inverted DTR (WebUSB) - CH34x",
|
|
1083
|
+
fn: async () => {
|
|
1084
|
+
return await self.hardResetInvertedDTRWebUSB();
|
|
1085
|
+
},
|
|
1086
|
+
});
|
|
1087
|
+
} else if (isCP2102) {
|
|
1088
|
+
// CP2102: UnixTight works best (tested and confirmed)
|
|
1089
|
+
// Try it first, then fallback to other strategies
|
|
1090
|
+
|
|
1091
|
+
resetStrategies.push({
|
|
1092
|
+
name: "UnixTight (WebUSB) - CP2102",
|
|
1093
|
+
fn: async () => {
|
|
1094
|
+
return await self.hardResetUnixTightWebUSB();
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
resetStrategies.push({
|
|
1099
|
+
name: "Classic (WebUSB) - CP2102",
|
|
1100
|
+
fn: async () => {
|
|
1101
|
+
return await self.hardResetClassicWebUSB();
|
|
1102
|
+
},
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
resetStrategies.push({
|
|
1106
|
+
name: "Inverted Both (WebUSB) - CP2102",
|
|
1107
|
+
fn: async () => {
|
|
1108
|
+
return await self.hardResetInvertedWebUSB();
|
|
1109
|
+
},
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
resetStrategies.push({
|
|
1113
|
+
name: "Inverted RTS (WebUSB) - CP2102",
|
|
1114
|
+
fn: async () => {
|
|
1115
|
+
return await self.hardResetInvertedRTSWebUSB();
|
|
1116
|
+
},
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
resetStrategies.push({
|
|
1120
|
+
name: "Inverted DTR (WebUSB) - CP2102",
|
|
1121
|
+
fn: async () => {
|
|
1122
|
+
return await self.hardResetInvertedDTRWebUSB();
|
|
1123
|
+
},
|
|
1124
|
+
});
|
|
1125
|
+
} else {
|
|
1126
|
+
// For other USB-Serial chips, try UnixTight first, then multiple strategies
|
|
1127
|
+
resetStrategies.push({
|
|
1128
|
+
name: "UnixTight (WebUSB)",
|
|
1129
|
+
fn: async () => {
|
|
1130
|
+
return await self.hardResetUnixTightWebUSB();
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
1133
|
+
resetStrategies.push({
|
|
1134
|
+
name: "Classic (WebUSB)",
|
|
1135
|
+
fn: async function () {
|
|
1136
|
+
return await self.hardResetClassicWebUSB();
|
|
1137
|
+
},
|
|
1138
|
+
});
|
|
1139
|
+
resetStrategies.push({
|
|
1140
|
+
name: "Inverted Both (WebUSB)",
|
|
1141
|
+
fn: async function () {
|
|
1142
|
+
return await self.hardResetInvertedWebUSB();
|
|
1143
|
+
},
|
|
1144
|
+
});
|
|
1145
|
+
resetStrategies.push({
|
|
1146
|
+
name: "Inverted RTS (WebUSB)",
|
|
1147
|
+
fn: async function () {
|
|
1148
|
+
return await self.hardResetInvertedRTSWebUSB();
|
|
1149
|
+
},
|
|
1150
|
+
});
|
|
1151
|
+
resetStrategies.push({
|
|
1152
|
+
name: "Inverted DTR (WebUSB)",
|
|
1153
|
+
fn: async function () {
|
|
1154
|
+
return await self.hardResetInvertedDTRWebUSB();
|
|
1155
|
+
},
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Add general fallback strategies only for non-CP2102 and non-ESP32-S2 Native USB chips
|
|
1161
|
+
if (!isCP2102 && !isESP32S2NativeUSB) {
|
|
1162
|
+
// Classic reset (for chips not handled above)
|
|
1163
|
+
if (portInfo.usbVendorId !== 0x1a86) {
|
|
1164
|
+
resetStrategies.push({
|
|
1165
|
+
name: "Classic (WebUSB)",
|
|
1166
|
+
fn: async function () {
|
|
1167
|
+
return await self.hardResetClassicWebUSB();
|
|
1168
|
+
},
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// UnixTight reset (sets DTR/RTS simultaneously)
|
|
1173
|
+
resetStrategies.push({
|
|
1174
|
+
name: "UnixTight (WebUSB)",
|
|
1175
|
+
fn: async function () {
|
|
1176
|
+
return await self.hardResetUnixTightWebUSB();
|
|
1177
|
+
},
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
// WebUSB Strategy: Classic with long delays
|
|
1181
|
+
resetStrategies.push({
|
|
1182
|
+
name: "Classic Long Delay (WebUSB)",
|
|
1183
|
+
fn: async function () {
|
|
1184
|
+
return await self.hardResetClassicLongDelayWebUSB();
|
|
1185
|
+
},
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
// WebUSB Strategy: Classic with short delays
|
|
1189
|
+
resetStrategies.push({
|
|
1190
|
+
name: "Classic Short Delay (WebUSB)",
|
|
1191
|
+
fn: async function () {
|
|
1192
|
+
return await self.hardResetClassicShortDelayWebUSB();
|
|
1193
|
+
},
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
// WebUSB Strategy: USB-JTAG/Serial fallback
|
|
1197
|
+
if (!isUSBJTAGSerial && !isEspressifUSB) {
|
|
1198
|
+
resetStrategies.push({
|
|
1199
|
+
name: "USB-JTAG/Serial fallback (WebUSB)",
|
|
1200
|
+
fn: async function () {
|
|
1201
|
+
return await self.hardResetUSBJTAGSerialWebUSB();
|
|
1202
|
+
},
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
} else {
|
|
1207
|
+
// Strategy: USB-JTAG/Serial reset
|
|
1208
|
+
if (isUSBJTAGSerial || isEspressifUSB) {
|
|
1209
|
+
resetStrategies.push({
|
|
1210
|
+
name: "USB-JTAG/Serial",
|
|
1211
|
+
fn: async function () {
|
|
1212
|
+
return await self.hardResetUSBJTAGSerial();
|
|
1213
|
+
},
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Strategy: Classic reset
|
|
1218
|
+
resetStrategies.push({
|
|
1219
|
+
name: "Classic",
|
|
1220
|
+
fn: async function () {
|
|
1221
|
+
return await self.hardResetClassic();
|
|
1222
|
+
},
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
// Strategy: USB-JTAG/Serial fallback
|
|
1226
|
+
if (!isUSBJTAGSerial && !isEspressifUSB) {
|
|
1227
|
+
resetStrategies.push({
|
|
1228
|
+
name: "USB-JTAG/Serial (fallback)",
|
|
1229
|
+
fn: async function () {
|
|
1230
|
+
return await self.hardResetUSBJTAGSerial();
|
|
1231
|
+
},
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
let lastError: Error | null = null;
|
|
1237
|
+
|
|
1238
|
+
// Try each reset strategy with timeout
|
|
1239
|
+
for (const strategy of resetStrategies) {
|
|
1240
|
+
try {
|
|
1241
|
+
// Check if port is still open, if not, skip this strategy
|
|
1242
|
+
if (!this.connected || !this.port.writable) {
|
|
1243
|
+
this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Clear abandon flag before starting new strategy
|
|
1248
|
+
this._abandonCurrentOperation = false;
|
|
1249
|
+
|
|
1250
|
+
await strategy.fn();
|
|
1251
|
+
|
|
1252
|
+
// Try to sync after reset with internally time-bounded sync (3 seconds per strategy)
|
|
1253
|
+
const syncSuccess = await this.syncWithTimeout(3000);
|
|
1254
|
+
|
|
1255
|
+
if (syncSuccess) {
|
|
1256
|
+
// Sync succeeded
|
|
1257
|
+
this.logger.log(
|
|
1258
|
+
`Connected successfully with ${strategy.name} reset.`,
|
|
1259
|
+
);
|
|
1260
|
+
return;
|
|
1261
|
+
} else {
|
|
1262
|
+
throw new Error("Sync timeout or abandoned");
|
|
1263
|
+
}
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
lastError = error as Error;
|
|
1266
|
+
this.logger.log(
|
|
1267
|
+
`${strategy.name} reset failed: ${(error as Error).message}`,
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1270
|
+
// Set abandon flag to stop any in-flight operations
|
|
1271
|
+
this._abandonCurrentOperation = true;
|
|
1272
|
+
|
|
1273
|
+
// Wait a bit for in-flight operations to abort
|
|
1274
|
+
await sleep(100);
|
|
1275
|
+
|
|
1276
|
+
// If port got disconnected, we can't try more strategies
|
|
1277
|
+
if (!this.connected || !this.port.writable) {
|
|
1278
|
+
this.logger.log(`Port disconnected during reset attempt`);
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Clear buffers before trying next strategy
|
|
1283
|
+
this._clearInputBuffer();
|
|
1284
|
+
await this.drainInputBuffer(200);
|
|
1285
|
+
await this.flushSerialBuffers();
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// All strategies failed - reset abandon flag before throwing
|
|
1290
|
+
this._abandonCurrentOperation = false;
|
|
1291
|
+
|
|
1292
|
+
throw new Error(
|
|
1293
|
+
`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError?.message}`,
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
470
1297
|
async hardReset(bootloader = false) {
|
|
471
1298
|
if (bootloader) {
|
|
472
1299
|
// enter flash mode
|
|
@@ -474,15 +1301,31 @@ export class ESPLoader extends EventTarget {
|
|
|
474
1301
|
await this.hardResetUSBJTAGSerial();
|
|
475
1302
|
this.logger.log("USB-JTAG/Serial reset.");
|
|
476
1303
|
} else {
|
|
477
|
-
|
|
478
|
-
this.
|
|
1304
|
+
// Use different reset strategy for WebUSB (Android) vs Web Serial (Desktop)
|
|
1305
|
+
if (this.isWebUSB()) {
|
|
1306
|
+
await this.hardResetClassicWebUSB();
|
|
1307
|
+
this.logger.log("Classic reset (WebUSB/Android).");
|
|
1308
|
+
} else {
|
|
1309
|
+
await this.hardResetClassic();
|
|
1310
|
+
this.logger.log("Classic reset.");
|
|
1311
|
+
}
|
|
479
1312
|
}
|
|
480
1313
|
} else {
|
|
481
|
-
// just reset
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
1314
|
+
// just reset (no bootloader mode)
|
|
1315
|
+
if (this.isWebUSB()) {
|
|
1316
|
+
// WebUSB: Use longer delays for better compatibility
|
|
1317
|
+
await this.setRTSWebUSB(true); // EN->LOW
|
|
1318
|
+
await this.sleep(200);
|
|
1319
|
+
await this.setRTSWebUSB(false);
|
|
1320
|
+
await this.sleep(200);
|
|
1321
|
+
this.logger.log("Hard reset (WebUSB).");
|
|
1322
|
+
} else {
|
|
1323
|
+
// Web Serial: Standard reset
|
|
1324
|
+
await this.setRTS(true); // EN->LOW
|
|
1325
|
+
await this.sleep(100);
|
|
1326
|
+
await this.setRTS(false);
|
|
1327
|
+
this.logger.log("Hard reset.");
|
|
1328
|
+
}
|
|
486
1329
|
}
|
|
487
1330
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
488
1331
|
}
|
|
@@ -613,6 +1456,13 @@ export class ESPLoader extends EventTarget {
|
|
|
613
1456
|
statusLen = 4;
|
|
614
1457
|
} else if ([2, 4].includes(data.length)) {
|
|
615
1458
|
statusLen = data.length;
|
|
1459
|
+
} else {
|
|
1460
|
+
// Default to 2-byte status if we can't determine
|
|
1461
|
+
// This prevents silent data corruption when statusLen would be 0
|
|
1462
|
+
statusLen = 2;
|
|
1463
|
+
this.logger.debug(
|
|
1464
|
+
`Unknown chip family, defaulting to 2-byte status (opcode: ${toHex(opcode)}, data.length: ${data.length})`,
|
|
1465
|
+
);
|
|
616
1466
|
}
|
|
617
1467
|
}
|
|
618
1468
|
|
|
@@ -656,6 +1506,7 @@ export class ESPLoader extends EventTarget {
|
|
|
656
1506
|
...pack("<BBHI", 0x00, opcode, buffer.length, checksum),
|
|
657
1507
|
...buffer,
|
|
658
1508
|
]);
|
|
1509
|
+
|
|
659
1510
|
if (this.debug) {
|
|
660
1511
|
this.logger.debug(
|
|
661
1512
|
`Writing ${packet.length} byte${packet.length == 1 ? "" : "s"}:`,
|
|
@@ -669,80 +1520,188 @@ export class ESPLoader extends EventTarget {
|
|
|
669
1520
|
* @name readPacket
|
|
670
1521
|
* Generator to read SLIP packets from a serial port.
|
|
671
1522
|
* Yields one full SLIP packet at a time, raises exception on timeout or invalid data.
|
|
1523
|
+
*
|
|
1524
|
+
* Two implementations:
|
|
1525
|
+
* - Burst: CDC devices (Native USB) and CH343 - very fast processing
|
|
1526
|
+
* - Byte-by-byte: CH340, CP2102, and other USB-Serial adapters - stable fast processing
|
|
672
1527
|
*/
|
|
673
|
-
|
|
674
1528
|
async readPacket(timeout: number): Promise<number[]> {
|
|
675
1529
|
let partialPacket: number[] | null = null;
|
|
676
1530
|
let inEscape = false;
|
|
677
1531
|
|
|
678
|
-
|
|
1532
|
+
// CDC devices use burst processing, non-CDC use byte-by-byte
|
|
1533
|
+
if (this._isCDCDevice) {
|
|
1534
|
+
// Burst version: Process all available bytes in one pass for ultra-high-speed transfers
|
|
1535
|
+
// Used for: CDC devices (all platforms) and CH343
|
|
1536
|
+
const startTime = Date.now();
|
|
1537
|
+
|
|
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
|
+
}
|
|
679
1545
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
1546
|
+
// Check timeout
|
|
1547
|
+
if (Date.now() - startTime > timeout) {
|
|
1548
|
+
const waitingFor = partialPacket === null ? "header" : "content";
|
|
1549
|
+
throw new SlipReadError("Timed out waiting for packet " + waitingFor);
|
|
1550
|
+
}
|
|
686
1551
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1552
|
+
// If no data available, wait a bit
|
|
1553
|
+
if (this._inputBufferAvailable === 0) {
|
|
1554
|
+
await sleep(1);
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
692
1557
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1558
|
+
// Process all available bytes without going back to outer loop
|
|
1559
|
+
// This is critical for handling high-speed burst transfers
|
|
1560
|
+
while (this._inputBufferAvailable > 0) {
|
|
1561
|
+
// Periodic timeout check to prevent hang on slow data
|
|
1562
|
+
if (Date.now() - startTime > timeout) {
|
|
1563
|
+
const waitingFor = partialPacket === null ? "header" : "content";
|
|
1564
|
+
throw new SlipReadError(
|
|
1565
|
+
"Timed out waiting for packet " + waitingFor,
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
const b = this._readByte()!;
|
|
697
1569
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1570
|
+
if (partialPacket === null) {
|
|
1571
|
+
// waiting for packet header
|
|
1572
|
+
if (b == 0xc0) {
|
|
1573
|
+
partialPacket = [];
|
|
1574
|
+
} else {
|
|
1575
|
+
if (this.debug) {
|
|
1576
|
+
this.logger.debug("Read invalid data: " + toHex(b));
|
|
1577
|
+
this.logger.debug(
|
|
1578
|
+
"Remaining data in serial buffer: " +
|
|
1579
|
+
hexFormatter(this._inputBuffer),
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
throw new SlipReadError(
|
|
1583
|
+
"Invalid head of packet (" + toHex(b) + ")",
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
} else if (inEscape) {
|
|
1587
|
+
// part-way through escape sequence
|
|
1588
|
+
inEscape = false;
|
|
1589
|
+
if (b == 0xdc) {
|
|
1590
|
+
partialPacket.push(0xc0);
|
|
1591
|
+
} else if (b == 0xdd) {
|
|
1592
|
+
partialPacket.push(0xdb);
|
|
1593
|
+
} else {
|
|
1594
|
+
if (this.debug) {
|
|
1595
|
+
this.logger.debug("Read invalid data: " + toHex(b));
|
|
1596
|
+
this.logger.debug(
|
|
1597
|
+
"Remaining data in serial buffer: " +
|
|
1598
|
+
hexFormatter(this._inputBuffer),
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
throw new SlipReadError(
|
|
1602
|
+
"Invalid SLIP escape (0xdb, " + toHex(b) + ")",
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
} else if (b == 0xdb) {
|
|
1606
|
+
// start of escape sequence
|
|
1607
|
+
inEscape = true;
|
|
1608
|
+
} else if (b == 0xc0) {
|
|
1609
|
+
// end of packet
|
|
1610
|
+
if (this.debug)
|
|
705
1611
|
this.logger.debug(
|
|
706
|
-
"
|
|
707
|
-
|
|
1612
|
+
"Received full packet: " + hexFormatter(partialPacket),
|
|
1613
|
+
);
|
|
1614
|
+
// Compact buffer periodically to prevent memory growth
|
|
1615
|
+
this._compactInputBuffer();
|
|
1616
|
+
return partialPacket;
|
|
1617
|
+
} else {
|
|
1618
|
+
// normal byte in packet
|
|
1619
|
+
partialPacket.push(b);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
} else {
|
|
1624
|
+
// Byte-by-byte version: Stable for non CDC USB-Serial adapters (CH340, CP2102, etc.)
|
|
1625
|
+
let readBytes: number[] = [];
|
|
1626
|
+
while (true) {
|
|
1627
|
+
// Check abandon flag (for reset strategy timeout)
|
|
1628
|
+
if (this._abandonCurrentOperation) {
|
|
1629
|
+
throw new SlipReadError(
|
|
1630
|
+
"Operation abandoned (reset strategy timeout)",
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
const stamp = Date.now();
|
|
1635
|
+
readBytes = [];
|
|
1636
|
+
while (Date.now() - stamp < timeout) {
|
|
1637
|
+
if (this._inputBufferAvailable > 0) {
|
|
1638
|
+
readBytes.push(this._readByte()!);
|
|
1639
|
+
break;
|
|
1640
|
+
} else {
|
|
1641
|
+
// Reduced sleep time for faster response during high-speed transfers
|
|
1642
|
+
await sleep(1);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
if (readBytes.length == 0) {
|
|
1646
|
+
const waitingFor = partialPacket === null ? "header" : "content";
|
|
1647
|
+
throw new SlipReadError("Timed out waiting for packet " + waitingFor);
|
|
1648
|
+
}
|
|
1649
|
+
if (this.debug)
|
|
1650
|
+
this.logger.debug(
|
|
1651
|
+
"Read " + readBytes.length + " bytes: " + hexFormatter(readBytes),
|
|
1652
|
+
);
|
|
1653
|
+
for (const b of readBytes) {
|
|
1654
|
+
if (partialPacket === null) {
|
|
1655
|
+
// waiting for packet header
|
|
1656
|
+
if (b == 0xc0) {
|
|
1657
|
+
partialPacket = [];
|
|
1658
|
+
} else {
|
|
1659
|
+
if (this.debug) {
|
|
1660
|
+
this.logger.debug("Read invalid data: " + toHex(b));
|
|
1661
|
+
this.logger.debug(
|
|
1662
|
+
"Remaining data in serial buffer: " +
|
|
1663
|
+
hexFormatter(this._inputBuffer),
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
throw new SlipReadError(
|
|
1667
|
+
"Invalid head of packet (" + toHex(b) + ")",
|
|
708
1668
|
);
|
|
709
1669
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1670
|
+
} else if (inEscape) {
|
|
1671
|
+
// part-way through escape sequence
|
|
1672
|
+
inEscape = false;
|
|
1673
|
+
if (b == 0xdc) {
|
|
1674
|
+
partialPacket.push(0xc0);
|
|
1675
|
+
} else if (b == 0xdd) {
|
|
1676
|
+
partialPacket.push(0xdb);
|
|
1677
|
+
} else {
|
|
1678
|
+
if (this.debug) {
|
|
1679
|
+
this.logger.debug("Read invalid data: " + toHex(b));
|
|
1680
|
+
this.logger.debug(
|
|
1681
|
+
"Remaining data in serial buffer: " +
|
|
1682
|
+
hexFormatter(this._inputBuffer),
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
throw new SlipReadError(
|
|
1686
|
+
"Invalid SLIP escape (0xdb, " + toHex(b) + ")",
|
|
727
1687
|
);
|
|
728
1688
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1689
|
+
} else if (b == 0xdb) {
|
|
1690
|
+
// start of escape sequence
|
|
1691
|
+
inEscape = true;
|
|
1692
|
+
} else if (b == 0xc0) {
|
|
1693
|
+
// end of packet
|
|
1694
|
+
if (this.debug)
|
|
1695
|
+
this.logger.debug(
|
|
1696
|
+
"Received full packet: " + hexFormatter(partialPacket),
|
|
1697
|
+
);
|
|
1698
|
+
// Compact buffer periodically to prevent memory growth
|
|
1699
|
+
this._compactInputBuffer();
|
|
1700
|
+
return partialPacket;
|
|
1701
|
+
} else {
|
|
1702
|
+
// normal byte in packet
|
|
1703
|
+
partialPacket.push(b);
|
|
732
1704
|
}
|
|
733
|
-
} else if (b == 0xdb) {
|
|
734
|
-
// start of escape sequence
|
|
735
|
-
inEscape = true;
|
|
736
|
-
} else if (b == 0xc0) {
|
|
737
|
-
// end of packet
|
|
738
|
-
if (this.debug)
|
|
739
|
-
this.logger.debug(
|
|
740
|
-
"Received full packet: " + hexFormatter(partialPacket),
|
|
741
|
-
);
|
|
742
|
-
return partialPacket;
|
|
743
|
-
} else {
|
|
744
|
-
// normal byte in packet
|
|
745
|
-
partialPacket.push(b);
|
|
746
1705
|
}
|
|
747
1706
|
}
|
|
748
1707
|
}
|
|
@@ -766,6 +1725,7 @@ export class ESPLoader extends EventTarget {
|
|
|
766
1725
|
}
|
|
767
1726
|
|
|
768
1727
|
const [resp, opRet, , val] = unpack("<BBHI", packet.slice(0, 8));
|
|
1728
|
+
|
|
769
1729
|
if (resp != 1) {
|
|
770
1730
|
continue;
|
|
771
1731
|
}
|
|
@@ -780,7 +1740,7 @@ export class ESPLoader extends EventTarget {
|
|
|
780
1740
|
throw new Error(`Invalid (unsupported) command ${toHex(opcode)}`);
|
|
781
1741
|
}
|
|
782
1742
|
}
|
|
783
|
-
throw "Response doesn't match request";
|
|
1743
|
+
throw new Error("Response doesn't match request");
|
|
784
1744
|
}
|
|
785
1745
|
|
|
786
1746
|
/**
|
|
@@ -843,9 +1803,10 @@ export class ESPLoader extends EventTarget {
|
|
|
843
1803
|
}
|
|
844
1804
|
|
|
845
1805
|
async reconfigurePort(baud: number) {
|
|
846
|
-
|
|
847
|
-
|
|
1806
|
+
// Block new writes during the entire reconfiguration (all paths)
|
|
1807
|
+
this._isReconfiguring = true;
|
|
848
1808
|
|
|
1809
|
+
try {
|
|
849
1810
|
// Wait for pending writes to complete
|
|
850
1811
|
try {
|
|
851
1812
|
await this._writeChain;
|
|
@@ -853,6 +1814,35 @@ export class ESPLoader extends EventTarget {
|
|
|
853
1814
|
this.logger.debug(`Pending write error during reconfigure: ${err}`);
|
|
854
1815
|
}
|
|
855
1816
|
|
|
1817
|
+
// WebUSB: Check if we should use setBaudRate() or close/reopen
|
|
1818
|
+
if (this.isWebUSB()) {
|
|
1819
|
+
const portInfo = this.port.getInfo();
|
|
1820
|
+
const isCH343 =
|
|
1821
|
+
portInfo.usbVendorId === 0x1a86 && portInfo.usbProductId === 0x55d3;
|
|
1822
|
+
|
|
1823
|
+
// CH343 is a CDC device and MUST use close/reopen
|
|
1824
|
+
// Other chips (CH340, CP2102, FTDI) MUST use setBaudRate()
|
|
1825
|
+
if (
|
|
1826
|
+
!isCH343 &&
|
|
1827
|
+
typeof (this.port as WebUSBSerialPort).setBaudRate === "function"
|
|
1828
|
+
) {
|
|
1829
|
+
// this.logger.log(
|
|
1830
|
+
// `[WebUSB] Changing baudrate to ${baud} using setBaudRate()...`,
|
|
1831
|
+
// );
|
|
1832
|
+
await (this.port as WebUSBSerialPort).setBaudRate(baud);
|
|
1833
|
+
// this.logger.log(`[WebUSB] Baudrate changed to ${baud}`);
|
|
1834
|
+
|
|
1835
|
+
// Give the chip time to adjust to new baudrate
|
|
1836
|
+
await sleep(100);
|
|
1837
|
+
return;
|
|
1838
|
+
} else if (isCH343) {
|
|
1839
|
+
// this.logger.log(
|
|
1840
|
+
// `[WebUSB] CH343 detected - using close/reopen for baudrate change`,
|
|
1841
|
+
// );
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// Web Serial or CH343: Close and reopen port
|
|
856
1846
|
// Release persistent writer before closing
|
|
857
1847
|
if (this._writer) {
|
|
858
1848
|
try {
|
|
@@ -882,137 +1872,50 @@ export class ESPLoader extends EventTarget {
|
|
|
882
1872
|
this.logger.error(`Reconfigure port error: ${e}`);
|
|
883
1873
|
throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
|
|
884
1874
|
} finally {
|
|
1875
|
+
// Always reset flag, even on error or early return
|
|
885
1876
|
this._isReconfiguring = false;
|
|
886
1877
|
}
|
|
887
1878
|
}
|
|
888
1879
|
|
|
889
1880
|
/**
|
|
890
|
-
* @name
|
|
891
|
-
*
|
|
892
|
-
*
|
|
1881
|
+
* @name syncWithTimeout
|
|
1882
|
+
* Sync with timeout that can be abandoned (for reset strategy loop)
|
|
1883
|
+
* This is internally time-bounded and checks the abandon flag
|
|
893
1884
|
*/
|
|
894
|
-
async
|
|
895
|
-
const
|
|
896
|
-
const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
|
|
897
|
-
const isEspressifUSB = portInfo.usbVendorId === 0x303a;
|
|
898
|
-
|
|
899
|
-
this.logger.log(
|
|
900
|
-
`Detected USB: VID=0x${portInfo.usbVendorId?.toString(16) || "unknown"}, PID=0x${portInfo.usbProductId?.toString(16) || "unknown"}`,
|
|
901
|
-
);
|
|
902
|
-
|
|
903
|
-
// Define reset strategies to try in order
|
|
904
|
-
const resetStrategies: Array<{ name: string; fn: () => Promise<void> }> =
|
|
905
|
-
[];
|
|
906
|
-
|
|
907
|
-
// Strategy 1: USB-JTAG/Serial reset (for ESP32-C3, C6, S3, etc.)
|
|
908
|
-
// Try this first if we detect Espressif USB VID or the specific PID
|
|
909
|
-
if (isUSBJTAGSerial || isEspressifUSB) {
|
|
910
|
-
resetStrategies.push({
|
|
911
|
-
name: "USB-JTAG/Serial",
|
|
912
|
-
fn: async () => await this.hardResetUSBJTAGSerial(),
|
|
913
|
-
});
|
|
914
|
-
}
|
|
1885
|
+
async syncWithTimeout(timeoutMs: number): Promise<boolean> {
|
|
1886
|
+
const startTime = Date.now();
|
|
915
1887
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1888
|
+
for (let i = 0; i < 5; i++) {
|
|
1889
|
+
// Check if we've exceeded the timeout
|
|
1890
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
921
1893
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
fn: async () => await this.hardResetUSBJTAGSerial(),
|
|
927
|
-
});
|
|
928
|
-
}
|
|
1894
|
+
// Check abandon flag
|
|
1895
|
+
if (this._abandonCurrentOperation) {
|
|
1896
|
+
return false;
|
|
1897
|
+
}
|
|
929
1898
|
|
|
930
|
-
|
|
1899
|
+
this._clearInputBuffer();
|
|
931
1900
|
|
|
932
|
-
// Try each reset strategy
|
|
933
|
-
for (const strategy of resetStrategies) {
|
|
934
1901
|
try {
|
|
935
|
-
this.
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
|
|
940
|
-
continue;
|
|
1902
|
+
const response = await this._sync();
|
|
1903
|
+
if (response) {
|
|
1904
|
+
await sleep(SYNC_TIMEOUT);
|
|
1905
|
+
return true;
|
|
941
1906
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
// If we get here, sync succeeded
|
|
949
|
-
this.logger.log(`Connected successfully with ${strategy.name} reset.`);
|
|
950
|
-
return;
|
|
951
|
-
} catch (error) {
|
|
952
|
-
lastError = error as Error;
|
|
953
|
-
this.logger.log(
|
|
954
|
-
`${strategy.name} reset failed: ${(error as Error).message}`,
|
|
955
|
-
);
|
|
956
|
-
|
|
957
|
-
// If port got disconnected, we can't try more strategies
|
|
958
|
-
if (!this.connected || !this.port.writable) {
|
|
959
|
-
this.logger.log(`Port disconnected during reset attempt`);
|
|
960
|
-
break;
|
|
1907
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1908
|
+
} catch (e) {
|
|
1909
|
+
// Check abandon flag after error
|
|
1910
|
+
if (this._abandonCurrentOperation) {
|
|
1911
|
+
return false;
|
|
961
1912
|
}
|
|
962
|
-
|
|
963
|
-
// Clear buffers before trying next strategy
|
|
964
|
-
this._inputBuffer.length = 0;
|
|
965
|
-
await this.drainInputBuffer(200);
|
|
966
|
-
await this.flushSerialBuffers();
|
|
967
1913
|
}
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
// All strategies failed
|
|
971
|
-
throw new Error(
|
|
972
|
-
`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError?.message}`,
|
|
973
|
-
);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
/**
|
|
977
|
-
* @name hardResetUSBJTAGSerial
|
|
978
|
-
* USB-JTAG/Serial reset sequence for ESP32-C3, ESP32-S3, ESP32-C6, etc.
|
|
979
|
-
*/
|
|
980
|
-
async hardResetUSBJTAGSerial() {
|
|
981
|
-
await this.setRTS(false);
|
|
982
|
-
await this.setDTR(false); // Idle
|
|
983
|
-
await this.sleep(100);
|
|
984
|
-
|
|
985
|
-
await this.setDTR(true); // Set IO0
|
|
986
|
-
await this.setRTS(false);
|
|
987
|
-
await this.sleep(100);
|
|
988
|
-
|
|
989
|
-
await this.setRTS(true); // Reset. Calls inverted to go through (1,1) instead of (0,0)
|
|
990
|
-
await this.setDTR(false);
|
|
991
|
-
await this.setRTS(true); // RTS set as Windows only propagates DTR on RTS setting
|
|
992
|
-
await this.sleep(100);
|
|
993
|
-
|
|
994
|
-
await this.setDTR(false);
|
|
995
|
-
await this.setRTS(false); // Chip out of reset
|
|
996
|
-
|
|
997
|
-
// Wait for chip to boot into bootloader
|
|
998
|
-
await this.sleep(200);
|
|
999
|
-
}
|
|
1000
1914
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
* Classic reset sequence for USB-to-Serial bridge chips (CH340, CP2102, etc.)
|
|
1004
|
-
*/
|
|
1005
|
-
async hardResetClassic() {
|
|
1006
|
-
await this.setDTR(false); // IO0=HIGH
|
|
1007
|
-
await this.setRTS(true); // EN=LOW, chip in reset
|
|
1008
|
-
await this.sleep(100);
|
|
1009
|
-
await this.setDTR(true); // IO0=LOW
|
|
1010
|
-
await this.setRTS(false); // EN=HIGH, chip out of reset
|
|
1011
|
-
await this.sleep(50);
|
|
1012
|
-
await this.setDTR(false); // IO0=HIGH, done
|
|
1915
|
+
await sleep(SYNC_TIMEOUT);
|
|
1916
|
+
}
|
|
1013
1917
|
|
|
1014
|
-
|
|
1015
|
-
await this.sleep(200);
|
|
1918
|
+
return false;
|
|
1016
1919
|
}
|
|
1017
1920
|
|
|
1018
1921
|
/**
|
|
@@ -1022,7 +1925,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1022
1925
|
*/
|
|
1023
1926
|
async sync() {
|
|
1024
1927
|
for (let i = 0; i < 5; i++) {
|
|
1025
|
-
this.
|
|
1928
|
+
this._clearInputBuffer();
|
|
1026
1929
|
const response = await this._sync();
|
|
1027
1930
|
if (response) {
|
|
1028
1931
|
await sleep(SYNC_TIMEOUT);
|
|
@@ -1041,14 +1944,17 @@ export class ESPLoader extends EventTarget {
|
|
|
1041
1944
|
*/
|
|
1042
1945
|
async _sync() {
|
|
1043
1946
|
await this.sendCommand(ESP_SYNC, SYNC_PACKET);
|
|
1947
|
+
|
|
1044
1948
|
for (let i = 0; i < 8; i++) {
|
|
1045
1949
|
try {
|
|
1046
1950
|
const [, data] = await this.getResponse(ESP_SYNC, SYNC_TIMEOUT);
|
|
1047
1951
|
if (data.length > 1 && data[0] == 0 && data[1] == 0) {
|
|
1048
1952
|
return true;
|
|
1049
1953
|
}
|
|
1050
|
-
} catch {
|
|
1051
|
-
|
|
1954
|
+
} catch (e) {
|
|
1955
|
+
if (this.debug) {
|
|
1956
|
+
this.logger.debug(`Sync attempt ${i + 1} failed: ${e}`);
|
|
1957
|
+
}
|
|
1052
1958
|
}
|
|
1053
1959
|
}
|
|
1054
1960
|
return false;
|
|
@@ -1692,13 +2598,21 @@ export class ESPLoader extends EventTarget {
|
|
|
1692
2598
|
},
|
|
1693
2599
|
async () => {
|
|
1694
2600
|
// Previous write failed, but still attempt this write
|
|
2601
|
+
this.logger.debug(
|
|
2602
|
+
"Previous write failed, attempting recovery for current write",
|
|
2603
|
+
);
|
|
1695
2604
|
if (!this.port.writable) {
|
|
1696
2605
|
throw new Error("Port became unavailable during write");
|
|
1697
2606
|
}
|
|
1698
2607
|
|
|
1699
2608
|
// Writer was likely cleaned up by previous error, create new one
|
|
1700
2609
|
if (!this._writer) {
|
|
1701
|
-
|
|
2610
|
+
try {
|
|
2611
|
+
this._writer = this.port.writable.getWriter();
|
|
2612
|
+
} catch (err) {
|
|
2613
|
+
this.logger.debug(`Failed to get writer in recovery: ${err}`);
|
|
2614
|
+
throw new Error("Cannot acquire writer lock");
|
|
2615
|
+
}
|
|
1702
2616
|
}
|
|
1703
2617
|
|
|
1704
2618
|
await this._writer.write(new Uint8Array(data));
|
|
@@ -1710,7 +2624,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1710
2624
|
if (this._writer) {
|
|
1711
2625
|
try {
|
|
1712
2626
|
this._writer.releaseLock();
|
|
1713
|
-
} catch
|
|
2627
|
+
} catch {
|
|
1714
2628
|
// Ignore release errors
|
|
1715
2629
|
}
|
|
1716
2630
|
this._writer = undefined;
|
|
@@ -1733,47 +2647,73 @@ export class ESPLoader extends EventTarget {
|
|
|
1733
2647
|
return;
|
|
1734
2648
|
}
|
|
1735
2649
|
|
|
2650
|
+
// Wait for pending writes to complete
|
|
1736
2651
|
try {
|
|
1737
|
-
this.
|
|
2652
|
+
await this._writeChain;
|
|
2653
|
+
} catch (err) {
|
|
2654
|
+
this.logger.debug(`Pending write error during disconnect: ${err}`);
|
|
2655
|
+
}
|
|
1738
2656
|
|
|
1739
|
-
|
|
2657
|
+
// Release persistent writer before closing
|
|
2658
|
+
if (this._writer) {
|
|
1740
2659
|
try {
|
|
1741
|
-
await this.
|
|
2660
|
+
await this._writer.close();
|
|
2661
|
+
this._writer.releaseLock();
|
|
2662
|
+
} catch (err) {
|
|
2663
|
+
this.logger.debug(`Writer close/release error: ${err}`);
|
|
2664
|
+
}
|
|
2665
|
+
this._writer = undefined;
|
|
2666
|
+
} else {
|
|
2667
|
+
// No persistent writer exists, close stream directly
|
|
2668
|
+
// This path is taken when no writes have been queued
|
|
2669
|
+
try {
|
|
2670
|
+
const writer = this.port.writable.getWriter();
|
|
2671
|
+
await writer.close();
|
|
2672
|
+
writer.releaseLock();
|
|
1742
2673
|
} catch (err) {
|
|
1743
|
-
this.logger.debug(`
|
|
2674
|
+
this.logger.debug(`Direct writer close error: ${err}`);
|
|
1744
2675
|
}
|
|
2676
|
+
}
|
|
1745
2677
|
|
|
1746
|
-
|
|
1747
|
-
if (this.
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
this._writer.releaseLock();
|
|
1751
|
-
} catch (err) {
|
|
1752
|
-
this.logger.debug(`Writer close/release error: ${err}`);
|
|
1753
|
-
}
|
|
1754
|
-
this._writer = undefined;
|
|
1755
|
-
} else {
|
|
1756
|
-
// No persistent writer exists, close stream directly
|
|
1757
|
-
// This path is taken when no writes have been queued
|
|
1758
|
-
try {
|
|
1759
|
-
const writer = this.port.writable.getWriter();
|
|
1760
|
-
await writer.close();
|
|
1761
|
-
writer.releaseLock();
|
|
1762
|
-
} catch (err) {
|
|
1763
|
-
this.logger.debug(`Direct writer close error: ${err}`);
|
|
1764
|
-
}
|
|
2678
|
+
await new Promise((resolve) => {
|
|
2679
|
+
if (!this._reader) {
|
|
2680
|
+
resolve(undefined);
|
|
2681
|
+
return;
|
|
1765
2682
|
}
|
|
1766
2683
|
|
|
1767
|
-
|
|
1768
|
-
|
|
2684
|
+
// Set a timeout to prevent hanging (important for node-usb)
|
|
2685
|
+
const timeout = setTimeout(() => {
|
|
2686
|
+
this.logger.debug("Disconnect timeout - forcing resolution");
|
|
2687
|
+
resolve(undefined);
|
|
2688
|
+
}, 1000);
|
|
2689
|
+
|
|
2690
|
+
this.addEventListener(
|
|
2691
|
+
"disconnect",
|
|
2692
|
+
() => {
|
|
2693
|
+
clearTimeout(timeout);
|
|
1769
2694
|
resolve(undefined);
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
2695
|
+
},
|
|
2696
|
+
{ once: true },
|
|
2697
|
+
);
|
|
2698
|
+
|
|
2699
|
+
// Only cancel if reader is still active
|
|
2700
|
+
try {
|
|
2701
|
+
this._reader.cancel();
|
|
2702
|
+
} catch (err) {
|
|
2703
|
+
this.logger.debug(`Reader cancel error: ${err}`);
|
|
2704
|
+
// Reader already released, resolve immediately
|
|
2705
|
+
clearTimeout(timeout);
|
|
2706
|
+
resolve(undefined);
|
|
2707
|
+
}
|
|
2708
|
+
});
|
|
2709
|
+
this.connected = false;
|
|
2710
|
+
|
|
2711
|
+
// Close the port (important for node-usb adapter)
|
|
2712
|
+
try {
|
|
2713
|
+
await this.port.close();
|
|
2714
|
+
this.logger.debug("Port closed successfully");
|
|
2715
|
+
} catch (err) {
|
|
2716
|
+
this.logger.debug(`Port close error: ${err}`);
|
|
1777
2717
|
}
|
|
1778
2718
|
}
|
|
1779
2719
|
|
|
@@ -1788,12 +2728,11 @@ export class ESPLoader extends EventTarget {
|
|
|
1788
2728
|
}
|
|
1789
2729
|
|
|
1790
2730
|
try {
|
|
1791
|
-
this._isReconfiguring = true;
|
|
1792
|
-
|
|
1793
2731
|
this.logger.log("Reconnecting serial port...");
|
|
1794
2732
|
|
|
1795
2733
|
this.connected = false;
|
|
1796
2734
|
this.__inputBuffer = [];
|
|
2735
|
+
this.__inputBufferReadIndex = 0;
|
|
1797
2736
|
|
|
1798
2737
|
// Wait for pending writes to complete
|
|
1799
2738
|
try {
|
|
@@ -1802,6 +2741,9 @@ export class ESPLoader extends EventTarget {
|
|
|
1802
2741
|
this.logger.debug(`Pending write error during reconnect: ${err}`);
|
|
1803
2742
|
}
|
|
1804
2743
|
|
|
2744
|
+
// Block new writes during port close/open
|
|
2745
|
+
this._isReconfiguring = true;
|
|
2746
|
+
|
|
1805
2747
|
// Release persistent writer
|
|
1806
2748
|
if (this._writer) {
|
|
1807
2749
|
try {
|
|
@@ -1846,6 +2788,9 @@ export class ESPLoader extends EventTarget {
|
|
|
1846
2788
|
);
|
|
1847
2789
|
}
|
|
1848
2790
|
|
|
2791
|
+
// Port is now open and ready - allow writes for initialization
|
|
2792
|
+
this._isReconfiguring = false;
|
|
2793
|
+
|
|
1849
2794
|
// Save chip info and flash size (no need to detect again)
|
|
1850
2795
|
const savedChipFamily = this.chipFamily;
|
|
1851
2796
|
const savedChipName = this.chipName;
|
|
@@ -1858,6 +2803,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1858
2803
|
|
|
1859
2804
|
if (!this._parent) {
|
|
1860
2805
|
this.__inputBuffer = [];
|
|
2806
|
+
this.__inputBufferReadIndex = 0;
|
|
1861
2807
|
this.__totalBytesRead = 0;
|
|
1862
2808
|
this.readLoop();
|
|
1863
2809
|
}
|
|
@@ -1895,13 +2841,16 @@ export class ESPLoader extends EventTarget {
|
|
|
1895
2841
|
}
|
|
1896
2842
|
}
|
|
1897
2843
|
|
|
1898
|
-
//
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
2844
|
+
// The stub is now running on the chip
|
|
2845
|
+
// stubLoader has this instance as _parent, so all operations go through this
|
|
2846
|
+
// We just need to mark this instance as running stub code
|
|
2847
|
+
this.IS_STUB = true;
|
|
2848
|
+
|
|
1902
2849
|
this.logger.debug("Reconnection successful");
|
|
1903
|
-
}
|
|
2850
|
+
} catch (err) {
|
|
2851
|
+
// Ensure flag is reset on error
|
|
1904
2852
|
this._isReconfiguring = false;
|
|
2853
|
+
throw err;
|
|
1905
2854
|
}
|
|
1906
2855
|
}
|
|
1907
2856
|
|
|
@@ -1931,8 +2880,8 @@ export class ESPLoader extends EventTarget {
|
|
|
1931
2880
|
const drainTimeout = 100; // Short timeout for draining
|
|
1932
2881
|
|
|
1933
2882
|
while (drained < bytesToDrain && Date.now() - drainStart < drainTimeout) {
|
|
1934
|
-
if (this.
|
|
1935
|
-
const byte = this.
|
|
2883
|
+
if (this._inputBufferAvailable > 0) {
|
|
2884
|
+
const byte = this._readByte();
|
|
1936
2885
|
if (byte !== undefined) {
|
|
1937
2886
|
drained++;
|
|
1938
2887
|
}
|
|
@@ -1949,6 +2898,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1949
2898
|
// Final clear of application buffer
|
|
1950
2899
|
if (!this._parent) {
|
|
1951
2900
|
this.__inputBuffer = [];
|
|
2901
|
+
this.__inputBufferReadIndex = 0;
|
|
1952
2902
|
}
|
|
1953
2903
|
}
|
|
1954
2904
|
|
|
@@ -1961,6 +2911,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1961
2911
|
// Clear application buffer
|
|
1962
2912
|
if (!this._parent) {
|
|
1963
2913
|
this.__inputBuffer = [];
|
|
2914
|
+
this.__inputBufferReadIndex = 0;
|
|
1964
2915
|
}
|
|
1965
2916
|
|
|
1966
2917
|
// Wait for any pending data
|
|
@@ -1969,6 +2920,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1969
2920
|
// Final clear
|
|
1970
2921
|
if (!this._parent) {
|
|
1971
2922
|
this.__inputBuffer = [];
|
|
2923
|
+
this.__inputBufferReadIndex = 0;
|
|
1972
2924
|
}
|
|
1973
2925
|
|
|
1974
2926
|
this.logger.debug("Serial buffers flushed");
|
|
@@ -2004,7 +2956,38 @@ export class ESPLoader extends EventTarget {
|
|
|
2004
2956
|
`Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`,
|
|
2005
2957
|
);
|
|
2006
2958
|
|
|
2007
|
-
|
|
2959
|
+
// Initialize adaptive speed multipliers for WebUSB devices
|
|
2960
|
+
if (this.isWebUSB()) {
|
|
2961
|
+
if (this._isCDCDevice) {
|
|
2962
|
+
// CDC devices (CH343): Start with maximum, adaptive adjustment enabled
|
|
2963
|
+
this._adaptiveBlockMultiplier = 8; // blockSize = 248 bytes
|
|
2964
|
+
this._adaptiveMaxInFlightMultiplier = 8; // maxInFlight = 248 bytes
|
|
2965
|
+
this._consecutiveSuccessfulChunks = 0;
|
|
2966
|
+
this.logger.debug(
|
|
2967
|
+
`CDC device - Initialized: blockMultiplier=${this._adaptiveBlockMultiplier}, maxInFlightMultiplier=${this._adaptiveMaxInFlightMultiplier}`,
|
|
2968
|
+
);
|
|
2969
|
+
} else {
|
|
2970
|
+
// Non-CDC devices (CH340, CP2102): Fixed values, no adaptive adjustment
|
|
2971
|
+
this._adaptiveBlockMultiplier = 1; // blockSize = 31 bytes (fixed)
|
|
2972
|
+
this._adaptiveMaxInFlightMultiplier = 1; // maxInFlight = 31 bytes (fixed)
|
|
2973
|
+
this._consecutiveSuccessfulChunks = 0;
|
|
2974
|
+
this.logger.debug(
|
|
2975
|
+
`Non-CDC device - Fixed values: blockSize=31, maxInFlight=31`,
|
|
2976
|
+
);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
// Chunk size: Amount of data to request from ESP in one command
|
|
2981
|
+
// For WebUSB (Android), use smaller chunks to avoid timeouts and buffer issues
|
|
2982
|
+
// For Web Serial (Desktop), use larger chunks for better performance
|
|
2983
|
+
let CHUNK_SIZE: number;
|
|
2984
|
+
if (this.isWebUSB()) {
|
|
2985
|
+
// WebUSB: Use smaller chunks to avoid SLIP timeout issues
|
|
2986
|
+
CHUNK_SIZE = 0x4 * 0x1000; // 4KB = 16384 bytes
|
|
2987
|
+
} else {
|
|
2988
|
+
// Web Serial: Use larger chunks for better performance
|
|
2989
|
+
CHUNK_SIZE = 0x40 * 0x1000;
|
|
2990
|
+
}
|
|
2008
2991
|
|
|
2009
2992
|
let allData = new Uint8Array(0);
|
|
2010
2993
|
let currentAddr = addr;
|
|
@@ -2014,11 +2997,13 @@ export class ESPLoader extends EventTarget {
|
|
|
2014
2997
|
const chunkSize = Math.min(CHUNK_SIZE, remainingSize);
|
|
2015
2998
|
let chunkSuccess = false;
|
|
2016
2999
|
let retryCount = 0;
|
|
2017
|
-
const MAX_RETRIES =
|
|
3000
|
+
const MAX_RETRIES = 5;
|
|
3001
|
+
let deepRecoveryAttempted = false;
|
|
2018
3002
|
|
|
2019
3003
|
// Retry loop for this chunk
|
|
2020
3004
|
while (!chunkSuccess && retryCount <= MAX_RETRIES) {
|
|
2021
3005
|
let resp = new Uint8Array(0);
|
|
3006
|
+
let lastAckedLength = 0; // Track last acknowledged length
|
|
2022
3007
|
|
|
2023
3008
|
try {
|
|
2024
3009
|
// Only log on first attempt or retries
|
|
@@ -2028,9 +3013,34 @@ export class ESPLoader extends EventTarget {
|
|
|
2028
3013
|
);
|
|
2029
3014
|
}
|
|
2030
3015
|
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
3016
|
+
let blockSize: number;
|
|
3017
|
+
let maxInFlight: number;
|
|
3018
|
+
|
|
3019
|
+
if (this.isWebUSB()) {
|
|
3020
|
+
// WebUSB (Android): All devices use adaptive speed
|
|
3021
|
+
// All have maxTransferSize=64, baseBlockSize=31
|
|
3022
|
+
const maxTransferSize =
|
|
3023
|
+
(this.port as WebUSBSerialPort).maxTransferSize || 64;
|
|
3024
|
+
const baseBlockSize = Math.floor((maxTransferSize - 2) / 2); // 31 bytes
|
|
3025
|
+
|
|
3026
|
+
// Use current adaptive multipliers (initialized at start of readFlash)
|
|
3027
|
+
blockSize = baseBlockSize * this._adaptiveBlockMultiplier;
|
|
3028
|
+
maxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
|
|
3029
|
+
} else {
|
|
3030
|
+
// Web Serial (Desktop): Use multiples of 63 for consistency
|
|
3031
|
+
const base = 63;
|
|
3032
|
+
blockSize = base * 65; // 63 * 65 = 4095 (close to 0x1000)
|
|
3033
|
+
maxInFlight = base * 130; // 63 * 130 = 8190 (close to blockSize * 2)
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
const pkt = pack(
|
|
3037
|
+
"<IIII",
|
|
3038
|
+
currentAddr,
|
|
3039
|
+
chunkSize,
|
|
3040
|
+
blockSize,
|
|
3041
|
+
maxInFlight,
|
|
3042
|
+
);
|
|
3043
|
+
|
|
2034
3044
|
const [res] = await this.checkCommand(ESP_READ_FLASH, pkt);
|
|
2035
3045
|
|
|
2036
3046
|
if (res != 0) {
|
|
@@ -2082,10 +3092,22 @@ export class ESPLoader extends EventTarget {
|
|
|
2082
3092
|
newResp.set(packetData, resp.length);
|
|
2083
3093
|
resp = newResp;
|
|
2084
3094
|
|
|
2085
|
-
// Send acknowledgment
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
3095
|
+
// Send acknowledgment when we've received maxInFlight bytes
|
|
3096
|
+
// The stub sends packets until (num_sent - num_acked) >= max_in_flight
|
|
3097
|
+
// We MUST wait for all packets before sending ACK
|
|
3098
|
+
const shouldAck =
|
|
3099
|
+
resp.length >= chunkSize || // End of chunk
|
|
3100
|
+
resp.length >= lastAckedLength + maxInFlight; // Received all packets
|
|
3101
|
+
|
|
3102
|
+
if (shouldAck) {
|
|
3103
|
+
const ackData = pack("<I", resp.length);
|
|
3104
|
+
const slipEncodedAck = slipEncode(ackData);
|
|
3105
|
+
await this.writeToStream(slipEncodedAck);
|
|
3106
|
+
|
|
3107
|
+
// Update lastAckedLength to current response length
|
|
3108
|
+
// This ensures next ACK is sent at the right time
|
|
3109
|
+
lastAckedLength = resp.length;
|
|
3110
|
+
}
|
|
2089
3111
|
}
|
|
2090
3112
|
}
|
|
2091
3113
|
|
|
@@ -2096,9 +3118,93 @@ export class ESPLoader extends EventTarget {
|
|
|
2096
3118
|
allData = newAllData;
|
|
2097
3119
|
|
|
2098
3120
|
chunkSuccess = true;
|
|
3121
|
+
|
|
3122
|
+
// ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
|
|
3123
|
+
// Non-CDC devices (CH340, CP2102) stay at fixed blockSize=31, maxInFlight=31
|
|
3124
|
+
if (this.isWebUSB() && this._isCDCDevice && retryCount === 0) {
|
|
3125
|
+
this._consecutiveSuccessfulChunks++;
|
|
3126
|
+
|
|
3127
|
+
// After 2 consecutive successful chunks, increase speed gradually
|
|
3128
|
+
if (this._consecutiveSuccessfulChunks >= 2) {
|
|
3129
|
+
const maxTransferSize =
|
|
3130
|
+
(this.port as WebUSBSerialPort).maxTransferSize || 64;
|
|
3131
|
+
const baseBlockSize = Math.floor((maxTransferSize - 2) / 2); // 31 bytes
|
|
3132
|
+
|
|
3133
|
+
// Maximum: blockSize=248 (8 * 31), maxInFlight=248 (8 * 31)
|
|
3134
|
+
const MAX_BLOCK_MULTIPLIER = 8; // 248 bytes - tested stable
|
|
3135
|
+
const MAX_INFLIGHT_MULTIPLIER = 8; // 248 bytes - tested stable
|
|
3136
|
+
|
|
3137
|
+
let adjusted = false;
|
|
3138
|
+
|
|
3139
|
+
// Increase blockSize first (up to 248), then maxInFlight
|
|
3140
|
+
if (this._adaptiveBlockMultiplier < MAX_BLOCK_MULTIPLIER) {
|
|
3141
|
+
this._adaptiveBlockMultiplier = Math.min(
|
|
3142
|
+
this._adaptiveBlockMultiplier * 2,
|
|
3143
|
+
MAX_BLOCK_MULTIPLIER,
|
|
3144
|
+
);
|
|
3145
|
+
adjusted = true;
|
|
3146
|
+
}
|
|
3147
|
+
// Once blockSize is at maximum, increase maxInFlight
|
|
3148
|
+
else if (
|
|
3149
|
+
this._adaptiveMaxInFlightMultiplier < MAX_INFLIGHT_MULTIPLIER
|
|
3150
|
+
) {
|
|
3151
|
+
this._adaptiveMaxInFlightMultiplier = Math.min(
|
|
3152
|
+
this._adaptiveMaxInFlightMultiplier * 2,
|
|
3153
|
+
MAX_INFLIGHT_MULTIPLIER,
|
|
3154
|
+
);
|
|
3155
|
+
adjusted = true;
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
if (adjusted) {
|
|
3159
|
+
const newBlockSize =
|
|
3160
|
+
baseBlockSize * this._adaptiveBlockMultiplier;
|
|
3161
|
+
const newMaxInFlight =
|
|
3162
|
+
baseBlockSize * this._adaptiveMaxInFlightMultiplier;
|
|
3163
|
+
this.logger.debug(
|
|
3164
|
+
`Speed increased: blockSize=${newBlockSize}, maxInFlight=${newMaxInFlight}`,
|
|
3165
|
+
);
|
|
3166
|
+
this._lastAdaptiveAdjustment = Date.now();
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
// Reset counter
|
|
3170
|
+
this._consecutiveSuccessfulChunks = 0;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
2099
3173
|
} catch (err) {
|
|
2100
3174
|
retryCount++;
|
|
2101
3175
|
|
|
3176
|
+
// ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
|
|
3177
|
+
// Non-CDC devices stay at fixed values
|
|
3178
|
+
if (this.isWebUSB() && this._isCDCDevice && retryCount === 1) {
|
|
3179
|
+
// Only reduce if we're above minimum
|
|
3180
|
+
if (
|
|
3181
|
+
this._adaptiveBlockMultiplier > 1 ||
|
|
3182
|
+
this._adaptiveMaxInFlightMultiplier > 1
|
|
3183
|
+
) {
|
|
3184
|
+
// Reduce to minimum on error
|
|
3185
|
+
this._adaptiveBlockMultiplier = 1; // 31 bytes (for CH343)
|
|
3186
|
+
this._adaptiveMaxInFlightMultiplier = 1; // 31 bytes
|
|
3187
|
+
this._consecutiveSuccessfulChunks = 0; // Reset success counter
|
|
3188
|
+
|
|
3189
|
+
const maxTransferSize =
|
|
3190
|
+
(this.port as WebUSBSerialPort).maxTransferSize || 64;
|
|
3191
|
+
const baseBlockSize = Math.floor((maxTransferSize - 2) / 2);
|
|
3192
|
+
const newBlockSize =
|
|
3193
|
+
baseBlockSize * this._adaptiveBlockMultiplier;
|
|
3194
|
+
const newMaxInFlight =
|
|
3195
|
+
baseBlockSize * this._adaptiveMaxInFlightMultiplier;
|
|
3196
|
+
|
|
3197
|
+
this.logger.debug(
|
|
3198
|
+
`Error at higher speed - reduced to minimum: blockSize=${newBlockSize}, maxInFlight=${newMaxInFlight}`,
|
|
3199
|
+
);
|
|
3200
|
+
} else {
|
|
3201
|
+
// Already at minimum and still failing - this is a real error
|
|
3202
|
+
this.logger.debug(
|
|
3203
|
+
`Error at minimum speed (blockSize=31, maxInFlight=31) - not a speed issue`,
|
|
3204
|
+
);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
|
|
2102
3208
|
// Check if it's a timeout error or SLIP error
|
|
2103
3209
|
if (err instanceof SlipReadError) {
|
|
2104
3210
|
if (retryCount <= MAX_RETRIES) {
|
|
@@ -2120,9 +3226,37 @@ export class ESPLoader extends EventTarget {
|
|
|
2120
3226
|
this.logger.debug(`Buffer drain error: ${drainErr}`);
|
|
2121
3227
|
}
|
|
2122
3228
|
} else {
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
)
|
|
3229
|
+
// All retries exhausted - attempt recovery by reloading stub
|
|
3230
|
+
// IMPORTANT: Do NOT close port to keep ESP32 in bootloader mode
|
|
3231
|
+
if (!deepRecoveryAttempted) {
|
|
3232
|
+
deepRecoveryAttempted = true;
|
|
3233
|
+
|
|
3234
|
+
this.logger.log(
|
|
3235
|
+
`All retries exhausted at 0x${currentAddr.toString(16)}. Attempting recovery (close and reopen port)...`,
|
|
3236
|
+
);
|
|
3237
|
+
|
|
3238
|
+
try {
|
|
3239
|
+
// Reconnect will close port, reopen, and reload stub
|
|
3240
|
+
await this.reconnect();
|
|
3241
|
+
|
|
3242
|
+
this.logger.log(
|
|
3243
|
+
"Deep recovery successful. Resuming read from current position...",
|
|
3244
|
+
);
|
|
3245
|
+
|
|
3246
|
+
// Reset retry counter to give it another chance after recovery
|
|
3247
|
+
retryCount = 0;
|
|
3248
|
+
continue;
|
|
3249
|
+
} catch (recoveryErr) {
|
|
3250
|
+
throw new Error(
|
|
3251
|
+
`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery failed: ${recoveryErr}`,
|
|
3252
|
+
);
|
|
3253
|
+
}
|
|
3254
|
+
} else {
|
|
3255
|
+
// Recovery already attempted, give up
|
|
3256
|
+
throw new Error(
|
|
3257
|
+
`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery attempt`,
|
|
3258
|
+
);
|
|
3259
|
+
}
|
|
2126
3260
|
}
|
|
2127
3261
|
} else {
|
|
2128
3262
|
// Non-SLIP error, don't retry
|
|
@@ -2144,7 +3278,6 @@ export class ESPLoader extends EventTarget {
|
|
|
2144
3278
|
);
|
|
2145
3279
|
}
|
|
2146
3280
|
|
|
2147
|
-
this.logger.debug(`Successfully read ${allData.length} bytes from flash`);
|
|
2148
3281
|
return allData;
|
|
2149
3282
|
}
|
|
2150
3283
|
}
|
|
@@ -2205,10 +3338,61 @@ class EspStubLoader extends ESPLoader {
|
|
|
2205
3338
|
}
|
|
2206
3339
|
|
|
2207
3340
|
/**
|
|
2208
|
-
* @name
|
|
2209
|
-
*
|
|
3341
|
+
* @name eraseFlash
|
|
3342
|
+
* Erase entire flash chip
|
|
2210
3343
|
*/
|
|
2211
3344
|
async eraseFlash() {
|
|
2212
3345
|
await this.checkCommand(ESP_ERASE_FLASH, [], 0, CHIP_ERASE_TIMEOUT);
|
|
2213
3346
|
}
|
|
3347
|
+
|
|
3348
|
+
/**
|
|
3349
|
+
* @name eraseRegion
|
|
3350
|
+
* Erase a specific region of flash
|
|
3351
|
+
*/
|
|
3352
|
+
async eraseRegion(offset: number, size: number) {
|
|
3353
|
+
// Validate inputs
|
|
3354
|
+
if (offset < 0) {
|
|
3355
|
+
throw new Error(`Invalid offset: ${offset} (must be non-negative)`);
|
|
3356
|
+
}
|
|
3357
|
+
if (size < 0) {
|
|
3358
|
+
throw new Error(`Invalid size: ${size} (must be non-negative)`);
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
// No-op for zero size
|
|
3362
|
+
if (size === 0) {
|
|
3363
|
+
this.logger.log("eraseRegion: size is 0, skipping erase");
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
// Check for sector alignment
|
|
3368
|
+
if (offset % FLASH_SECTOR_SIZE !== 0) {
|
|
3369
|
+
throw new Error(
|
|
3370
|
+
`Offset ${offset} (0x${offset.toString(16)}) is not aligned to flash sector size ${FLASH_SECTOR_SIZE} (0x${FLASH_SECTOR_SIZE.toString(16)})`,
|
|
3371
|
+
);
|
|
3372
|
+
}
|
|
3373
|
+
if (size % FLASH_SECTOR_SIZE !== 0) {
|
|
3374
|
+
throw new Error(
|
|
3375
|
+
`Size ${size} (0x${size.toString(16)}) is not aligned to flash sector size ${FLASH_SECTOR_SIZE} (0x${FLASH_SECTOR_SIZE.toString(16)})`,
|
|
3376
|
+
);
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
// Check for reasonable bounds (prevent wrapping in pack)
|
|
3380
|
+
const maxValue = 0xffffffff; // 32-bit unsigned max
|
|
3381
|
+
if (offset > maxValue) {
|
|
3382
|
+
throw new Error(`Offset ${offset} exceeds maximum value ${maxValue}`);
|
|
3383
|
+
}
|
|
3384
|
+
if (size > maxValue) {
|
|
3385
|
+
throw new Error(`Size ${size} exceeds maximum value ${maxValue}`);
|
|
3386
|
+
}
|
|
3387
|
+
// Check for wrap-around
|
|
3388
|
+
if (offset + size > maxValue) {
|
|
3389
|
+
throw new Error(
|
|
3390
|
+
`Region end (offset + size = ${offset + size}) exceeds maximum addressable range ${maxValue}`,
|
|
3391
|
+
);
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
const timeout = timeoutPerMb(ERASE_REGION_TIMEOUT_PER_MB, size);
|
|
3395
|
+
const buffer = pack("<II", offset, size);
|
|
3396
|
+
await this.checkCommand(ESP_ERASE_REGION, buffer, 0, timeout);
|
|
3397
|
+
}
|
|
2214
3398
|
}
|