esp32tool 1.1.8 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nojekyll +0 -0
- package/README.md +100 -6
- package/apple-touch-icon.png +0 -0
- package/build-electron-cli.cjs +177 -0
- package/build-single-binary.cjs +295 -0
- package/css/light.css +11 -0
- package/css/style.css +225 -35
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +458 -0
- package/dist/esp_loader.d.ts +129 -21
- package/dist/esp_loader.js +1227 -222
- package/dist/index.d.ts +2 -1
- package/dist/index.js +37 -4
- package/dist/node-usb-adapter.d.ts +47 -0
- package/dist/node-usb-adapter.js +725 -0
- package/dist/stubs/index.d.ts +1 -2
- package/dist/stubs/index.js +4 -0
- package/dist/web/index.js +1 -1
- package/electron/cli-main.cjs +74 -0
- package/electron/main.cjs +338 -0
- package/electron/main.js +7 -2
- package/favicon.ico +0 -0
- package/fix-cli-imports.cjs +127 -0
- package/generate-icons.sh +89 -0
- package/icons/icon-128.png +0 -0
- package/icons/icon-144.png +0 -0
- package/icons/icon-152.png +0 -0
- package/icons/icon-192.png +0 -0
- package/icons/icon-384.png +0 -0
- package/icons/icon-512.png +0 -0
- package/icons/icon-72.png +0 -0
- package/icons/icon-96.png +0 -0
- package/index.html +94 -64
- package/install-android.html +411 -0
- package/js/modules/esptool.js +1 -1
- package/js/script.js +165 -160
- package/js/webusb-serial.js +1017 -0
- package/license.md +1 -1
- package/manifest.json +89 -0
- package/package.cli.json +29 -0
- package/package.json +31 -21
- package/screenshots/desktop.png +0 -0
- package/screenshots/mobile.png +0 -0
- package/src/cli.ts +618 -0
- package/src/esp_loader.ts +1438 -254
- package/src/index.ts +69 -3
- package/src/node-usb-adapter.ts +924 -0
- package/src/stubs/index.ts +4 -1
- package/sw.js +155 -0
package/dist/esp_loader.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/// <reference types="@types/w3c-web-serial" />
|
|
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";
|
|
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_ERASE_REGION, 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) {
|
|
@@ -11,9 +10,9 @@ export class ESPLoader extends EventTarget {
|
|
|
11
10
|
this.port = port;
|
|
12
11
|
this.logger = logger;
|
|
13
12
|
this._parent = _parent;
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
13
|
+
this.__chipName = null;
|
|
14
|
+
this.__chipRevision = null;
|
|
15
|
+
this.__chipVariant = null;
|
|
17
16
|
this._efuses = new Array(4).fill(0);
|
|
18
17
|
this._flashsize = 4 * 1024 * 1024;
|
|
19
18
|
this.debug = false;
|
|
@@ -24,12 +23,110 @@ export class ESPLoader extends EventTarget {
|
|
|
24
23
|
this._isESP32S2NativeUSB = false;
|
|
25
24
|
this._initializationSucceeded = false;
|
|
26
25
|
this.__commandLock = Promise.resolve([0, []]);
|
|
27
|
-
this.
|
|
26
|
+
this.__isReconfiguring = false;
|
|
27
|
+
this.__abandonCurrentOperation = false;
|
|
28
|
+
// Adaptive speed adjustment for flash read operations
|
|
29
|
+
this.__adaptiveBlockMultiplier = 1;
|
|
30
|
+
this.__adaptiveMaxInFlightMultiplier = 1;
|
|
31
|
+
this.__consecutiveSuccessfulChunks = 0;
|
|
32
|
+
this.__lastAdaptiveAdjustment = 0;
|
|
33
|
+
this.__isCDCDevice = false;
|
|
28
34
|
this.state_DTR = false;
|
|
35
|
+
this.state_RTS = false;
|
|
29
36
|
this.__writeChain = Promise.resolve();
|
|
30
37
|
}
|
|
38
|
+
// Chip properties with parent delegation
|
|
39
|
+
// chipFamily accessed before initialization as designed
|
|
40
|
+
get chipFamily() {
|
|
41
|
+
return this._parent ? this._parent.chipFamily : this.__chipFamily;
|
|
42
|
+
}
|
|
43
|
+
set chipFamily(value) {
|
|
44
|
+
if (this._parent) {
|
|
45
|
+
this._parent.chipFamily = value;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
this.__chipFamily = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
get chipName() {
|
|
52
|
+
return this._parent ? this._parent.chipName : this.__chipName;
|
|
53
|
+
}
|
|
54
|
+
set chipName(value) {
|
|
55
|
+
if (this._parent) {
|
|
56
|
+
this._parent.chipName = value;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
this.__chipName = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
get chipRevision() {
|
|
63
|
+
return this._parent ? this._parent.chipRevision : this.__chipRevision;
|
|
64
|
+
}
|
|
65
|
+
set chipRevision(value) {
|
|
66
|
+
if (this._parent) {
|
|
67
|
+
this._parent.chipRevision = value;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.__chipRevision = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
get chipVariant() {
|
|
74
|
+
return this._parent ? this._parent.chipVariant : this.__chipVariant;
|
|
75
|
+
}
|
|
76
|
+
set chipVariant(value) {
|
|
77
|
+
if (this._parent) {
|
|
78
|
+
this._parent.chipVariant = value;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.__chipVariant = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
31
84
|
get _inputBuffer() {
|
|
32
|
-
|
|
85
|
+
if (this._parent) {
|
|
86
|
+
return this._parent._inputBuffer;
|
|
87
|
+
}
|
|
88
|
+
if (this.__inputBuffer === undefined) {
|
|
89
|
+
throw new Error("_inputBuffer accessed before initialization");
|
|
90
|
+
}
|
|
91
|
+
return this.__inputBuffer;
|
|
92
|
+
}
|
|
93
|
+
get _inputBufferReadIndex() {
|
|
94
|
+
return this._parent
|
|
95
|
+
? this._parent._inputBufferReadIndex
|
|
96
|
+
: this.__inputBufferReadIndex || 0;
|
|
97
|
+
}
|
|
98
|
+
set _inputBufferReadIndex(value) {
|
|
99
|
+
if (this._parent) {
|
|
100
|
+
this._parent._inputBufferReadIndex = value;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
this.__inputBufferReadIndex = value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Get available bytes in buffer (from read index to end)
|
|
107
|
+
get _inputBufferAvailable() {
|
|
108
|
+
return this._inputBuffer.length - this._inputBufferReadIndex;
|
|
109
|
+
}
|
|
110
|
+
// Read one byte from buffer (ring-buffer style with index pointer)
|
|
111
|
+
_readByte() {
|
|
112
|
+
if (this._inputBufferReadIndex >= this._inputBuffer.length) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return this._inputBuffer[this._inputBufferReadIndex++];
|
|
116
|
+
}
|
|
117
|
+
// Clear input buffer and reset read index
|
|
118
|
+
_clearInputBuffer() {
|
|
119
|
+
this._inputBuffer.length = 0;
|
|
120
|
+
this._inputBufferReadIndex = 0;
|
|
121
|
+
}
|
|
122
|
+
// Compact buffer when read index gets too large (prevent memory growth)
|
|
123
|
+
_compactInputBuffer() {
|
|
124
|
+
if (this._inputBufferReadIndex > 1000 &&
|
|
125
|
+
this._inputBufferReadIndex > this._inputBuffer.length / 2) {
|
|
126
|
+
// Remove already-read bytes and reset index
|
|
127
|
+
this._inputBuffer.splice(0, this._inputBufferReadIndex);
|
|
128
|
+
this._inputBufferReadIndex = 0;
|
|
129
|
+
}
|
|
33
130
|
}
|
|
34
131
|
get _totalBytesRead() {
|
|
35
132
|
return this._parent
|
|
@@ -55,6 +152,95 @@ export class ESPLoader extends EventTarget {
|
|
|
55
152
|
this.__commandLock = value;
|
|
56
153
|
}
|
|
57
154
|
}
|
|
155
|
+
get _isReconfiguring() {
|
|
156
|
+
return this._parent
|
|
157
|
+
? this._parent._isReconfiguring
|
|
158
|
+
: this.__isReconfiguring;
|
|
159
|
+
}
|
|
160
|
+
set _isReconfiguring(value) {
|
|
161
|
+
if (this._parent) {
|
|
162
|
+
this._parent._isReconfiguring = value;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
this.__isReconfiguring = value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
get _abandonCurrentOperation() {
|
|
169
|
+
return this._parent
|
|
170
|
+
? this._parent._abandonCurrentOperation
|
|
171
|
+
: this.__abandonCurrentOperation;
|
|
172
|
+
}
|
|
173
|
+
set _abandonCurrentOperation(value) {
|
|
174
|
+
if (this._parent) {
|
|
175
|
+
this._parent._abandonCurrentOperation = value;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
this.__abandonCurrentOperation = value;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
get _adaptiveBlockMultiplier() {
|
|
182
|
+
return this._parent
|
|
183
|
+
? this._parent._adaptiveBlockMultiplier
|
|
184
|
+
: this.__adaptiveBlockMultiplier;
|
|
185
|
+
}
|
|
186
|
+
set _adaptiveBlockMultiplier(value) {
|
|
187
|
+
if (this._parent) {
|
|
188
|
+
this._parent._adaptiveBlockMultiplier = value;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
this.__adaptiveBlockMultiplier = value;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
get _adaptiveMaxInFlightMultiplier() {
|
|
195
|
+
return this._parent
|
|
196
|
+
? this._parent._adaptiveMaxInFlightMultiplier
|
|
197
|
+
: this.__adaptiveMaxInFlightMultiplier;
|
|
198
|
+
}
|
|
199
|
+
set _adaptiveMaxInFlightMultiplier(value) {
|
|
200
|
+
if (this._parent) {
|
|
201
|
+
this._parent._adaptiveMaxInFlightMultiplier = value;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
this.__adaptiveMaxInFlightMultiplier = value;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
get _consecutiveSuccessfulChunks() {
|
|
208
|
+
return this._parent
|
|
209
|
+
? this._parent._consecutiveSuccessfulChunks
|
|
210
|
+
: this.__consecutiveSuccessfulChunks;
|
|
211
|
+
}
|
|
212
|
+
set _consecutiveSuccessfulChunks(value) {
|
|
213
|
+
if (this._parent) {
|
|
214
|
+
this._parent._consecutiveSuccessfulChunks = value;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
this.__consecutiveSuccessfulChunks = value;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
get _lastAdaptiveAdjustment() {
|
|
221
|
+
return this._parent
|
|
222
|
+
? this._parent._lastAdaptiveAdjustment
|
|
223
|
+
: this.__lastAdaptiveAdjustment;
|
|
224
|
+
}
|
|
225
|
+
set _lastAdaptiveAdjustment(value) {
|
|
226
|
+
if (this._parent) {
|
|
227
|
+
this._parent._lastAdaptiveAdjustment = value;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
this.__lastAdaptiveAdjustment = value;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
get _isCDCDevice() {
|
|
234
|
+
return this._parent ? this._parent._isCDCDevice : this.__isCDCDevice;
|
|
235
|
+
}
|
|
236
|
+
set _isCDCDevice(value) {
|
|
237
|
+
if (this._parent) {
|
|
238
|
+
this._parent._isCDCDevice = value;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
this.__isCDCDevice = value;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
58
244
|
detectUSBSerialChip(vendorId, productId) {
|
|
59
245
|
// Common USB-Serial chip vendors and their products
|
|
60
246
|
const chips = {
|
|
@@ -102,6 +288,7 @@ export class ESPLoader extends EventTarget {
|
|
|
102
288
|
async initialize() {
|
|
103
289
|
if (!this._parent) {
|
|
104
290
|
this.__inputBuffer = [];
|
|
291
|
+
this.__inputBufferReadIndex = 0;
|
|
105
292
|
this.__totalBytesRead = 0;
|
|
106
293
|
// Detect and log USB-Serial chip info
|
|
107
294
|
const portInfo = this.port.getInfo();
|
|
@@ -116,6 +303,12 @@ export class ESPLoader extends EventTarget {
|
|
|
116
303
|
if (portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x2) {
|
|
117
304
|
this._isESP32S2NativeUSB = true;
|
|
118
305
|
}
|
|
306
|
+
// Detect CDC devices for adaptive speed adjustment
|
|
307
|
+
// Espressif Native USB (VID: 0x303a) or CH343 (VID: 0x1a86, PID: 0x55d3)
|
|
308
|
+
if (portInfo.usbVendorId === 0x303a ||
|
|
309
|
+
(portInfo.usbVendorId === 0x1a86 && portInfo.usbProductId === 0x55d3)) {
|
|
310
|
+
this._isCDCDevice = true;
|
|
311
|
+
}
|
|
119
312
|
}
|
|
120
313
|
// Don't await this promise so it doesn't block rest of method.
|
|
121
314
|
this.readLoop();
|
|
@@ -172,7 +365,7 @@ export class ESPLoader extends EventTarget {
|
|
|
172
365
|
// This ensures all error responses are cleared before continuing
|
|
173
366
|
await this.drainInputBuffer(200);
|
|
174
367
|
// Clear input buffer and re-sync to recover from failed command
|
|
175
|
-
this.
|
|
368
|
+
this._clearInputBuffer();
|
|
176
369
|
await sleep(SYNC_TIMEOUT);
|
|
177
370
|
// Re-sync with the chip to ensure clean communication
|
|
178
371
|
try {
|
|
@@ -249,6 +442,18 @@ export class ESPLoader extends EventTarget {
|
|
|
249
442
|
apiVersion,
|
|
250
443
|
};
|
|
251
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* Get MAC address from efuses
|
|
447
|
+
*/
|
|
448
|
+
async getMacAddress() {
|
|
449
|
+
if (!this._initializationSucceeded) {
|
|
450
|
+
throw new Error("getMacAddress() requires initialize() to have completed successfully");
|
|
451
|
+
}
|
|
452
|
+
const macBytes = this.macAddr(); // chip-family-aware
|
|
453
|
+
return macBytes
|
|
454
|
+
.map((b) => b.toString(16).padStart(2, "0").toUpperCase())
|
|
455
|
+
.join(":");
|
|
456
|
+
}
|
|
252
457
|
/**
|
|
253
458
|
* @name readLoop
|
|
254
459
|
* Reads data from the input stream and places it in the inputBuffer
|
|
@@ -281,6 +486,12 @@ export class ESPLoader extends EventTarget {
|
|
|
281
486
|
catch {
|
|
282
487
|
this.logger.error("Read loop got disconnected");
|
|
283
488
|
}
|
|
489
|
+
finally {
|
|
490
|
+
// Always reset reconfiguring flag when read loop ends
|
|
491
|
+
// This prevents "Cannot write during port reconfiguration" errors
|
|
492
|
+
// when the read loop dies unexpectedly
|
|
493
|
+
this._isReconfiguring = false;
|
|
494
|
+
}
|
|
284
495
|
// Disconnected!
|
|
285
496
|
this.connected = false;
|
|
286
497
|
// Check if this is ESP32-S2 Native USB that needs port reselection
|
|
@@ -297,6 +508,9 @@ export class ESPLoader extends EventTarget {
|
|
|
297
508
|
sleep(ms = 100) {
|
|
298
509
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
299
510
|
}
|
|
511
|
+
// ============================================================================
|
|
512
|
+
// Web Serial (Desktop) - DTR/RTS Signal Handling & Reset Strategies
|
|
513
|
+
// ============================================================================
|
|
300
514
|
async setRTS(state) {
|
|
301
515
|
await this.port.setSignals({ requestToSend: state });
|
|
302
516
|
// Work-around for adapters on Windows using the usbser.sys driver:
|
|
@@ -309,6 +523,511 @@ export class ESPLoader extends EventTarget {
|
|
|
309
523
|
this.state_DTR = state;
|
|
310
524
|
await this.port.setSignals({ dataTerminalReady: state });
|
|
311
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* @name hardResetUSBJTAGSerial
|
|
528
|
+
* USB-JTAG/Serial reset for Web Serial (Desktop)
|
|
529
|
+
*/
|
|
530
|
+
async hardResetUSBJTAGSerial() {
|
|
531
|
+
await this.setRTS(false);
|
|
532
|
+
await this.setDTR(false); // Idle
|
|
533
|
+
await this.sleep(100);
|
|
534
|
+
await this.setDTR(true); // Set IO0
|
|
535
|
+
await this.setRTS(false);
|
|
536
|
+
await this.sleep(100);
|
|
537
|
+
await this.setRTS(true); // Reset
|
|
538
|
+
await this.setDTR(false);
|
|
539
|
+
await this.setRTS(true);
|
|
540
|
+
await this.sleep(100);
|
|
541
|
+
await this.setDTR(false);
|
|
542
|
+
await this.setRTS(false); // Chip out of reset
|
|
543
|
+
await this.sleep(200);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* @name hardResetClassic
|
|
547
|
+
* Classic reset for Web Serial (Desktop)
|
|
548
|
+
*/
|
|
549
|
+
async hardResetClassic() {
|
|
550
|
+
await this.setDTR(false); // IO0=HIGH
|
|
551
|
+
await this.setRTS(true); // EN=LOW, chip in reset
|
|
552
|
+
await this.sleep(100);
|
|
553
|
+
await this.setDTR(true); // IO0=LOW
|
|
554
|
+
await this.setRTS(false); // EN=HIGH, chip out of reset
|
|
555
|
+
await this.sleep(50);
|
|
556
|
+
await this.setDTR(false); // IO0=HIGH, done
|
|
557
|
+
await this.sleep(200);
|
|
558
|
+
}
|
|
559
|
+
// ============================================================================
|
|
560
|
+
// WebUSB (Android) - DTR/RTS Signal Handling & Reset Strategies
|
|
561
|
+
// ============================================================================
|
|
562
|
+
async setRTSWebUSB(state) {
|
|
563
|
+
this.state_RTS = state;
|
|
564
|
+
// Always specify both signals to avoid flipping the other line
|
|
565
|
+
// The WebUSB setSignals() now preserves unspecified signals, but being explicit is safer
|
|
566
|
+
await this.port.setSignals({
|
|
567
|
+
requestToSend: state,
|
|
568
|
+
dataTerminalReady: this.state_DTR,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
async setDTRWebUSB(state) {
|
|
572
|
+
this.state_DTR = state;
|
|
573
|
+
// Always specify both signals to avoid flipping the other line
|
|
574
|
+
await this.port.setSignals({
|
|
575
|
+
dataTerminalReady: state,
|
|
576
|
+
requestToSend: this.state_RTS, // Explicitly preserve current RTS state
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
async setDTRandRTSWebUSB(dtr, rts) {
|
|
580
|
+
this.state_DTR = dtr;
|
|
581
|
+
this.state_RTS = rts;
|
|
582
|
+
await this.port.setSignals({
|
|
583
|
+
dataTerminalReady: dtr,
|
|
584
|
+
requestToSend: rts,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* @name hardResetUSBJTAGSerialWebUSB
|
|
589
|
+
* USB-JTAG/Serial reset for WebUSB (Android)
|
|
590
|
+
*/
|
|
591
|
+
async hardResetUSBJTAGSerialWebUSB() {
|
|
592
|
+
await this.setRTSWebUSB(false);
|
|
593
|
+
await this.setDTRWebUSB(false); // Idle
|
|
594
|
+
await this.sleep(100);
|
|
595
|
+
await this.setDTRWebUSB(true); // Set IO0
|
|
596
|
+
await this.setRTSWebUSB(false);
|
|
597
|
+
await this.sleep(100);
|
|
598
|
+
await this.setRTSWebUSB(true); // Reset
|
|
599
|
+
await this.setDTRWebUSB(false);
|
|
600
|
+
await this.setRTSWebUSB(true);
|
|
601
|
+
await this.sleep(100);
|
|
602
|
+
await this.setDTRWebUSB(false);
|
|
603
|
+
await this.setRTSWebUSB(false); // Chip out of reset
|
|
604
|
+
await this.sleep(200);
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* @name hardResetUSBJTAGSerialInvertedDTRWebUSB
|
|
608
|
+
* USB-JTAG/Serial reset with inverted DTR for WebUSB (Android)
|
|
609
|
+
*/
|
|
610
|
+
async hardResetUSBJTAGSerialInvertedDTRWebUSB() {
|
|
611
|
+
await this.setRTSWebUSB(false);
|
|
612
|
+
await this.setDTRWebUSB(true); // Idle (DTR inverted)
|
|
613
|
+
await this.sleep(100);
|
|
614
|
+
await this.setDTRWebUSB(false); // Set IO0 (DTR inverted)
|
|
615
|
+
await this.setRTSWebUSB(false);
|
|
616
|
+
await this.sleep(100);
|
|
617
|
+
await this.setRTSWebUSB(true); // Reset
|
|
618
|
+
await this.setDTRWebUSB(true); // (DTR inverted)
|
|
619
|
+
await this.setRTSWebUSB(true);
|
|
620
|
+
await this.sleep(100);
|
|
621
|
+
await this.setDTRWebUSB(true); // (DTR inverted)
|
|
622
|
+
await this.setRTSWebUSB(false); // Chip out of reset
|
|
623
|
+
await this.sleep(200);
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* @name hardResetClassicWebUSB
|
|
627
|
+
* Classic reset for WebUSB (Android)
|
|
628
|
+
*/
|
|
629
|
+
async hardResetClassicWebUSB() {
|
|
630
|
+
await this.setDTRWebUSB(false); // IO0=HIGH
|
|
631
|
+
await this.setRTSWebUSB(true); // EN=LOW, chip in reset
|
|
632
|
+
await this.sleep(100);
|
|
633
|
+
await this.setDTRWebUSB(true); // IO0=LOW
|
|
634
|
+
await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
|
|
635
|
+
await this.sleep(50);
|
|
636
|
+
await this.setDTRWebUSB(false); // IO0=HIGH, done
|
|
637
|
+
await this.sleep(200);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* @name hardResetUnixTightWebUSB
|
|
641
|
+
* Unix Tight reset for WebUSB (Android) - sets DTR and RTS simultaneously
|
|
642
|
+
*/
|
|
643
|
+
async hardResetUnixTightWebUSB() {
|
|
644
|
+
await this.setDTRandRTSWebUSB(false, false);
|
|
645
|
+
await this.setDTRandRTSWebUSB(true, true);
|
|
646
|
+
await this.setDTRandRTSWebUSB(false, true); // IO0=HIGH & EN=LOW, chip in reset
|
|
647
|
+
await this.sleep(100);
|
|
648
|
+
await this.setDTRandRTSWebUSB(true, false); // IO0=LOW & EN=HIGH, chip out of reset
|
|
649
|
+
await this.sleep(50);
|
|
650
|
+
await this.setDTRandRTSWebUSB(false, false); // IO0=HIGH, done
|
|
651
|
+
await this.setDTRWebUSB(false); // Ensure IO0=HIGH
|
|
652
|
+
await this.sleep(200);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* @name hardResetClassicLongDelayWebUSB
|
|
656
|
+
* Classic reset with longer delays for WebUSB (Android)
|
|
657
|
+
* Specifically for CP2102/CH340 which may need more time
|
|
658
|
+
*/
|
|
659
|
+
async hardResetClassicLongDelayWebUSB() {
|
|
660
|
+
await this.setDTRWebUSB(false); // IO0=HIGH
|
|
661
|
+
await this.setRTSWebUSB(true); // EN=LOW, chip in reset
|
|
662
|
+
await this.sleep(500); // Extra long delay
|
|
663
|
+
await this.setDTRWebUSB(true); // IO0=LOW
|
|
664
|
+
await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
|
|
665
|
+
await this.sleep(200);
|
|
666
|
+
await this.setDTRWebUSB(false); // IO0=HIGH, done
|
|
667
|
+
await this.sleep(500); // Extra long delay
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* @name hardResetClassicShortDelayWebUSB
|
|
671
|
+
* Classic reset with shorter delays for WebUSB (Android)
|
|
672
|
+
*/
|
|
673
|
+
async hardResetClassicShortDelayWebUSB() {
|
|
674
|
+
await this.setDTRWebUSB(false); // IO0=HIGH
|
|
675
|
+
await this.setRTSWebUSB(true); // EN=LOW, chip in reset
|
|
676
|
+
await this.sleep(50);
|
|
677
|
+
await this.setDTRWebUSB(true); // IO0=LOW
|
|
678
|
+
await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
|
|
679
|
+
await this.sleep(25);
|
|
680
|
+
await this.setDTRWebUSB(false); // IO0=HIGH, done
|
|
681
|
+
await this.sleep(100);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* @name hardResetInvertedWebUSB
|
|
685
|
+
* Inverted reset sequence for WebUSB (Android) - both signals inverted
|
|
686
|
+
*/
|
|
687
|
+
async hardResetInvertedWebUSB() {
|
|
688
|
+
await this.setDTRWebUSB(true); // IO0=HIGH (inverted)
|
|
689
|
+
await this.setRTSWebUSB(false); // EN=LOW, chip in reset (inverted)
|
|
690
|
+
await this.sleep(100);
|
|
691
|
+
await this.setDTRWebUSB(false); // IO0=LOW (inverted)
|
|
692
|
+
await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (inverted)
|
|
693
|
+
await this.sleep(50);
|
|
694
|
+
await this.setDTRWebUSB(true); // IO0=HIGH, done (inverted)
|
|
695
|
+
await this.sleep(200);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* @name hardResetInvertedDTRWebUSB
|
|
699
|
+
* Only DTR inverted for WebUSB (Android)
|
|
700
|
+
*/
|
|
701
|
+
async hardResetInvertedDTRWebUSB() {
|
|
702
|
+
await this.setDTRWebUSB(true); // IO0=HIGH (DTR inverted)
|
|
703
|
+
await this.setRTSWebUSB(true); // EN=LOW, chip in reset (RTS normal)
|
|
704
|
+
await this.sleep(100);
|
|
705
|
+
await this.setDTRWebUSB(false); // IO0=LOW (DTR inverted)
|
|
706
|
+
await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset (RTS normal)
|
|
707
|
+
await this.sleep(50);
|
|
708
|
+
await this.setDTRWebUSB(true); // IO0=HIGH, done (DTR inverted)
|
|
709
|
+
await this.sleep(200);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* @name hardResetInvertedRTSWebUSB
|
|
713
|
+
* Only RTS inverted for WebUSB (Android)
|
|
714
|
+
*/
|
|
715
|
+
async hardResetInvertedRTSWebUSB() {
|
|
716
|
+
await this.setDTRWebUSB(false); // IO0=HIGH (DTR normal)
|
|
717
|
+
await this.setRTSWebUSB(false); // EN=LOW, chip in reset (RTS inverted)
|
|
718
|
+
await this.sleep(100);
|
|
719
|
+
await this.setDTRWebUSB(true); // IO0=LOW (DTR normal)
|
|
720
|
+
await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (RTS inverted)
|
|
721
|
+
await this.sleep(50);
|
|
722
|
+
await this.setDTRWebUSB(false); // IO0=HIGH, done (DTR normal)
|
|
723
|
+
await this.sleep(200);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Check if we're using WebUSB (Android) or Web Serial (Desktop)
|
|
727
|
+
*/
|
|
728
|
+
isWebUSB() {
|
|
729
|
+
// WebUSBSerial class has isWebUSB flag - this is the most reliable check
|
|
730
|
+
return this.port.isWebUSB === true;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* @name connectWithResetStrategies
|
|
734
|
+
* Try different reset strategies to enter bootloader mode
|
|
735
|
+
* Similar to esptool.py's connect() method with multiple reset strategies
|
|
736
|
+
*/
|
|
737
|
+
async connectWithResetStrategies() {
|
|
738
|
+
const portInfo = this.port.getInfo();
|
|
739
|
+
const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
|
|
740
|
+
const isEspressifUSB = portInfo.usbVendorId === 0x303a;
|
|
741
|
+
// this.logger.log(
|
|
742
|
+
// `Detected USB: VID=0x${portInfo.usbVendorId?.toString(16) || "unknown"}, PID=0x${portInfo.usbProductId?.toString(16) || "unknown"}`,
|
|
743
|
+
// );
|
|
744
|
+
// Define reset strategies to try in order
|
|
745
|
+
const resetStrategies = [];
|
|
746
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
747
|
+
const self = this;
|
|
748
|
+
// WebUSB (Android) uses different reset methods than Web Serial (Desktop)
|
|
749
|
+
if (this.isWebUSB()) {
|
|
750
|
+
// For USB-Serial chips (CP2102, CH340, etc.), try inverted strategies first
|
|
751
|
+
const isUSBSerialChip = !isUSBJTAGSerial && !isEspressifUSB;
|
|
752
|
+
// Detect specific chip types once
|
|
753
|
+
const isCP2102 = portInfo.usbVendorId === 0x10c4;
|
|
754
|
+
const isCH34x = portInfo.usbVendorId === 0x1a86;
|
|
755
|
+
// Check for ESP32-S2 Native USB (VID: 0x303a, PID: 0x0002)
|
|
756
|
+
const isESP32S2NativeUSB = portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x0002;
|
|
757
|
+
// WebUSB Strategy 1: USB-JTAG/Serial reset (for Native USB only)
|
|
758
|
+
if (isUSBJTAGSerial || isEspressifUSB) {
|
|
759
|
+
if (isESP32S2NativeUSB) {
|
|
760
|
+
// ESP32-S2 Native USB: Try multiple strategies
|
|
761
|
+
// The device might be in JTAG mode OR CDC mode
|
|
762
|
+
// Strategy 1: USB-JTAG/Serial (works in CDC mode on Desktop)
|
|
763
|
+
resetStrategies.push({
|
|
764
|
+
name: "USB-JTAG/Serial (WebUSB) - ESP32-S2",
|
|
765
|
+
fn: async () => {
|
|
766
|
+
return await self.hardResetUSBJTAGSerialWebUSB();
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
// Strategy 2: USB-JTAG/Serial Inverted DTR (works in JTAG mode)
|
|
770
|
+
resetStrategies.push({
|
|
771
|
+
name: "USB-JTAG/Serial Inverted DTR (WebUSB) - ESP32-S2",
|
|
772
|
+
fn: async () => {
|
|
773
|
+
return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
// Strategy 3: UnixTight (CDC fallback)
|
|
777
|
+
resetStrategies.push({
|
|
778
|
+
name: "UnixTight (WebUSB) - ESP32-S2 CDC",
|
|
779
|
+
fn: async () => {
|
|
780
|
+
return await self.hardResetUnixTightWebUSB();
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
// Strategy 4: Classic reset (CDC fallback)
|
|
784
|
+
resetStrategies.push({
|
|
785
|
+
name: "Classic (WebUSB) - ESP32-S2 CDC",
|
|
786
|
+
fn: async () => {
|
|
787
|
+
return await self.hardResetClassicWebUSB();
|
|
788
|
+
},
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
// Other USB-JTAG chips: Try Inverted DTR first - works best for ESP32-H2 and other JTAG chips
|
|
793
|
+
resetStrategies.push({
|
|
794
|
+
name: "USB-JTAG/Serial Inverted DTR (WebUSB)",
|
|
795
|
+
fn: async () => {
|
|
796
|
+
return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
|
|
797
|
+
},
|
|
798
|
+
});
|
|
799
|
+
resetStrategies.push({
|
|
800
|
+
name: "USB-JTAG/Serial (WebUSB)",
|
|
801
|
+
fn: async () => {
|
|
802
|
+
return await self.hardResetUSBJTAGSerialWebUSB();
|
|
803
|
+
},
|
|
804
|
+
});
|
|
805
|
+
resetStrategies.push({
|
|
806
|
+
name: "Inverted DTR Classic (WebUSB)",
|
|
807
|
+
fn: async () => {
|
|
808
|
+
return await self.hardResetInvertedDTRWebUSB();
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// For USB-Serial chips, try inverted strategies first
|
|
814
|
+
if (isUSBSerialChip) {
|
|
815
|
+
if (isCH34x) {
|
|
816
|
+
// CH340/CH343: UnixTight works best (like CP2102)
|
|
817
|
+
resetStrategies.push({
|
|
818
|
+
name: "UnixTight (WebUSB) - CH34x",
|
|
819
|
+
fn: async () => {
|
|
820
|
+
return await self.hardResetUnixTightWebUSB();
|
|
821
|
+
},
|
|
822
|
+
});
|
|
823
|
+
resetStrategies.push({
|
|
824
|
+
name: "Classic (WebUSB) - CH34x",
|
|
825
|
+
fn: async () => {
|
|
826
|
+
return await self.hardResetClassicWebUSB();
|
|
827
|
+
},
|
|
828
|
+
});
|
|
829
|
+
resetStrategies.push({
|
|
830
|
+
name: "Inverted Both (WebUSB) - CH34x",
|
|
831
|
+
fn: async () => {
|
|
832
|
+
return await self.hardResetInvertedWebUSB();
|
|
833
|
+
},
|
|
834
|
+
});
|
|
835
|
+
resetStrategies.push({
|
|
836
|
+
name: "Inverted RTS (WebUSB) - CH34x",
|
|
837
|
+
fn: async () => {
|
|
838
|
+
return await self.hardResetInvertedRTSWebUSB();
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
resetStrategies.push({
|
|
842
|
+
name: "Inverted DTR (WebUSB) - CH34x",
|
|
843
|
+
fn: async () => {
|
|
844
|
+
return await self.hardResetInvertedDTRWebUSB();
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
else if (isCP2102) {
|
|
849
|
+
// CP2102: UnixTight works best (tested and confirmed)
|
|
850
|
+
// Try it first, then fallback to other strategies
|
|
851
|
+
resetStrategies.push({
|
|
852
|
+
name: "UnixTight (WebUSB) - CP2102",
|
|
853
|
+
fn: async () => {
|
|
854
|
+
return await self.hardResetUnixTightWebUSB();
|
|
855
|
+
},
|
|
856
|
+
});
|
|
857
|
+
resetStrategies.push({
|
|
858
|
+
name: "Classic (WebUSB) - CP2102",
|
|
859
|
+
fn: async () => {
|
|
860
|
+
return await self.hardResetClassicWebUSB();
|
|
861
|
+
},
|
|
862
|
+
});
|
|
863
|
+
resetStrategies.push({
|
|
864
|
+
name: "Inverted Both (WebUSB) - CP2102",
|
|
865
|
+
fn: async () => {
|
|
866
|
+
return await self.hardResetInvertedWebUSB();
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
resetStrategies.push({
|
|
870
|
+
name: "Inverted RTS (WebUSB) - CP2102",
|
|
871
|
+
fn: async () => {
|
|
872
|
+
return await self.hardResetInvertedRTSWebUSB();
|
|
873
|
+
},
|
|
874
|
+
});
|
|
875
|
+
resetStrategies.push({
|
|
876
|
+
name: "Inverted DTR (WebUSB) - CP2102",
|
|
877
|
+
fn: async () => {
|
|
878
|
+
return await self.hardResetInvertedDTRWebUSB();
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
else {
|
|
883
|
+
// For other USB-Serial chips, try UnixTight first, then multiple strategies
|
|
884
|
+
resetStrategies.push({
|
|
885
|
+
name: "UnixTight (WebUSB)",
|
|
886
|
+
fn: async () => {
|
|
887
|
+
return await self.hardResetUnixTightWebUSB();
|
|
888
|
+
},
|
|
889
|
+
});
|
|
890
|
+
resetStrategies.push({
|
|
891
|
+
name: "Classic (WebUSB)",
|
|
892
|
+
fn: async function () {
|
|
893
|
+
return await self.hardResetClassicWebUSB();
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
resetStrategies.push({
|
|
897
|
+
name: "Inverted Both (WebUSB)",
|
|
898
|
+
fn: async function () {
|
|
899
|
+
return await self.hardResetInvertedWebUSB();
|
|
900
|
+
},
|
|
901
|
+
});
|
|
902
|
+
resetStrategies.push({
|
|
903
|
+
name: "Inverted RTS (WebUSB)",
|
|
904
|
+
fn: async function () {
|
|
905
|
+
return await self.hardResetInvertedRTSWebUSB();
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
resetStrategies.push({
|
|
909
|
+
name: "Inverted DTR (WebUSB)",
|
|
910
|
+
fn: async function () {
|
|
911
|
+
return await self.hardResetInvertedDTRWebUSB();
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// Add general fallback strategies only for non-CP2102 and non-ESP32-S2 Native USB chips
|
|
917
|
+
if (!isCP2102 && !isESP32S2NativeUSB) {
|
|
918
|
+
// Classic reset (for chips not handled above)
|
|
919
|
+
if (portInfo.usbVendorId !== 0x1a86) {
|
|
920
|
+
resetStrategies.push({
|
|
921
|
+
name: "Classic (WebUSB)",
|
|
922
|
+
fn: async function () {
|
|
923
|
+
return await self.hardResetClassicWebUSB();
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
// UnixTight reset (sets DTR/RTS simultaneously)
|
|
928
|
+
resetStrategies.push({
|
|
929
|
+
name: "UnixTight (WebUSB)",
|
|
930
|
+
fn: async function () {
|
|
931
|
+
return await self.hardResetUnixTightWebUSB();
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
// WebUSB Strategy: Classic with long delays
|
|
935
|
+
resetStrategies.push({
|
|
936
|
+
name: "Classic Long Delay (WebUSB)",
|
|
937
|
+
fn: async function () {
|
|
938
|
+
return await self.hardResetClassicLongDelayWebUSB();
|
|
939
|
+
},
|
|
940
|
+
});
|
|
941
|
+
// WebUSB Strategy: Classic with short delays
|
|
942
|
+
resetStrategies.push({
|
|
943
|
+
name: "Classic Short Delay (WebUSB)",
|
|
944
|
+
fn: async function () {
|
|
945
|
+
return await self.hardResetClassicShortDelayWebUSB();
|
|
946
|
+
},
|
|
947
|
+
});
|
|
948
|
+
// WebUSB Strategy: USB-JTAG/Serial fallback
|
|
949
|
+
if (!isUSBJTAGSerial && !isEspressifUSB) {
|
|
950
|
+
resetStrategies.push({
|
|
951
|
+
name: "USB-JTAG/Serial fallback (WebUSB)",
|
|
952
|
+
fn: async function () {
|
|
953
|
+
return await self.hardResetUSBJTAGSerialWebUSB();
|
|
954
|
+
},
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
// Strategy: USB-JTAG/Serial reset
|
|
961
|
+
if (isUSBJTAGSerial || isEspressifUSB) {
|
|
962
|
+
resetStrategies.push({
|
|
963
|
+
name: "USB-JTAG/Serial",
|
|
964
|
+
fn: async function () {
|
|
965
|
+
return await self.hardResetUSBJTAGSerial();
|
|
966
|
+
},
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
// Strategy: Classic reset
|
|
970
|
+
resetStrategies.push({
|
|
971
|
+
name: "Classic",
|
|
972
|
+
fn: async function () {
|
|
973
|
+
return await self.hardResetClassic();
|
|
974
|
+
},
|
|
975
|
+
});
|
|
976
|
+
// Strategy: USB-JTAG/Serial fallback
|
|
977
|
+
if (!isUSBJTAGSerial && !isEspressifUSB) {
|
|
978
|
+
resetStrategies.push({
|
|
979
|
+
name: "USB-JTAG/Serial (fallback)",
|
|
980
|
+
fn: async function () {
|
|
981
|
+
return await self.hardResetUSBJTAGSerial();
|
|
982
|
+
},
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
let lastError = null;
|
|
987
|
+
// Try each reset strategy with timeout
|
|
988
|
+
for (const strategy of resetStrategies) {
|
|
989
|
+
try {
|
|
990
|
+
// Check if port is still open, if not, skip this strategy
|
|
991
|
+
if (!this.connected || !this.port.writable) {
|
|
992
|
+
this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
// Clear abandon flag before starting new strategy
|
|
996
|
+
this._abandonCurrentOperation = false;
|
|
997
|
+
await strategy.fn();
|
|
998
|
+
// Try to sync after reset with internally time-bounded sync (3 seconds per strategy)
|
|
999
|
+
const syncSuccess = await this.syncWithTimeout(3000);
|
|
1000
|
+
if (syncSuccess) {
|
|
1001
|
+
// Sync succeeded
|
|
1002
|
+
this.logger.log(`Connected successfully with ${strategy.name} reset.`);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
throw new Error("Sync timeout or abandoned");
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
catch (error) {
|
|
1010
|
+
lastError = error;
|
|
1011
|
+
this.logger.log(`${strategy.name} reset failed: ${error.message}`);
|
|
1012
|
+
// Set abandon flag to stop any in-flight operations
|
|
1013
|
+
this._abandonCurrentOperation = true;
|
|
1014
|
+
// Wait a bit for in-flight operations to abort
|
|
1015
|
+
await sleep(100);
|
|
1016
|
+
// If port got disconnected, we can't try more strategies
|
|
1017
|
+
if (!this.connected || !this.port.writable) {
|
|
1018
|
+
this.logger.log(`Port disconnected during reset attempt`);
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
// Clear buffers before trying next strategy
|
|
1022
|
+
this._clearInputBuffer();
|
|
1023
|
+
await this.drainInputBuffer(200);
|
|
1024
|
+
await this.flushSerialBuffers();
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
// All strategies failed - reset abandon flag before throwing
|
|
1028
|
+
this._abandonCurrentOperation = false;
|
|
1029
|
+
throw new Error(`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
|
|
1030
|
+
}
|
|
312
1031
|
async hardReset(bootloader = false) {
|
|
313
1032
|
if (bootloader) {
|
|
314
1033
|
// enter flash mode
|
|
@@ -317,16 +1036,34 @@ export class ESPLoader extends EventTarget {
|
|
|
317
1036
|
this.logger.log("USB-JTAG/Serial reset.");
|
|
318
1037
|
}
|
|
319
1038
|
else {
|
|
320
|
-
|
|
321
|
-
this.
|
|
1039
|
+
// Use different reset strategy for WebUSB (Android) vs Web Serial (Desktop)
|
|
1040
|
+
if (this.isWebUSB()) {
|
|
1041
|
+
await this.hardResetClassicWebUSB();
|
|
1042
|
+
this.logger.log("Classic reset (WebUSB/Android).");
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
await this.hardResetClassic();
|
|
1046
|
+
this.logger.log("Classic reset.");
|
|
1047
|
+
}
|
|
322
1048
|
}
|
|
323
1049
|
}
|
|
324
1050
|
else {
|
|
325
|
-
// just reset
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
1051
|
+
// just reset (no bootloader mode)
|
|
1052
|
+
if (this.isWebUSB()) {
|
|
1053
|
+
// WebUSB: Use longer delays for better compatibility
|
|
1054
|
+
await this.setRTSWebUSB(true); // EN->LOW
|
|
1055
|
+
await this.sleep(200);
|
|
1056
|
+
await this.setRTSWebUSB(false);
|
|
1057
|
+
await this.sleep(200);
|
|
1058
|
+
this.logger.log("Hard reset (WebUSB).");
|
|
1059
|
+
}
|
|
1060
|
+
else {
|
|
1061
|
+
// Web Serial: Standard reset
|
|
1062
|
+
await this.setRTS(true); // EN->LOW
|
|
1063
|
+
await this.sleep(100);
|
|
1064
|
+
await this.setRTS(false);
|
|
1065
|
+
this.logger.log("Hard reset.");
|
|
1066
|
+
}
|
|
330
1067
|
}
|
|
331
1068
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
332
1069
|
}
|
|
@@ -451,6 +1188,12 @@ export class ESPLoader extends EventTarget {
|
|
|
451
1188
|
else if ([2, 4].includes(data.length)) {
|
|
452
1189
|
statusLen = data.length;
|
|
453
1190
|
}
|
|
1191
|
+
else {
|
|
1192
|
+
// Default to 2-byte status if we can't determine
|
|
1193
|
+
// This prevents silent data corruption when statusLen would be 0
|
|
1194
|
+
statusLen = 2;
|
|
1195
|
+
this.logger.debug(`Unknown chip family, defaulting to 2-byte status (opcode: ${toHex(opcode)}, data.length: ${data.length})`);
|
|
1196
|
+
}
|
|
454
1197
|
}
|
|
455
1198
|
if (data.length < statusLen) {
|
|
456
1199
|
throw new Error("Didn't get enough status bytes");
|
|
@@ -499,71 +1242,169 @@ export class ESPLoader extends EventTarget {
|
|
|
499
1242
|
* @name readPacket
|
|
500
1243
|
* Generator to read SLIP packets from a serial port.
|
|
501
1244
|
* Yields one full SLIP packet at a time, raises exception on timeout or invalid data.
|
|
1245
|
+
*
|
|
1246
|
+
* Two implementations:
|
|
1247
|
+
* - Burst: CDC devices (Native USB) and CH343 - very fast processing
|
|
1248
|
+
* - Byte-by-byte: CH340, CP2102, and other USB-Serial adapters - stable fast processing
|
|
502
1249
|
*/
|
|
503
1250
|
async readPacket(timeout) {
|
|
504
1251
|
let partialPacket = null;
|
|
505
1252
|
let inEscape = false;
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
1253
|
+
// CDC devices use burst processing, non-CDC use byte-by-byte
|
|
1254
|
+
if (this._isCDCDevice) {
|
|
1255
|
+
// Burst version: Process all available bytes in one pass for ultra-high-speed transfers
|
|
1256
|
+
// Used for: CDC devices (all platforms) and CH343
|
|
1257
|
+
const startTime = Date.now();
|
|
1258
|
+
while (true) {
|
|
1259
|
+
// Check abandon flag (for reset strategy timeout)
|
|
1260
|
+
if (this._abandonCurrentOperation) {
|
|
1261
|
+
throw new SlipReadError("Operation abandoned (reset strategy timeout)");
|
|
1262
|
+
}
|
|
1263
|
+
// Check timeout
|
|
1264
|
+
if (Date.now() - startTime > timeout) {
|
|
1265
|
+
const waitingFor = partialPacket === null ? "header" : "content";
|
|
1266
|
+
throw new SlipReadError("Timed out waiting for packet " + waitingFor);
|
|
1267
|
+
}
|
|
1268
|
+
// If no data available, wait a bit
|
|
1269
|
+
if (this._inputBufferAvailable === 0) {
|
|
1270
|
+
await sleep(1);
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
// Process all available bytes without going back to outer loop
|
|
1274
|
+
// This is critical for handling high-speed burst transfers
|
|
1275
|
+
while (this._inputBufferAvailable > 0) {
|
|
1276
|
+
// Periodic timeout check to prevent hang on slow data
|
|
1277
|
+
if (Date.now() - startTime > timeout) {
|
|
1278
|
+
const waitingFor = partialPacket === null ? "header" : "content";
|
|
1279
|
+
throw new SlipReadError("Timed out waiting for packet " + waitingFor);
|
|
526
1280
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
1281
|
+
const b = this._readByte();
|
|
1282
|
+
if (partialPacket === null) {
|
|
1283
|
+
// waiting for packet header
|
|
1284
|
+
if (b == 0xc0) {
|
|
1285
|
+
partialPacket = [];
|
|
1286
|
+
}
|
|
1287
|
+
else {
|
|
1288
|
+
if (this.debug) {
|
|
1289
|
+
this.logger.debug("Read invalid data: " + toHex(b));
|
|
1290
|
+
this.logger.debug("Remaining data in serial buffer: " +
|
|
1291
|
+
hexFormatter(this._inputBuffer));
|
|
1292
|
+
}
|
|
1293
|
+
throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
|
|
532
1294
|
}
|
|
533
|
-
throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
|
|
534
1295
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
1296
|
+
else if (inEscape) {
|
|
1297
|
+
// part-way through escape sequence
|
|
1298
|
+
inEscape = false;
|
|
1299
|
+
if (b == 0xdc) {
|
|
1300
|
+
partialPacket.push(0xc0);
|
|
1301
|
+
}
|
|
1302
|
+
else if (b == 0xdd) {
|
|
1303
|
+
partialPacket.push(0xdb);
|
|
1304
|
+
}
|
|
1305
|
+
else {
|
|
1306
|
+
if (this.debug) {
|
|
1307
|
+
this.logger.debug("Read invalid data: " + toHex(b));
|
|
1308
|
+
this.logger.debug("Remaining data in serial buffer: " +
|
|
1309
|
+
hexFormatter(this._inputBuffer));
|
|
1310
|
+
}
|
|
1311
|
+
throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
|
|
1312
|
+
}
|
|
541
1313
|
}
|
|
542
|
-
else if (b ==
|
|
543
|
-
|
|
1314
|
+
else if (b == 0xdb) {
|
|
1315
|
+
// start of escape sequence
|
|
1316
|
+
inEscape = true;
|
|
1317
|
+
}
|
|
1318
|
+
else if (b == 0xc0) {
|
|
1319
|
+
// end of packet
|
|
1320
|
+
if (this.debug)
|
|
1321
|
+
this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
|
|
1322
|
+
// Compact buffer periodically to prevent memory growth
|
|
1323
|
+
this._compactInputBuffer();
|
|
1324
|
+
return partialPacket;
|
|
544
1325
|
}
|
|
545
1326
|
else {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
this.logger.debug("Remaining data in serial buffer: " +
|
|
549
|
-
hexFormatter(this._inputBuffer));
|
|
550
|
-
}
|
|
551
|
-
throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
|
|
1327
|
+
// normal byte in packet
|
|
1328
|
+
partialPacket.push(b);
|
|
552
1329
|
}
|
|
553
1330
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
else {
|
|
1334
|
+
// Byte-by-byte version: Stable for non CDC USB-Serial adapters (CH340, CP2102, etc.)
|
|
1335
|
+
let readBytes = [];
|
|
1336
|
+
while (true) {
|
|
1337
|
+
// Check abandon flag (for reset strategy timeout)
|
|
1338
|
+
if (this._abandonCurrentOperation) {
|
|
1339
|
+
throw new SlipReadError("Operation abandoned (reset strategy timeout)");
|
|
557
1340
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
1341
|
+
const stamp = Date.now();
|
|
1342
|
+
readBytes = [];
|
|
1343
|
+
while (Date.now() - stamp < timeout) {
|
|
1344
|
+
if (this._inputBufferAvailable > 0) {
|
|
1345
|
+
readBytes.push(this._readByte());
|
|
1346
|
+
break;
|
|
1347
|
+
}
|
|
1348
|
+
else {
|
|
1349
|
+
// Reduced sleep time for faster response during high-speed transfers
|
|
1350
|
+
await sleep(1);
|
|
1351
|
+
}
|
|
563
1352
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
1353
|
+
if (readBytes.length == 0) {
|
|
1354
|
+
const waitingFor = partialPacket === null ? "header" : "content";
|
|
1355
|
+
throw new SlipReadError("Timed out waiting for packet " + waitingFor);
|
|
1356
|
+
}
|
|
1357
|
+
if (this.debug)
|
|
1358
|
+
this.logger.debug("Read " + readBytes.length + " bytes: " + hexFormatter(readBytes));
|
|
1359
|
+
for (const b of readBytes) {
|
|
1360
|
+
if (partialPacket === null) {
|
|
1361
|
+
// waiting for packet header
|
|
1362
|
+
if (b == 0xc0) {
|
|
1363
|
+
partialPacket = [];
|
|
1364
|
+
}
|
|
1365
|
+
else {
|
|
1366
|
+
if (this.debug) {
|
|
1367
|
+
this.logger.debug("Read invalid data: " + toHex(b));
|
|
1368
|
+
this.logger.debug("Remaining data in serial buffer: " +
|
|
1369
|
+
hexFormatter(this._inputBuffer));
|
|
1370
|
+
}
|
|
1371
|
+
throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
else if (inEscape) {
|
|
1375
|
+
// part-way through escape sequence
|
|
1376
|
+
inEscape = false;
|
|
1377
|
+
if (b == 0xdc) {
|
|
1378
|
+
partialPacket.push(0xc0);
|
|
1379
|
+
}
|
|
1380
|
+
else if (b == 0xdd) {
|
|
1381
|
+
partialPacket.push(0xdb);
|
|
1382
|
+
}
|
|
1383
|
+
else {
|
|
1384
|
+
if (this.debug) {
|
|
1385
|
+
this.logger.debug("Read invalid data: " + toHex(b));
|
|
1386
|
+
this.logger.debug("Remaining data in serial buffer: " +
|
|
1387
|
+
hexFormatter(this._inputBuffer));
|
|
1388
|
+
}
|
|
1389
|
+
throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
else if (b == 0xdb) {
|
|
1393
|
+
// start of escape sequence
|
|
1394
|
+
inEscape = true;
|
|
1395
|
+
}
|
|
1396
|
+
else if (b == 0xc0) {
|
|
1397
|
+
// end of packet
|
|
1398
|
+
if (this.debug)
|
|
1399
|
+
this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
|
|
1400
|
+
// Compact buffer periodically to prevent memory growth
|
|
1401
|
+
this._compactInputBuffer();
|
|
1402
|
+
return partialPacket;
|
|
1403
|
+
}
|
|
1404
|
+
else {
|
|
1405
|
+
// normal byte in packet
|
|
1406
|
+
partialPacket.push(b);
|
|
1407
|
+
}
|
|
567
1408
|
}
|
|
568
1409
|
}
|
|
569
1410
|
}
|
|
@@ -595,7 +1436,7 @@ export class ESPLoader extends EventTarget {
|
|
|
595
1436
|
throw new Error(`Invalid (unsupported) command ${toHex(opcode)}`);
|
|
596
1437
|
}
|
|
597
1438
|
}
|
|
598
|
-
throw "Response doesn't match request";
|
|
1439
|
+
throw new Error("Response doesn't match request");
|
|
599
1440
|
}
|
|
600
1441
|
/**
|
|
601
1442
|
* @name checksum
|
|
@@ -647,8 +1488,9 @@ export class ESPLoader extends EventTarget {
|
|
|
647
1488
|
}
|
|
648
1489
|
async reconfigurePort(baud) {
|
|
649
1490
|
var _a;
|
|
1491
|
+
// Block new writes during the entire reconfiguration (all paths)
|
|
1492
|
+
this._isReconfiguring = true;
|
|
650
1493
|
try {
|
|
651
|
-
this._isReconfiguring = true;
|
|
652
1494
|
// Wait for pending writes to complete
|
|
653
1495
|
try {
|
|
654
1496
|
await this._writeChain;
|
|
@@ -656,6 +1498,30 @@ export class ESPLoader extends EventTarget {
|
|
|
656
1498
|
catch (err) {
|
|
657
1499
|
this.logger.debug(`Pending write error during reconfigure: ${err}`);
|
|
658
1500
|
}
|
|
1501
|
+
// WebUSB: Check if we should use setBaudRate() or close/reopen
|
|
1502
|
+
if (this.isWebUSB()) {
|
|
1503
|
+
const portInfo = this.port.getInfo();
|
|
1504
|
+
const isCH343 = portInfo.usbVendorId === 0x1a86 && portInfo.usbProductId === 0x55d3;
|
|
1505
|
+
// CH343 is a CDC device and MUST use close/reopen
|
|
1506
|
+
// Other chips (CH340, CP2102, FTDI) MUST use setBaudRate()
|
|
1507
|
+
if (!isCH343 &&
|
|
1508
|
+
typeof this.port.setBaudRate === "function") {
|
|
1509
|
+
// this.logger.log(
|
|
1510
|
+
// `[WebUSB] Changing baudrate to ${baud} using setBaudRate()...`,
|
|
1511
|
+
// );
|
|
1512
|
+
await this.port.setBaudRate(baud);
|
|
1513
|
+
// this.logger.log(`[WebUSB] Baudrate changed to ${baud}`);
|
|
1514
|
+
// Give the chip time to adjust to new baudrate
|
|
1515
|
+
await sleep(100);
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
else if (isCH343) {
|
|
1519
|
+
// this.logger.log(
|
|
1520
|
+
// `[WebUSB] CH343 detected - using close/reopen for baudrate change`,
|
|
1521
|
+
// );
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
// Web Serial or CH343: Close and reopen port
|
|
659
1525
|
// Release persistent writer before closing
|
|
660
1526
|
if (this._writer) {
|
|
661
1527
|
try {
|
|
@@ -684,110 +1550,44 @@ export class ESPLoader extends EventTarget {
|
|
|
684
1550
|
throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
|
|
685
1551
|
}
|
|
686
1552
|
finally {
|
|
1553
|
+
// Always reset flag, even on error or early return
|
|
687
1554
|
this._isReconfiguring = false;
|
|
688
1555
|
}
|
|
689
1556
|
}
|
|
690
1557
|
/**
|
|
691
|
-
* @name
|
|
692
|
-
*
|
|
693
|
-
*
|
|
1558
|
+
* @name syncWithTimeout
|
|
1559
|
+
* Sync with timeout that can be abandoned (for reset strategy loop)
|
|
1560
|
+
* This is internally time-bounded and checks the abandon flag
|
|
694
1561
|
*/
|
|
695
|
-
async
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
name: "USB-JTAG/Serial",
|
|
708
|
-
fn: async () => await this.hardResetUSBJTAGSerial(),
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
// Strategy 2: Classic reset (for USB-to-Serial bridges)
|
|
712
|
-
resetStrategies.push({
|
|
713
|
-
name: "Classic",
|
|
714
|
-
fn: async () => await this.hardResetClassic(),
|
|
715
|
-
});
|
|
716
|
-
// Strategy 3: If USB-JTAG/Serial was not tried yet, try it as fallback
|
|
717
|
-
if (!isUSBJTAGSerial && !isEspressifUSB) {
|
|
718
|
-
resetStrategies.push({
|
|
719
|
-
name: "USB-JTAG/Serial (fallback)",
|
|
720
|
-
fn: async () => await this.hardResetUSBJTAGSerial(),
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
let lastError = null;
|
|
724
|
-
// Try each reset strategy
|
|
725
|
-
for (const strategy of resetStrategies) {
|
|
1562
|
+
async syncWithTimeout(timeoutMs) {
|
|
1563
|
+
const startTime = Date.now();
|
|
1564
|
+
for (let i = 0; i < 5; i++) {
|
|
1565
|
+
// Check if we've exceeded the timeout
|
|
1566
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
1567
|
+
return false;
|
|
1568
|
+
}
|
|
1569
|
+
// Check abandon flag
|
|
1570
|
+
if (this._abandonCurrentOperation) {
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
this._clearInputBuffer();
|
|
726
1574
|
try {
|
|
727
|
-
this.
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
continue;
|
|
1575
|
+
const response = await this._sync();
|
|
1576
|
+
if (response) {
|
|
1577
|
+
await sleep(SYNC_TIMEOUT);
|
|
1578
|
+
return true;
|
|
732
1579
|
}
|
|
733
|
-
|
|
734
|
-
// Try to sync after reset
|
|
735
|
-
await this.sync();
|
|
736
|
-
// If we get here, sync succeeded
|
|
737
|
-
this.logger.log(`Connected successfully with ${strategy.name} reset.`);
|
|
738
|
-
return;
|
|
1580
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
739
1581
|
}
|
|
740
|
-
catch (
|
|
741
|
-
|
|
742
|
-
this.
|
|
743
|
-
|
|
744
|
-
if (!this.connected || !this.port.writable) {
|
|
745
|
-
this.logger.log(`Port disconnected during reset attempt`);
|
|
746
|
-
break;
|
|
1582
|
+
catch (e) {
|
|
1583
|
+
// Check abandon flag after error
|
|
1584
|
+
if (this._abandonCurrentOperation) {
|
|
1585
|
+
return false;
|
|
747
1586
|
}
|
|
748
|
-
// Clear buffers before trying next strategy
|
|
749
|
-
this._inputBuffer.length = 0;
|
|
750
|
-
await this.drainInputBuffer(200);
|
|
751
|
-
await this.flushSerialBuffers();
|
|
752
1587
|
}
|
|
1588
|
+
await sleep(SYNC_TIMEOUT);
|
|
753
1589
|
}
|
|
754
|
-
|
|
755
|
-
throw new Error(`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
|
|
756
|
-
}
|
|
757
|
-
/**
|
|
758
|
-
* @name hardResetUSBJTAGSerial
|
|
759
|
-
* USB-JTAG/Serial reset sequence for ESP32-C3, ESP32-S3, ESP32-C6, etc.
|
|
760
|
-
*/
|
|
761
|
-
async hardResetUSBJTAGSerial() {
|
|
762
|
-
await this.setRTS(false);
|
|
763
|
-
await this.setDTR(false); // Idle
|
|
764
|
-
await this.sleep(100);
|
|
765
|
-
await this.setDTR(true); // Set IO0
|
|
766
|
-
await this.setRTS(false);
|
|
767
|
-
await this.sleep(100);
|
|
768
|
-
await this.setRTS(true); // Reset. Calls inverted to go through (1,1) instead of (0,0)
|
|
769
|
-
await this.setDTR(false);
|
|
770
|
-
await this.setRTS(true); // RTS set as Windows only propagates DTR on RTS setting
|
|
771
|
-
await this.sleep(100);
|
|
772
|
-
await this.setDTR(false);
|
|
773
|
-
await this.setRTS(false); // Chip out of reset
|
|
774
|
-
// Wait for chip to boot into bootloader
|
|
775
|
-
await this.sleep(200);
|
|
776
|
-
}
|
|
777
|
-
/**
|
|
778
|
-
* @name hardResetClassic
|
|
779
|
-
* Classic reset sequence for USB-to-Serial bridge chips (CH340, CP2102, etc.)
|
|
780
|
-
*/
|
|
781
|
-
async hardResetClassic() {
|
|
782
|
-
await this.setDTR(false); // IO0=HIGH
|
|
783
|
-
await this.setRTS(true); // EN=LOW, chip in reset
|
|
784
|
-
await this.sleep(100);
|
|
785
|
-
await this.setDTR(true); // IO0=LOW
|
|
786
|
-
await this.setRTS(false); // EN=HIGH, chip out of reset
|
|
787
|
-
await this.sleep(50);
|
|
788
|
-
await this.setDTR(false); // IO0=HIGH, done
|
|
789
|
-
// Wait for chip to boot into bootloader
|
|
790
|
-
await this.sleep(200);
|
|
1590
|
+
return false;
|
|
791
1591
|
}
|
|
792
1592
|
/**
|
|
793
1593
|
* @name sync
|
|
@@ -796,7 +1596,7 @@ export class ESPLoader extends EventTarget {
|
|
|
796
1596
|
*/
|
|
797
1597
|
async sync() {
|
|
798
1598
|
for (let i = 0; i < 5; i++) {
|
|
799
|
-
this.
|
|
1599
|
+
this._clearInputBuffer();
|
|
800
1600
|
const response = await this._sync();
|
|
801
1601
|
if (response) {
|
|
802
1602
|
await sleep(SYNC_TIMEOUT);
|
|
@@ -820,8 +1620,10 @@ export class ESPLoader extends EventTarget {
|
|
|
820
1620
|
return true;
|
|
821
1621
|
}
|
|
822
1622
|
}
|
|
823
|
-
catch {
|
|
824
|
-
|
|
1623
|
+
catch (e) {
|
|
1624
|
+
if (this.debug) {
|
|
1625
|
+
this.logger.debug(`Sync attempt ${i + 1} failed: ${e}`);
|
|
1626
|
+
}
|
|
825
1627
|
}
|
|
826
1628
|
}
|
|
827
1629
|
return false;
|
|
@@ -1296,12 +2098,19 @@ export class ESPLoader extends EventTarget {
|
|
|
1296
2098
|
await this._writer.write(new Uint8Array(data));
|
|
1297
2099
|
}, async () => {
|
|
1298
2100
|
// Previous write failed, but still attempt this write
|
|
2101
|
+
this.logger.debug("Previous write failed, attempting recovery for current write");
|
|
1299
2102
|
if (!this.port.writable) {
|
|
1300
2103
|
throw new Error("Port became unavailable during write");
|
|
1301
2104
|
}
|
|
1302
2105
|
// Writer was likely cleaned up by previous error, create new one
|
|
1303
2106
|
if (!this._writer) {
|
|
1304
|
-
|
|
2107
|
+
try {
|
|
2108
|
+
this._writer = this.port.writable.getWriter();
|
|
2109
|
+
}
|
|
2110
|
+
catch (err) {
|
|
2111
|
+
this.logger.debug(`Failed to get writer in recovery: ${err}`);
|
|
2112
|
+
throw new Error("Cannot acquire writer lock");
|
|
2113
|
+
}
|
|
1305
2114
|
}
|
|
1306
2115
|
await this._writer.write(new Uint8Array(data));
|
|
1307
2116
|
})
|
|
@@ -1312,7 +2121,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1312
2121
|
try {
|
|
1313
2122
|
this._writer.releaseLock();
|
|
1314
2123
|
}
|
|
1315
|
-
catch
|
|
2124
|
+
catch {
|
|
1316
2125
|
// Ignore release errors
|
|
1317
2126
|
}
|
|
1318
2127
|
this._writer = undefined;
|
|
@@ -1332,49 +2141,69 @@ export class ESPLoader extends EventTarget {
|
|
|
1332
2141
|
this.logger.debug("Port already closed, skipping disconnect");
|
|
1333
2142
|
return;
|
|
1334
2143
|
}
|
|
2144
|
+
// Wait for pending writes to complete
|
|
1335
2145
|
try {
|
|
1336
|
-
this.
|
|
1337
|
-
|
|
2146
|
+
await this._writeChain;
|
|
2147
|
+
}
|
|
2148
|
+
catch (err) {
|
|
2149
|
+
this.logger.debug(`Pending write error during disconnect: ${err}`);
|
|
2150
|
+
}
|
|
2151
|
+
// Release persistent writer before closing
|
|
2152
|
+
if (this._writer) {
|
|
1338
2153
|
try {
|
|
1339
|
-
await this.
|
|
2154
|
+
await this._writer.close();
|
|
2155
|
+
this._writer.releaseLock();
|
|
1340
2156
|
}
|
|
1341
2157
|
catch (err) {
|
|
1342
|
-
this.logger.debug(`
|
|
2158
|
+
this.logger.debug(`Writer close/release error: ${err}`);
|
|
1343
2159
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
this._writer = undefined;
|
|
2160
|
+
this._writer = undefined;
|
|
2161
|
+
}
|
|
2162
|
+
else {
|
|
2163
|
+
// No persistent writer exists, close stream directly
|
|
2164
|
+
// This path is taken when no writes have been queued
|
|
2165
|
+
try {
|
|
2166
|
+
const writer = this.port.writable.getWriter();
|
|
2167
|
+
await writer.close();
|
|
2168
|
+
writer.releaseLock();
|
|
1354
2169
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
// This path is taken when no writes have been queued
|
|
1358
|
-
try {
|
|
1359
|
-
const writer = this.port.writable.getWriter();
|
|
1360
|
-
await writer.close();
|
|
1361
|
-
writer.releaseLock();
|
|
1362
|
-
}
|
|
1363
|
-
catch (err) {
|
|
1364
|
-
this.logger.debug(`Direct writer close error: ${err}`);
|
|
1365
|
-
}
|
|
2170
|
+
catch (err) {
|
|
2171
|
+
this.logger.debug(`Direct writer close error: ${err}`);
|
|
1366
2172
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
2173
|
+
}
|
|
2174
|
+
await new Promise((resolve) => {
|
|
2175
|
+
if (!this._reader) {
|
|
2176
|
+
resolve(undefined);
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
// Set a timeout to prevent hanging (important for node-usb)
|
|
2180
|
+
const timeout = setTimeout(() => {
|
|
2181
|
+
this.logger.debug("Disconnect timeout - forcing resolution");
|
|
2182
|
+
resolve(undefined);
|
|
2183
|
+
}, 1000);
|
|
2184
|
+
this.addEventListener("disconnect", () => {
|
|
2185
|
+
clearTimeout(timeout);
|
|
2186
|
+
resolve(undefined);
|
|
2187
|
+
}, { once: true });
|
|
2188
|
+
// Only cancel if reader is still active
|
|
2189
|
+
try {
|
|
1372
2190
|
this._reader.cancel();
|
|
1373
|
-
}
|
|
1374
|
-
|
|
2191
|
+
}
|
|
2192
|
+
catch (err) {
|
|
2193
|
+
this.logger.debug(`Reader cancel error: ${err}`);
|
|
2194
|
+
// Reader already released, resolve immediately
|
|
2195
|
+
clearTimeout(timeout);
|
|
2196
|
+
resolve(undefined);
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
this.connected = false;
|
|
2200
|
+
// Close the port (important for node-usb adapter)
|
|
2201
|
+
try {
|
|
2202
|
+
await this.port.close();
|
|
2203
|
+
this.logger.debug("Port closed successfully");
|
|
1375
2204
|
}
|
|
1376
|
-
|
|
1377
|
-
this.
|
|
2205
|
+
catch (err) {
|
|
2206
|
+
this.logger.debug(`Port close error: ${err}`);
|
|
1378
2207
|
}
|
|
1379
2208
|
}
|
|
1380
2209
|
/**
|
|
@@ -1387,10 +2216,10 @@ export class ESPLoader extends EventTarget {
|
|
|
1387
2216
|
return;
|
|
1388
2217
|
}
|
|
1389
2218
|
try {
|
|
1390
|
-
this._isReconfiguring = true;
|
|
1391
2219
|
this.logger.log("Reconnecting serial port...");
|
|
1392
2220
|
this.connected = false;
|
|
1393
2221
|
this.__inputBuffer = [];
|
|
2222
|
+
this.__inputBufferReadIndex = 0;
|
|
1394
2223
|
// Wait for pending writes to complete
|
|
1395
2224
|
try {
|
|
1396
2225
|
await this._writeChain;
|
|
@@ -1398,6 +2227,8 @@ export class ESPLoader extends EventTarget {
|
|
|
1398
2227
|
catch (err) {
|
|
1399
2228
|
this.logger.debug(`Pending write error during reconnect: ${err}`);
|
|
1400
2229
|
}
|
|
2230
|
+
// Block new writes during port close/open
|
|
2231
|
+
this._isReconfiguring = true;
|
|
1401
2232
|
// Release persistent writer
|
|
1402
2233
|
if (this._writer) {
|
|
1403
2234
|
try {
|
|
@@ -1439,6 +2270,8 @@ export class ESPLoader extends EventTarget {
|
|
|
1439
2270
|
if (!this.port.readable || !this.port.writable) {
|
|
1440
2271
|
throw new Error(`Port streams not available after open (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
|
|
1441
2272
|
}
|
|
2273
|
+
// Port is now open and ready - allow writes for initialization
|
|
2274
|
+
this._isReconfiguring = false;
|
|
1442
2275
|
// Save chip info and flash size (no need to detect again)
|
|
1443
2276
|
const savedChipFamily = this.chipFamily;
|
|
1444
2277
|
const savedChipName = this.chipName;
|
|
@@ -1449,6 +2282,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1449
2282
|
await this.hardReset(true);
|
|
1450
2283
|
if (!this._parent) {
|
|
1451
2284
|
this.__inputBuffer = [];
|
|
2285
|
+
this.__inputBufferReadIndex = 0;
|
|
1452
2286
|
this.__totalBytesRead = 0;
|
|
1453
2287
|
this.readLoop();
|
|
1454
2288
|
}
|
|
@@ -1476,14 +2310,16 @@ export class ESPLoader extends EventTarget {
|
|
|
1476
2310
|
throw new Error(`Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
|
|
1477
2311
|
}
|
|
1478
2312
|
}
|
|
1479
|
-
//
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
2313
|
+
// The stub is now running on the chip
|
|
2314
|
+
// stubLoader has this instance as _parent, so all operations go through this
|
|
2315
|
+
// We just need to mark this instance as running stub code
|
|
2316
|
+
this.IS_STUB = true;
|
|
1483
2317
|
this.logger.debug("Reconnection successful");
|
|
1484
2318
|
}
|
|
1485
|
-
|
|
2319
|
+
catch (err) {
|
|
2320
|
+
// Ensure flag is reset on error
|
|
1486
2321
|
this._isReconfiguring = false;
|
|
2322
|
+
throw err;
|
|
1487
2323
|
}
|
|
1488
2324
|
}
|
|
1489
2325
|
/**
|
|
@@ -1509,8 +2345,8 @@ export class ESPLoader extends EventTarget {
|
|
|
1509
2345
|
const drainStart = Date.now();
|
|
1510
2346
|
const drainTimeout = 100; // Short timeout for draining
|
|
1511
2347
|
while (drained < bytesToDrain && Date.now() - drainStart < drainTimeout) {
|
|
1512
|
-
if (this.
|
|
1513
|
-
const byte = this.
|
|
2348
|
+
if (this._inputBufferAvailable > 0) {
|
|
2349
|
+
const byte = this._readByte();
|
|
1514
2350
|
if (byte !== undefined) {
|
|
1515
2351
|
drained++;
|
|
1516
2352
|
}
|
|
@@ -1526,6 +2362,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1526
2362
|
// Final clear of application buffer
|
|
1527
2363
|
if (!this._parent) {
|
|
1528
2364
|
this.__inputBuffer = [];
|
|
2365
|
+
this.__inputBufferReadIndex = 0;
|
|
1529
2366
|
}
|
|
1530
2367
|
}
|
|
1531
2368
|
/**
|
|
@@ -1537,12 +2374,14 @@ export class ESPLoader extends EventTarget {
|
|
|
1537
2374
|
// Clear application buffer
|
|
1538
2375
|
if (!this._parent) {
|
|
1539
2376
|
this.__inputBuffer = [];
|
|
2377
|
+
this.__inputBufferReadIndex = 0;
|
|
1540
2378
|
}
|
|
1541
2379
|
// Wait for any pending data
|
|
1542
2380
|
await sleep(SYNC_TIMEOUT);
|
|
1543
2381
|
// Final clear
|
|
1544
2382
|
if (!this._parent) {
|
|
1545
2383
|
this.__inputBuffer = [];
|
|
2384
|
+
this.__inputBufferReadIndex = 0;
|
|
1546
2385
|
}
|
|
1547
2386
|
this.logger.debug("Serial buffers flushed");
|
|
1548
2387
|
}
|
|
@@ -1561,7 +2400,35 @@ export class ESPLoader extends EventTarget {
|
|
|
1561
2400
|
// Flush serial buffers before flash read operation
|
|
1562
2401
|
await this.flushSerialBuffers();
|
|
1563
2402
|
this.logger.log(`Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`);
|
|
1564
|
-
|
|
2403
|
+
// Initialize adaptive speed multipliers for WebUSB devices
|
|
2404
|
+
if (this.isWebUSB()) {
|
|
2405
|
+
if (this._isCDCDevice) {
|
|
2406
|
+
// CDC devices (CH343): Start with maximum, adaptive adjustment enabled
|
|
2407
|
+
this._adaptiveBlockMultiplier = 8; // blockSize = 248 bytes
|
|
2408
|
+
this._adaptiveMaxInFlightMultiplier = 8; // maxInFlight = 248 bytes
|
|
2409
|
+
this._consecutiveSuccessfulChunks = 0;
|
|
2410
|
+
this.logger.debug(`CDC device - Initialized: blockMultiplier=${this._adaptiveBlockMultiplier}, maxInFlightMultiplier=${this._adaptiveMaxInFlightMultiplier}`);
|
|
2411
|
+
}
|
|
2412
|
+
else {
|
|
2413
|
+
// Non-CDC devices (CH340, CP2102): Fixed values, no adaptive adjustment
|
|
2414
|
+
this._adaptiveBlockMultiplier = 1; // blockSize = 31 bytes (fixed)
|
|
2415
|
+
this._adaptiveMaxInFlightMultiplier = 1; // maxInFlight = 31 bytes (fixed)
|
|
2416
|
+
this._consecutiveSuccessfulChunks = 0;
|
|
2417
|
+
this.logger.debug(`Non-CDC device - Fixed values: blockSize=31, maxInFlight=31`);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
// Chunk size: Amount of data to request from ESP in one command
|
|
2421
|
+
// For WebUSB (Android), use smaller chunks to avoid timeouts and buffer issues
|
|
2422
|
+
// For Web Serial (Desktop), use larger chunks for better performance
|
|
2423
|
+
let CHUNK_SIZE;
|
|
2424
|
+
if (this.isWebUSB()) {
|
|
2425
|
+
// WebUSB: Use smaller chunks to avoid SLIP timeout issues
|
|
2426
|
+
CHUNK_SIZE = 0x4 * 0x1000; // 4KB = 16384 bytes
|
|
2427
|
+
}
|
|
2428
|
+
else {
|
|
2429
|
+
// Web Serial: Use larger chunks for better performance
|
|
2430
|
+
CHUNK_SIZE = 0x40 * 0x1000;
|
|
2431
|
+
}
|
|
1565
2432
|
let allData = new Uint8Array(0);
|
|
1566
2433
|
let currentAddr = addr;
|
|
1567
2434
|
let remainingSize = size;
|
|
@@ -1569,18 +2436,35 @@ export class ESPLoader extends EventTarget {
|
|
|
1569
2436
|
const chunkSize = Math.min(CHUNK_SIZE, remainingSize);
|
|
1570
2437
|
let chunkSuccess = false;
|
|
1571
2438
|
let retryCount = 0;
|
|
1572
|
-
const MAX_RETRIES =
|
|
2439
|
+
const MAX_RETRIES = 5;
|
|
2440
|
+
let deepRecoveryAttempted = false;
|
|
1573
2441
|
// Retry loop for this chunk
|
|
1574
2442
|
while (!chunkSuccess && retryCount <= MAX_RETRIES) {
|
|
1575
2443
|
let resp = new Uint8Array(0);
|
|
2444
|
+
let lastAckedLength = 0; // Track last acknowledged length
|
|
1576
2445
|
try {
|
|
1577
2446
|
// Only log on first attempt or retries
|
|
1578
2447
|
if (retryCount === 0) {
|
|
1579
2448
|
this.logger.debug(`Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`);
|
|
1580
2449
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
2450
|
+
let blockSize;
|
|
2451
|
+
let maxInFlight;
|
|
2452
|
+
if (this.isWebUSB()) {
|
|
2453
|
+
// WebUSB (Android): All devices use adaptive speed
|
|
2454
|
+
// All have maxTransferSize=64, baseBlockSize=31
|
|
2455
|
+
const maxTransferSize = this.port.maxTransferSize || 64;
|
|
2456
|
+
const baseBlockSize = Math.floor((maxTransferSize - 2) / 2); // 31 bytes
|
|
2457
|
+
// Use current adaptive multipliers (initialized at start of readFlash)
|
|
2458
|
+
blockSize = baseBlockSize * this._adaptiveBlockMultiplier;
|
|
2459
|
+
maxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
|
|
2460
|
+
}
|
|
2461
|
+
else {
|
|
2462
|
+
// Web Serial (Desktop): Use multiples of 63 for consistency
|
|
2463
|
+
const base = 63;
|
|
2464
|
+
blockSize = base * 65; // 63 * 65 = 4095 (close to 0x1000)
|
|
2465
|
+
maxInFlight = base * 130; // 63 * 130 = 8190 (close to blockSize * 2)
|
|
2466
|
+
}
|
|
2467
|
+
const pkt = pack("<IIII", currentAddr, chunkSize, blockSize, maxInFlight);
|
|
1584
2468
|
const [res] = await this.checkCommand(ESP_READ_FLASH, pkt);
|
|
1585
2469
|
if (res != 0) {
|
|
1586
2470
|
throw new Error("Failed to read memory: " + res);
|
|
@@ -1623,10 +2507,19 @@ export class ESPLoader extends EventTarget {
|
|
|
1623
2507
|
newResp.set(resp);
|
|
1624
2508
|
newResp.set(packetData, resp.length);
|
|
1625
2509
|
resp = newResp;
|
|
1626
|
-
// Send acknowledgment
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
2510
|
+
// Send acknowledgment when we've received maxInFlight bytes
|
|
2511
|
+
// The stub sends packets until (num_sent - num_acked) >= max_in_flight
|
|
2512
|
+
// We MUST wait for all packets before sending ACK
|
|
2513
|
+
const shouldAck = resp.length >= chunkSize || // End of chunk
|
|
2514
|
+
resp.length >= lastAckedLength + maxInFlight; // Received all packets
|
|
2515
|
+
if (shouldAck) {
|
|
2516
|
+
const ackData = pack("<I", resp.length);
|
|
2517
|
+
const slipEncodedAck = slipEncode(ackData);
|
|
2518
|
+
await this.writeToStream(slipEncodedAck);
|
|
2519
|
+
// Update lastAckedLength to current response length
|
|
2520
|
+
// This ensures next ACK is sent at the right time
|
|
2521
|
+
lastAckedLength = resp.length;
|
|
2522
|
+
}
|
|
1630
2523
|
}
|
|
1631
2524
|
}
|
|
1632
2525
|
// Chunk read successfully - append to all data
|
|
@@ -1635,9 +2528,62 @@ export class ESPLoader extends EventTarget {
|
|
|
1635
2528
|
newAllData.set(resp, allData.length);
|
|
1636
2529
|
allData = newAllData;
|
|
1637
2530
|
chunkSuccess = true;
|
|
2531
|
+
// ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
|
|
2532
|
+
// Non-CDC devices (CH340, CP2102) stay at fixed blockSize=31, maxInFlight=31
|
|
2533
|
+
if (this.isWebUSB() && this._isCDCDevice && retryCount === 0) {
|
|
2534
|
+
this._consecutiveSuccessfulChunks++;
|
|
2535
|
+
// After 2 consecutive successful chunks, increase speed gradually
|
|
2536
|
+
if (this._consecutiveSuccessfulChunks >= 2) {
|
|
2537
|
+
const maxTransferSize = this.port.maxTransferSize || 64;
|
|
2538
|
+
const baseBlockSize = Math.floor((maxTransferSize - 2) / 2); // 31 bytes
|
|
2539
|
+
// Maximum: blockSize=248 (8 * 31), maxInFlight=248 (8 * 31)
|
|
2540
|
+
const MAX_BLOCK_MULTIPLIER = 8; // 248 bytes - tested stable
|
|
2541
|
+
const MAX_INFLIGHT_MULTIPLIER = 8; // 248 bytes - tested stable
|
|
2542
|
+
let adjusted = false;
|
|
2543
|
+
// Increase blockSize first (up to 248), then maxInFlight
|
|
2544
|
+
if (this._adaptiveBlockMultiplier < MAX_BLOCK_MULTIPLIER) {
|
|
2545
|
+
this._adaptiveBlockMultiplier = Math.min(this._adaptiveBlockMultiplier * 2, MAX_BLOCK_MULTIPLIER);
|
|
2546
|
+
adjusted = true;
|
|
2547
|
+
}
|
|
2548
|
+
// Once blockSize is at maximum, increase maxInFlight
|
|
2549
|
+
else if (this._adaptiveMaxInFlightMultiplier < MAX_INFLIGHT_MULTIPLIER) {
|
|
2550
|
+
this._adaptiveMaxInFlightMultiplier = Math.min(this._adaptiveMaxInFlightMultiplier * 2, MAX_INFLIGHT_MULTIPLIER);
|
|
2551
|
+
adjusted = true;
|
|
2552
|
+
}
|
|
2553
|
+
if (adjusted) {
|
|
2554
|
+
const newBlockSize = baseBlockSize * this._adaptiveBlockMultiplier;
|
|
2555
|
+
const newMaxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
|
|
2556
|
+
this.logger.debug(`Speed increased: blockSize=${newBlockSize}, maxInFlight=${newMaxInFlight}`);
|
|
2557
|
+
this._lastAdaptiveAdjustment = Date.now();
|
|
2558
|
+
}
|
|
2559
|
+
// Reset counter
|
|
2560
|
+
this._consecutiveSuccessfulChunks = 0;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
1638
2563
|
}
|
|
1639
2564
|
catch (err) {
|
|
1640
2565
|
retryCount++;
|
|
2566
|
+
// ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
|
|
2567
|
+
// Non-CDC devices stay at fixed values
|
|
2568
|
+
if (this.isWebUSB() && this._isCDCDevice && retryCount === 1) {
|
|
2569
|
+
// Only reduce if we're above minimum
|
|
2570
|
+
if (this._adaptiveBlockMultiplier > 1 ||
|
|
2571
|
+
this._adaptiveMaxInFlightMultiplier > 1) {
|
|
2572
|
+
// Reduce to minimum on error
|
|
2573
|
+
this._adaptiveBlockMultiplier = 1; // 31 bytes (for CH343)
|
|
2574
|
+
this._adaptiveMaxInFlightMultiplier = 1; // 31 bytes
|
|
2575
|
+
this._consecutiveSuccessfulChunks = 0; // Reset success counter
|
|
2576
|
+
const maxTransferSize = this.port.maxTransferSize || 64;
|
|
2577
|
+
const baseBlockSize = Math.floor((maxTransferSize - 2) / 2);
|
|
2578
|
+
const newBlockSize = baseBlockSize * this._adaptiveBlockMultiplier;
|
|
2579
|
+
const newMaxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
|
|
2580
|
+
this.logger.debug(`Error at higher speed - reduced to minimum: blockSize=${newBlockSize}, maxInFlight=${newMaxInFlight}`);
|
|
2581
|
+
}
|
|
2582
|
+
else {
|
|
2583
|
+
// Already at minimum and still failing - this is a real error
|
|
2584
|
+
this.logger.debug(`Error at minimum speed (blockSize=31, maxInFlight=31) - not a speed issue`);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
1641
2587
|
// Check if it's a timeout error or SLIP error
|
|
1642
2588
|
if (err instanceof SlipReadError) {
|
|
1643
2589
|
if (retryCount <= MAX_RETRIES) {
|
|
@@ -1655,7 +2601,27 @@ export class ESPLoader extends EventTarget {
|
|
|
1655
2601
|
}
|
|
1656
2602
|
}
|
|
1657
2603
|
else {
|
|
1658
|
-
|
|
2604
|
+
// All retries exhausted - attempt recovery by reloading stub
|
|
2605
|
+
// IMPORTANT: Do NOT close port to keep ESP32 in bootloader mode
|
|
2606
|
+
if (!deepRecoveryAttempted) {
|
|
2607
|
+
deepRecoveryAttempted = true;
|
|
2608
|
+
this.logger.log(`All retries exhausted at 0x${currentAddr.toString(16)}. Attempting recovery (close and reopen port)...`);
|
|
2609
|
+
try {
|
|
2610
|
+
// Reconnect will close port, reopen, and reload stub
|
|
2611
|
+
await this.reconnect();
|
|
2612
|
+
this.logger.log("Deep recovery successful. Resuming read from current position...");
|
|
2613
|
+
// Reset retry counter to give it another chance after recovery
|
|
2614
|
+
retryCount = 0;
|
|
2615
|
+
continue;
|
|
2616
|
+
}
|
|
2617
|
+
catch (recoveryErr) {
|
|
2618
|
+
throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery failed: ${recoveryErr}`);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
else {
|
|
2622
|
+
// Recovery already attempted, give up
|
|
2623
|
+
throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery attempt`);
|
|
2624
|
+
}
|
|
1659
2625
|
}
|
|
1660
2626
|
}
|
|
1661
2627
|
else {
|
|
@@ -1672,7 +2638,6 @@ export class ESPLoader extends EventTarget {
|
|
|
1672
2638
|
remainingSize -= chunkSize;
|
|
1673
2639
|
this.logger.debug(`Total progress: 0x${allData.length.toString(16)} from 0x${size.toString(16)} bytes`);
|
|
1674
2640
|
}
|
|
1675
|
-
this.logger.debug(`Successfully read ${allData.length} bytes from flash`);
|
|
1676
2641
|
return allData;
|
|
1677
2642
|
}
|
|
1678
2643
|
}
|
|
@@ -1720,10 +2685,50 @@ class EspStubLoader extends ESPLoader {
|
|
|
1720
2685
|
return [0, []];
|
|
1721
2686
|
}
|
|
1722
2687
|
/**
|
|
1723
|
-
* @name
|
|
1724
|
-
*
|
|
2688
|
+
* @name eraseFlash
|
|
2689
|
+
* Erase entire flash chip
|
|
1725
2690
|
*/
|
|
1726
2691
|
async eraseFlash() {
|
|
1727
2692
|
await this.checkCommand(ESP_ERASE_FLASH, [], 0, CHIP_ERASE_TIMEOUT);
|
|
1728
2693
|
}
|
|
2694
|
+
/**
|
|
2695
|
+
* @name eraseRegion
|
|
2696
|
+
* Erase a specific region of flash
|
|
2697
|
+
*/
|
|
2698
|
+
async eraseRegion(offset, size) {
|
|
2699
|
+
// Validate inputs
|
|
2700
|
+
if (offset < 0) {
|
|
2701
|
+
throw new Error(`Invalid offset: ${offset} (must be non-negative)`);
|
|
2702
|
+
}
|
|
2703
|
+
if (size < 0) {
|
|
2704
|
+
throw new Error(`Invalid size: ${size} (must be non-negative)`);
|
|
2705
|
+
}
|
|
2706
|
+
// No-op for zero size
|
|
2707
|
+
if (size === 0) {
|
|
2708
|
+
this.logger.log("eraseRegion: size is 0, skipping erase");
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
// Check for sector alignment
|
|
2712
|
+
if (offset % FLASH_SECTOR_SIZE !== 0) {
|
|
2713
|
+
throw new Error(`Offset ${offset} (0x${offset.toString(16)}) is not aligned to flash sector size ${FLASH_SECTOR_SIZE} (0x${FLASH_SECTOR_SIZE.toString(16)})`);
|
|
2714
|
+
}
|
|
2715
|
+
if (size % FLASH_SECTOR_SIZE !== 0) {
|
|
2716
|
+
throw new Error(`Size ${size} (0x${size.toString(16)}) is not aligned to flash sector size ${FLASH_SECTOR_SIZE} (0x${FLASH_SECTOR_SIZE.toString(16)})`);
|
|
2717
|
+
}
|
|
2718
|
+
// Check for reasonable bounds (prevent wrapping in pack)
|
|
2719
|
+
const maxValue = 0xffffffff; // 32-bit unsigned max
|
|
2720
|
+
if (offset > maxValue) {
|
|
2721
|
+
throw new Error(`Offset ${offset} exceeds maximum value ${maxValue}`);
|
|
2722
|
+
}
|
|
2723
|
+
if (size > maxValue) {
|
|
2724
|
+
throw new Error(`Size ${size} exceeds maximum value ${maxValue}`);
|
|
2725
|
+
}
|
|
2726
|
+
// Check for wrap-around
|
|
2727
|
+
if (offset + size > maxValue) {
|
|
2728
|
+
throw new Error(`Region end (offset + size = ${offset + size}) exceeds maximum addressable range ${maxValue}`);
|
|
2729
|
+
}
|
|
2730
|
+
const timeout = timeoutPerMb(ERASE_REGION_TIMEOUT_PER_MB, size);
|
|
2731
|
+
const buffer = pack("<II", offset, size);
|
|
2732
|
+
await this.checkCommand(ESP_ERASE_REGION, buffer, 0, timeout);
|
|
2733
|
+
}
|
|
1729
2734
|
}
|