tasmota-webserial-esptool 7.3.3 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/css/dark.css +74 -0
- package/css/light.css +74 -0
- package/css/style.css +663 -35
- package/dist/esp_loader.d.ts +102 -14
- package/dist/esp_loader.js +1015 -186
- package/dist/index.d.ts +1 -0
- package/dist/index.js +31 -2
- package/dist/stubs/index.d.ts +1 -2
- package/dist/stubs/index.js +4 -0
- package/dist/web/index.js +1 -1
- package/js/modules/esptool.js +1 -1
- package/js/script.js +198 -85
- package/js/webusb-serial.js +1028 -0
- package/package.json +2 -2
- package/src/esp_loader.ts +1184 -216
- package/src/index.ts +38 -2
- package/src/stubs/index.ts +4 -1
package/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,71 +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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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)");
|
|
1190
|
+
}
|
|
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) {
|
|
1198
|
+
await sleep(1);
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
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
|
+
}
|
|
539
1218
|
}
|
|
540
|
-
else {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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) + ")");
|
|
545
1235
|
}
|
|
546
|
-
throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
|
|
547
1236
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
inEscape = false;
|
|
552
|
-
if (b == 0xdc) {
|
|
553
|
-
partialPacket.push(0xc0);
|
|
1237
|
+
else if (b == 0xdb) {
|
|
1238
|
+
// start of escape sequence
|
|
1239
|
+
inEscape = true;
|
|
554
1240
|
}
|
|
555
|
-
else if (b ==
|
|
556
|
-
|
|
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;
|
|
557
1248
|
}
|
|
558
1249
|
else {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
this.logger.debug("Remaining data in serial buffer: " +
|
|
562
|
-
hexFormatter(this._inputBuffer));
|
|
563
|
-
}
|
|
564
|
-
throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
|
|
1250
|
+
// normal byte in packet
|
|
1251
|
+
partialPacket.push(b);
|
|
565
1252
|
}
|
|
566
1253
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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)");
|
|
1263
|
+
}
|
|
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
|
+
}
|
|
570
1275
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
|
|
575
|
-
return partialPacket;
|
|
1276
|
+
if (readBytes.length == 0) {
|
|
1277
|
+
const waitingFor = partialPacket === null ? "header" : "content";
|
|
1278
|
+
throw new SlipReadError("Timed out waiting for packet " + waitingFor);
|
|
576
1279
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
+
}
|
|
580
1331
|
}
|
|
581
1332
|
}
|
|
582
1333
|
}
|
|
@@ -608,7 +1359,7 @@ export class ESPLoader extends EventTarget {
|
|
|
608
1359
|
throw new Error(`Invalid (unsupported) command ${toHex(opcode)}`);
|
|
609
1360
|
}
|
|
610
1361
|
}
|
|
611
|
-
throw "Response doesn't match request";
|
|
1362
|
+
throw new Error("Response doesn't match request");
|
|
612
1363
|
}
|
|
613
1364
|
/**
|
|
614
1365
|
* @name checksum
|
|
@@ -660,6 +1411,8 @@ export class ESPLoader extends EventTarget {
|
|
|
660
1411
|
}
|
|
661
1412
|
async reconfigurePort(baud) {
|
|
662
1413
|
var _a;
|
|
1414
|
+
// Block new writes during the entire reconfiguration (all paths)
|
|
1415
|
+
this._isReconfiguring = true;
|
|
663
1416
|
try {
|
|
664
1417
|
// Wait for pending writes to complete
|
|
665
1418
|
try {
|
|
@@ -668,8 +1421,30 @@ export class ESPLoader extends EventTarget {
|
|
|
668
1421
|
catch (err) {
|
|
669
1422
|
this.logger.debug(`Pending write error during reconfigure: ${err}`);
|
|
670
1423
|
}
|
|
671
|
-
//
|
|
672
|
-
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
|
|
673
1448
|
// Release persistent writer before closing
|
|
674
1449
|
if (this._writer) {
|
|
675
1450
|
try {
|
|
@@ -688,120 +1463,54 @@ export class ESPLoader extends EventTarget {
|
|
|
688
1463
|
await this.port.close();
|
|
689
1464
|
// Reopen Port
|
|
690
1465
|
await this.port.open({ baudRate: baud });
|
|
691
|
-
// Port is now open - allow writes again
|
|
692
|
-
this._isReconfiguring = false;
|
|
693
1466
|
// Clear buffer again
|
|
694
1467
|
await this.flushSerialBuffers();
|
|
695
1468
|
// Restart Readloop
|
|
696
1469
|
this.readLoop();
|
|
697
1470
|
}
|
|
698
1471
|
catch (e) {
|
|
699
|
-
this._isReconfiguring = false;
|
|
700
1472
|
this.logger.error(`Reconfigure port error: ${e}`);
|
|
701
1473
|
throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
|
|
702
1474
|
}
|
|
1475
|
+
finally {
|
|
1476
|
+
// Always reset flag, even on error or early return
|
|
1477
|
+
this._isReconfiguring = false;
|
|
1478
|
+
}
|
|
703
1479
|
}
|
|
704
1480
|
/**
|
|
705
|
-
* @name
|
|
706
|
-
*
|
|
707
|
-
*
|
|
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
|
|
708
1484
|
*/
|
|
709
|
-
async
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
name: "USB-JTAG/Serial",
|
|
722
|
-
fn: async () => await this.hardResetUSBJTAGSerial(),
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
// Strategy 2: Classic reset (for USB-to-Serial bridges)
|
|
726
|
-
resetStrategies.push({
|
|
727
|
-
name: "Classic",
|
|
728
|
-
fn: async () => await this.hardResetClassic(),
|
|
729
|
-
});
|
|
730
|
-
// Strategy 3: If USB-JTAG/Serial was not tried yet, try it as fallback
|
|
731
|
-
if (!isUSBJTAGSerial && !isEspressifUSB) {
|
|
732
|
-
resetStrategies.push({
|
|
733
|
-
name: "USB-JTAG/Serial (fallback)",
|
|
734
|
-
fn: async () => await this.hardResetUSBJTAGSerial(),
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
let lastError = null;
|
|
738
|
-
// Try each reset strategy
|
|
739
|
-
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();
|
|
740
1497
|
try {
|
|
741
|
-
this.
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
continue;
|
|
1498
|
+
const response = await this._sync();
|
|
1499
|
+
if (response) {
|
|
1500
|
+
await sleep(SYNC_TIMEOUT);
|
|
1501
|
+
return true;
|
|
746
1502
|
}
|
|
747
|
-
|
|
748
|
-
// Try to sync after reset
|
|
749
|
-
await this.sync();
|
|
750
|
-
// If we get here, sync succeeded
|
|
751
|
-
this.logger.log(`Connected successfully with ${strategy.name} reset.`);
|
|
752
|
-
return;
|
|
1503
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
753
1504
|
}
|
|
754
|
-
catch (
|
|
755
|
-
|
|
756
|
-
this.
|
|
757
|
-
|
|
758
|
-
if (!this.connected || !this.port.writable) {
|
|
759
|
-
this.logger.log(`Port disconnected during reset attempt`);
|
|
760
|
-
break;
|
|
1505
|
+
catch (e) {
|
|
1506
|
+
// Check abandon flag after error
|
|
1507
|
+
if (this._abandonCurrentOperation) {
|
|
1508
|
+
return false;
|
|
761
1509
|
}
|
|
762
|
-
// Clear buffers before trying next strategy
|
|
763
|
-
this._inputBuffer.length = 0;
|
|
764
|
-
await this.drainInputBuffer(200);
|
|
765
|
-
await this.flushSerialBuffers();
|
|
766
1510
|
}
|
|
1511
|
+
await sleep(SYNC_TIMEOUT);
|
|
767
1512
|
}
|
|
768
|
-
|
|
769
|
-
throw new Error(`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
|
|
770
|
-
}
|
|
771
|
-
/**
|
|
772
|
-
* @name hardResetUSBJTAGSerial
|
|
773
|
-
* USB-JTAG/Serial reset sequence for ESP32-C3, ESP32-S3, ESP32-C6, etc.
|
|
774
|
-
*/
|
|
775
|
-
async hardResetUSBJTAGSerial() {
|
|
776
|
-
await this.setRTS(false);
|
|
777
|
-
await this.setDTR(false); // Idle
|
|
778
|
-
await this.sleep(100);
|
|
779
|
-
await this.setDTR(true); // Set IO0
|
|
780
|
-
await this.setRTS(false);
|
|
781
|
-
await this.sleep(100);
|
|
782
|
-
await this.setRTS(true); // Reset. Calls inverted to go through (1,1) instead of (0,0)
|
|
783
|
-
await this.setDTR(false);
|
|
784
|
-
await this.setRTS(true); // RTS set as Windows only propagates DTR on RTS setting
|
|
785
|
-
await this.sleep(100);
|
|
786
|
-
await this.setDTR(false);
|
|
787
|
-
await this.setRTS(false); // Chip out of reset
|
|
788
|
-
// Wait for chip to boot into bootloader
|
|
789
|
-
await this.sleep(200);
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* @name hardResetClassic
|
|
793
|
-
* Classic reset sequence for USB-to-Serial bridge chips (CH340, CP2102, etc.)
|
|
794
|
-
*/
|
|
795
|
-
async hardResetClassic() {
|
|
796
|
-
await this.setDTR(false); // IO0=HIGH
|
|
797
|
-
await this.setRTS(true); // EN=LOW, chip in reset
|
|
798
|
-
await this.sleep(100);
|
|
799
|
-
await this.setDTR(true); // IO0=LOW
|
|
800
|
-
await this.setRTS(false); // EN=HIGH, chip out of reset
|
|
801
|
-
await this.sleep(50);
|
|
802
|
-
await this.setDTR(false); // IO0=HIGH, done
|
|
803
|
-
// Wait for chip to boot into bootloader
|
|
804
|
-
await this.sleep(200);
|
|
1513
|
+
return false;
|
|
805
1514
|
}
|
|
806
1515
|
/**
|
|
807
1516
|
* @name sync
|
|
@@ -810,7 +1519,7 @@ export class ESPLoader extends EventTarget {
|
|
|
810
1519
|
*/
|
|
811
1520
|
async sync() {
|
|
812
1521
|
for (let i = 0; i < 5; i++) {
|
|
813
|
-
this.
|
|
1522
|
+
this._clearInputBuffer();
|
|
814
1523
|
const response = await this._sync();
|
|
815
1524
|
if (response) {
|
|
816
1525
|
await sleep(SYNC_TIMEOUT);
|
|
@@ -834,8 +1543,10 @@ export class ESPLoader extends EventTarget {
|
|
|
834
1543
|
return true;
|
|
835
1544
|
}
|
|
836
1545
|
}
|
|
837
|
-
catch {
|
|
838
|
-
|
|
1546
|
+
catch (e) {
|
|
1547
|
+
if (this.debug) {
|
|
1548
|
+
this.logger.debug(`Sync attempt ${i + 1} failed: ${e}`);
|
|
1549
|
+
}
|
|
839
1550
|
}
|
|
840
1551
|
}
|
|
841
1552
|
return false;
|
|
@@ -1310,12 +2021,19 @@ export class ESPLoader extends EventTarget {
|
|
|
1310
2021
|
await this._writer.write(new Uint8Array(data));
|
|
1311
2022
|
}, async () => {
|
|
1312
2023
|
// Previous write failed, but still attempt this write
|
|
2024
|
+
this.logger.debug("Previous write failed, attempting recovery for current write");
|
|
1313
2025
|
if (!this.port.writable) {
|
|
1314
2026
|
throw new Error("Port became unavailable during write");
|
|
1315
2027
|
}
|
|
1316
2028
|
// Writer was likely cleaned up by previous error, create new one
|
|
1317
2029
|
if (!this._writer) {
|
|
1318
|
-
|
|
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
|
+
}
|
|
1319
2037
|
}
|
|
1320
2038
|
await this._writer.write(new Uint8Array(data));
|
|
1321
2039
|
})
|
|
@@ -1326,7 +2044,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1326
2044
|
try {
|
|
1327
2045
|
this._writer.releaseLock();
|
|
1328
2046
|
}
|
|
1329
|
-
catch
|
|
2047
|
+
catch {
|
|
1330
2048
|
// Ignore release errors
|
|
1331
2049
|
}
|
|
1332
2050
|
this._writer = undefined;
|
|
@@ -1382,6 +2100,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1382
2100
|
await new Promise((resolve) => {
|
|
1383
2101
|
if (!this._reader) {
|
|
1384
2102
|
resolve(undefined);
|
|
2103
|
+
return;
|
|
1385
2104
|
}
|
|
1386
2105
|
this.addEventListener("disconnect", resolve, { once: true });
|
|
1387
2106
|
this._reader.cancel();
|
|
@@ -1405,6 +2124,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1405
2124
|
this.logger.log("Reconnecting serial port...");
|
|
1406
2125
|
this.connected = false;
|
|
1407
2126
|
this.__inputBuffer = [];
|
|
2127
|
+
this.__inputBufferReadIndex = 0;
|
|
1408
2128
|
// Wait for pending writes to complete
|
|
1409
2129
|
try {
|
|
1410
2130
|
await this._writeChain;
|
|
@@ -1467,6 +2187,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1467
2187
|
await this.hardReset(true);
|
|
1468
2188
|
if (!this._parent) {
|
|
1469
2189
|
this.__inputBuffer = [];
|
|
2190
|
+
this.__inputBufferReadIndex = 0;
|
|
1470
2191
|
this.__totalBytesRead = 0;
|
|
1471
2192
|
this.readLoop();
|
|
1472
2193
|
}
|
|
@@ -1494,10 +2215,10 @@ export class ESPLoader extends EventTarget {
|
|
|
1494
2215
|
throw new Error(`Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
|
|
1495
2216
|
}
|
|
1496
2217
|
}
|
|
1497
|
-
//
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
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;
|
|
1501
2222
|
this.logger.debug("Reconnection successful");
|
|
1502
2223
|
}
|
|
1503
2224
|
catch (err) {
|
|
@@ -1529,8 +2250,8 @@ export class ESPLoader extends EventTarget {
|
|
|
1529
2250
|
const drainStart = Date.now();
|
|
1530
2251
|
const drainTimeout = 100; // Short timeout for draining
|
|
1531
2252
|
while (drained < bytesToDrain && Date.now() - drainStart < drainTimeout) {
|
|
1532
|
-
if (this.
|
|
1533
|
-
const byte = this.
|
|
2253
|
+
if (this._inputBufferAvailable > 0) {
|
|
2254
|
+
const byte = this._readByte();
|
|
1534
2255
|
if (byte !== undefined) {
|
|
1535
2256
|
drained++;
|
|
1536
2257
|
}
|
|
@@ -1546,6 +2267,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1546
2267
|
// Final clear of application buffer
|
|
1547
2268
|
if (!this._parent) {
|
|
1548
2269
|
this.__inputBuffer = [];
|
|
2270
|
+
this.__inputBufferReadIndex = 0;
|
|
1549
2271
|
}
|
|
1550
2272
|
}
|
|
1551
2273
|
/**
|
|
@@ -1557,12 +2279,14 @@ export class ESPLoader extends EventTarget {
|
|
|
1557
2279
|
// Clear application buffer
|
|
1558
2280
|
if (!this._parent) {
|
|
1559
2281
|
this.__inputBuffer = [];
|
|
2282
|
+
this.__inputBufferReadIndex = 0;
|
|
1560
2283
|
}
|
|
1561
2284
|
// Wait for any pending data
|
|
1562
2285
|
await sleep(SYNC_TIMEOUT);
|
|
1563
2286
|
// Final clear
|
|
1564
2287
|
if (!this._parent) {
|
|
1565
2288
|
this.__inputBuffer = [];
|
|
2289
|
+
this.__inputBufferReadIndex = 0;
|
|
1566
2290
|
}
|
|
1567
2291
|
this.logger.debug("Serial buffers flushed");
|
|
1568
2292
|
}
|
|
@@ -1581,7 +2305,35 @@ export class ESPLoader extends EventTarget {
|
|
|
1581
2305
|
// Flush serial buffers before flash read operation
|
|
1582
2306
|
await this.flushSerialBuffers();
|
|
1583
2307
|
this.logger.log(`Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`);
|
|
1584
|
-
|
|
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
|
+
}
|
|
1585
2337
|
let allData = new Uint8Array(0);
|
|
1586
2338
|
let currentAddr = addr;
|
|
1587
2339
|
let remainingSize = size;
|
|
@@ -1594,14 +2346,30 @@ export class ESPLoader extends EventTarget {
|
|
|
1594
2346
|
// Retry loop for this chunk
|
|
1595
2347
|
while (!chunkSuccess && retryCount <= MAX_RETRIES) {
|
|
1596
2348
|
let resp = new Uint8Array(0);
|
|
2349
|
+
let lastAckedLength = 0; // Track last acknowledged length
|
|
1597
2350
|
try {
|
|
1598
2351
|
// Only log on first attempt or retries
|
|
1599
2352
|
if (retryCount === 0) {
|
|
1600
2353
|
this.logger.debug(`Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`);
|
|
1601
2354
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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);
|
|
1605
2373
|
const [res] = await this.checkCommand(ESP_READ_FLASH, pkt);
|
|
1606
2374
|
if (res != 0) {
|
|
1607
2375
|
throw new Error("Failed to read memory: " + res);
|
|
@@ -1644,10 +2412,18 @@ export class ESPLoader extends EventTarget {
|
|
|
1644
2412
|
newResp.set(resp);
|
|
1645
2413
|
newResp.set(packetData, resp.length);
|
|
1646
2414
|
resp = newResp;
|
|
1647
|
-
// Send acknowledgment
|
|
1648
|
-
|
|
1649
|
-
const
|
|
1650
|
-
|
|
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
|
+
}
|
|
1651
2427
|
}
|
|
1652
2428
|
}
|
|
1653
2429
|
// Chunk read successfully - append to all data
|
|
@@ -1656,9 +2432,62 @@ export class ESPLoader extends EventTarget {
|
|
|
1656
2432
|
newAllData.set(resp, allData.length);
|
|
1657
2433
|
allData = newAllData;
|
|
1658
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
|
+
}
|
|
1659
2467
|
}
|
|
1660
2468
|
catch (err) {
|
|
1661
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
|
+
}
|
|
1662
2491
|
// Check if it's a timeout error or SLIP error
|
|
1663
2492
|
if (err instanceof SlipReadError) {
|
|
1664
2493
|
if (retryCount <= MAX_RETRIES) {
|
|
@@ -1676,10 +2505,11 @@ export class ESPLoader extends EventTarget {
|
|
|
1676
2505
|
}
|
|
1677
2506
|
}
|
|
1678
2507
|
else {
|
|
1679
|
-
// 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
|
|
1680
2510
|
if (!deepRecoveryAttempted) {
|
|
1681
2511
|
deepRecoveryAttempted = true;
|
|
1682
|
-
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)...`);
|
|
1683
2513
|
try {
|
|
1684
2514
|
// Reconnect will close port, reopen, and reload stub
|
|
1685
2515
|
await this.reconnect();
|
|
@@ -1688,13 +2518,13 @@ export class ESPLoader extends EventTarget {
|
|
|
1688
2518
|
retryCount = 0;
|
|
1689
2519
|
continue;
|
|
1690
2520
|
}
|
|
1691
|
-
catch (
|
|
1692
|
-
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}`);
|
|
1693
2523
|
}
|
|
1694
2524
|
}
|
|
1695
2525
|
else {
|
|
1696
|
-
//
|
|
1697
|
-
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`);
|
|
1698
2528
|
}
|
|
1699
2529
|
}
|
|
1700
2530
|
}
|
|
@@ -1712,7 +2542,6 @@ export class ESPLoader extends EventTarget {
|
|
|
1712
2542
|
remainingSize -= chunkSize;
|
|
1713
2543
|
this.logger.debug(`Total progress: 0x${allData.length.toString(16)} from 0x${size.toString(16)} bytes`);
|
|
1714
2544
|
}
|
|
1715
|
-
this.logger.debug(`Successfully read ${allData.length} bytes from flash`);
|
|
1716
2545
|
return allData;
|
|
1717
2546
|
}
|
|
1718
2547
|
}
|