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