esp32tool 1.1.9 → 1.3.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.
Files changed (64) hide show
  1. package/.nojekyll +0 -0
  2. package/README.md +100 -6
  3. package/apple-touch-icon.png +0 -0
  4. package/build-electron-cli.cjs +177 -0
  5. package/build-single-binary.cjs +295 -0
  6. package/css/light.css +11 -0
  7. package/css/style.css +261 -41
  8. package/dist/cli.d.ts +17 -0
  9. package/dist/cli.js +458 -0
  10. package/dist/console.d.ts +15 -0
  11. package/dist/console.js +237 -0
  12. package/dist/const.d.ts +99 -0
  13. package/dist/const.js +129 -8
  14. package/dist/esp_loader.d.ts +244 -22
  15. package/dist/esp_loader.js +1960 -251
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.js +37 -4
  18. package/dist/node-usb-adapter.d.ts +47 -0
  19. package/dist/node-usb-adapter.js +725 -0
  20. package/dist/stubs/index.d.ts +1 -2
  21. package/dist/stubs/index.js +4 -0
  22. package/dist/util/console-color.d.ts +19 -0
  23. package/dist/util/console-color.js +272 -0
  24. package/dist/util/line-break-transformer.d.ts +5 -0
  25. package/dist/util/line-break-transformer.js +17 -0
  26. package/dist/web/index.js +1 -1
  27. package/electron/cli-main.cjs +74 -0
  28. package/electron/main.cjs +338 -0
  29. package/electron/main.js +7 -2
  30. package/favicon.ico +0 -0
  31. package/fix-cli-imports.cjs +127 -0
  32. package/generate-icons.sh +89 -0
  33. package/icons/icon-128.png +0 -0
  34. package/icons/icon-144.png +0 -0
  35. package/icons/icon-152.png +0 -0
  36. package/icons/icon-192.png +0 -0
  37. package/icons/icon-384.png +0 -0
  38. package/icons/icon-512.png +0 -0
  39. package/icons/icon-72.png +0 -0
  40. package/icons/icon-96.png +0 -0
  41. package/index.html +143 -73
  42. package/install-android.html +411 -0
  43. package/js/console.js +269 -0
  44. package/js/modules/esptool.js +1 -1
  45. package/js/script.js +750 -175
  46. package/js/util/console-color.js +282 -0
  47. package/js/util/line-break-transformer.js +19 -0
  48. package/js/webusb-serial.js +1017 -0
  49. package/license.md +1 -1
  50. package/manifest.json +89 -0
  51. package/package.cli.json +29 -0
  52. package/package.json +35 -24
  53. package/screenshots/desktop.png +0 -0
  54. package/screenshots/mobile.png +0 -0
  55. package/src/cli.ts +618 -0
  56. package/src/console.ts +278 -0
  57. package/src/const.ts +165 -8
  58. package/src/esp_loader.ts +2354 -302
  59. package/src/index.ts +69 -3
  60. package/src/node-usb-adapter.ts +924 -0
  61. package/src/stubs/index.ts +4 -1
  62. package/src/util/console-color.ts +290 -0
  63. package/src/util/line-break-transformer.ts +20 -0
  64. package/sw.js +155 -0
@@ -1,35 +1,158 @@
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, ESP32S2_RTC_CNTL_WDTWPROTECT_REG, ESP32S2_RTC_CNTL_WDTCONFIG0_REG, ESP32S2_RTC_CNTL_WDTCONFIG1_REG, ESP32S2_RTC_CNTL_WDT_WKEY, ESP32S2_GPIO_STRAP_REG, ESP32S2_GPIO_STRAP_SPI_BOOT_MASK, ESP32S2_RTC_CNTL_OPTION1_REG, ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, ESP32S3_RTC_CNTL_WDTWPROTECT_REG, ESP32S3_RTC_CNTL_WDTCONFIG0_REG, ESP32S3_RTC_CNTL_WDTCONFIG1_REG, ESP32S3_RTC_CNTL_WDT_WKEY, ESP32S3_GPIO_STRAP_REG, ESP32S3_GPIO_STRAP_SPI_BOOT_MASK, ESP32S3_RTC_CNTL_OPTION1_REG, ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, ESP32S2_UARTDEV_BUF_NO, ESP32S2_UARTDEV_BUF_NO_USB_OTG, ESP32S3_UARTDEV_BUF_NO, ESP32S3_UARTDEV_BUF_NO_USB_OTG, ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL, ESP32C3_UARTDEV_BUF_NO_USB_JTAG_SERIAL, ESP32C3_BUF_UART_NO_OFFSET, ESP32C3_EFUSE_RD_MAC_SPI_SYS_3_REG, ESP32C3_EFUSE_RD_MAC_SPI_SYS_5_REG, ESP32C3_RTC_CNTL_WDTWPROTECT_REG, ESP32C3_RTC_CNTL_WDTCONFIG0_REG, ESP32C3_RTC_CNTL_WDTCONFIG1_REG, ESP32C3_RTC_CNTL_WDT_WKEY, ESP32C5_C6_RTC_CNTL_WDTWPROTECT_REG, ESP32C5_C6_RTC_CNTL_WDTCONFIG0_REG, ESP32C5_C6_RTC_CNTL_WDTCONFIG1_REG, ESP32C5_C6_RTC_CNTL_WDT_WKEY, ESP32C5_UARTDEV_BUF_NO, ESP32C5_UARTDEV_BUF_NO_USB_JTAG_SERIAL, ESP32C6_UARTDEV_BUF_NO, ESP32C6_UARTDEV_BUF_NO_USB_JTAG_SERIAL, ESP32P4_RTC_CNTL_WDTWPROTECT_REG, ESP32P4_RTC_CNTL_WDTCONFIG0_REG, ESP32P4_RTC_CNTL_WDTCONFIG1_REG, ESP32P4_RTC_CNTL_WDT_WKEY, ESP32P4_UARTDEV_BUF_NO_REV0, ESP32P4_UARTDEV_BUF_NO_REV300, ESP32P4_UARTDEV_BUF_NO_USB_OTG, ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL, ESP32P4_RTC_CNTL_OPTION1_REG, ESP32P4_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, ESP32H2_UARTDEV_BUF_NO, ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL, } from "./const";
3
3
  import { getStubCode } from "./stubs";
4
4
  import { hexFormatter, sleep, slipEncode, toHex } from "./util";
5
- // @ts-expect-error pako ESM module doesn't have proper type definitions
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 {
8
+ /**
9
+ * Check if device is using USB-JTAG or USB-OTG (not external serial chip)
10
+ * Returns undefined if not yet determined
11
+ */
12
+ get isUsbJtagOrOtg() {
13
+ return this._parent ? this._parent._isUsbJtagOrOtg : this._isUsbJtagOrOtg;
14
+ }
9
15
  constructor(port, logger, _parent) {
10
16
  super();
11
17
  this.port = port;
12
18
  this.logger = logger;
13
19
  this._parent = _parent;
14
- this.chipName = null;
15
- this.chipRevision = null;
16
- this.chipVariant = null;
20
+ this.__chipName = null;
21
+ this.__chipRevision = null;
22
+ this.__chipVariant = null;
17
23
  this._efuses = new Array(4).fill(0);
18
24
  this._flashsize = 4 * 1024 * 1024;
19
25
  this.debug = false;
20
26
  this.IS_STUB = false;
21
27
  this.connected = true;
22
28
  this.flashSize = null;
23
- this._currentBaudRate = ESP_ROM_BAUD;
29
+ this.currentBaudRate = ESP_ROM_BAUD;
24
30
  this._isESP32S2NativeUSB = false;
25
31
  this._initializationSucceeded = false;
26
32
  this.__commandLock = Promise.resolve([0, []]);
27
33
  this.__isReconfiguring = false;
34
+ this.__abandonCurrentOperation = false;
35
+ this._suppressDisconnect = false;
36
+ this.__consoleMode = false;
37
+ this._isUsbJtagOrOtg = undefined;
38
+ // Adaptive speed adjustment for flash read operations
39
+ this.__adaptiveBlockMultiplier = 1;
40
+ this.__adaptiveMaxInFlightMultiplier = 1;
41
+ this.__consecutiveSuccessfulChunks = 0;
42
+ this.__lastAdaptiveAdjustment = 0;
43
+ this.__isCDCDevice = false;
28
44
  this.state_DTR = false;
45
+ this.state_RTS = false;
29
46
  this.__writeChain = Promise.resolve();
30
47
  }
48
+ // Chip properties with parent delegation
49
+ // chipFamily accessed before initialization as designed
50
+ get chipFamily() {
51
+ return this._parent ? this._parent.chipFamily : this.__chipFamily;
52
+ }
53
+ set chipFamily(value) {
54
+ if (this._parent) {
55
+ this._parent.chipFamily = value;
56
+ }
57
+ else {
58
+ this.__chipFamily = value;
59
+ }
60
+ }
61
+ get chipName() {
62
+ return this._parent ? this._parent.chipName : this.__chipName;
63
+ }
64
+ set chipName(value) {
65
+ if (this._parent) {
66
+ this._parent.chipName = value;
67
+ }
68
+ else {
69
+ this.__chipName = value;
70
+ }
71
+ }
72
+ get chipRevision() {
73
+ return this._parent ? this._parent.chipRevision : this.__chipRevision;
74
+ }
75
+ set chipRevision(value) {
76
+ if (this._parent) {
77
+ this._parent.chipRevision = value;
78
+ }
79
+ else {
80
+ this.__chipRevision = value;
81
+ }
82
+ }
83
+ get chipVariant() {
84
+ return this._parent ? this._parent.chipVariant : this.__chipVariant;
85
+ }
86
+ set chipVariant(value) {
87
+ if (this._parent) {
88
+ this._parent.chipVariant = value;
89
+ }
90
+ else {
91
+ this.__chipVariant = value;
92
+ }
93
+ }
94
+ // Console mode with parent delegation
95
+ get _consoleMode() {
96
+ return this._parent ? this._parent._consoleMode : this.__consoleMode;
97
+ }
98
+ set _consoleMode(value) {
99
+ if (this._parent) {
100
+ this._parent._consoleMode = value;
101
+ }
102
+ else {
103
+ this.__consoleMode = value;
104
+ }
105
+ }
106
+ // Public setter for console mode (used by script.js)
107
+ setConsoleMode(value) {
108
+ this._consoleMode = value;
109
+ }
31
110
  get _inputBuffer() {
32
- return this._parent ? this._parent._inputBuffer : this.__inputBuffer;
111
+ if (this._parent) {
112
+ return this._parent._inputBuffer;
113
+ }
114
+ if (this.__inputBuffer === undefined) {
115
+ throw new Error("_inputBuffer accessed before initialization");
116
+ }
117
+ return this.__inputBuffer;
118
+ }
119
+ get _inputBufferReadIndex() {
120
+ return this._parent
121
+ ? this._parent._inputBufferReadIndex
122
+ : this.__inputBufferReadIndex || 0;
123
+ }
124
+ set _inputBufferReadIndex(value) {
125
+ if (this._parent) {
126
+ this._parent._inputBufferReadIndex = value;
127
+ }
128
+ else {
129
+ this.__inputBufferReadIndex = value;
130
+ }
131
+ }
132
+ // Get available bytes in buffer (from read index to end)
133
+ get _inputBufferAvailable() {
134
+ return this._inputBuffer.length - this._inputBufferReadIndex;
135
+ }
136
+ // Read one byte from buffer (ring-buffer style with index pointer)
137
+ _readByte() {
138
+ if (this._inputBufferReadIndex >= this._inputBuffer.length) {
139
+ return undefined;
140
+ }
141
+ return this._inputBuffer[this._inputBufferReadIndex++];
142
+ }
143
+ // Clear input buffer and reset read index
144
+ _clearInputBuffer() {
145
+ this._inputBuffer.length = 0;
146
+ this._inputBufferReadIndex = 0;
147
+ }
148
+ // Compact buffer when read index gets too large (prevent memory growth)
149
+ _compactInputBuffer() {
150
+ if (this._inputBufferReadIndex > 1000 &&
151
+ this._inputBufferReadIndex > this._inputBuffer.length / 2) {
152
+ // Remove already-read bytes and reset index
153
+ this._inputBuffer.splice(0, this._inputBufferReadIndex);
154
+ this._inputBufferReadIndex = 0;
155
+ }
33
156
  }
34
157
  get _totalBytesRead() {
35
158
  return this._parent
@@ -68,6 +191,82 @@ export class ESPLoader extends EventTarget {
68
191
  this.__isReconfiguring = value;
69
192
  }
70
193
  }
194
+ get _abandonCurrentOperation() {
195
+ return this._parent
196
+ ? this._parent._abandonCurrentOperation
197
+ : this.__abandonCurrentOperation;
198
+ }
199
+ set _abandonCurrentOperation(value) {
200
+ if (this._parent) {
201
+ this._parent._abandonCurrentOperation = value;
202
+ }
203
+ else {
204
+ this.__abandonCurrentOperation = value;
205
+ }
206
+ }
207
+ get _adaptiveBlockMultiplier() {
208
+ return this._parent
209
+ ? this._parent._adaptiveBlockMultiplier
210
+ : this.__adaptiveBlockMultiplier;
211
+ }
212
+ set _adaptiveBlockMultiplier(value) {
213
+ if (this._parent) {
214
+ this._parent._adaptiveBlockMultiplier = value;
215
+ }
216
+ else {
217
+ this.__adaptiveBlockMultiplier = value;
218
+ }
219
+ }
220
+ get _adaptiveMaxInFlightMultiplier() {
221
+ return this._parent
222
+ ? this._parent._adaptiveMaxInFlightMultiplier
223
+ : this.__adaptiveMaxInFlightMultiplier;
224
+ }
225
+ set _adaptiveMaxInFlightMultiplier(value) {
226
+ if (this._parent) {
227
+ this._parent._adaptiveMaxInFlightMultiplier = value;
228
+ }
229
+ else {
230
+ this.__adaptiveMaxInFlightMultiplier = value;
231
+ }
232
+ }
233
+ get _consecutiveSuccessfulChunks() {
234
+ return this._parent
235
+ ? this._parent._consecutiveSuccessfulChunks
236
+ : this.__consecutiveSuccessfulChunks;
237
+ }
238
+ set _consecutiveSuccessfulChunks(value) {
239
+ if (this._parent) {
240
+ this._parent._consecutiveSuccessfulChunks = value;
241
+ }
242
+ else {
243
+ this.__consecutiveSuccessfulChunks = value;
244
+ }
245
+ }
246
+ get _lastAdaptiveAdjustment() {
247
+ return this._parent
248
+ ? this._parent._lastAdaptiveAdjustment
249
+ : this.__lastAdaptiveAdjustment;
250
+ }
251
+ set _lastAdaptiveAdjustment(value) {
252
+ if (this._parent) {
253
+ this._parent._lastAdaptiveAdjustment = value;
254
+ }
255
+ else {
256
+ this.__lastAdaptiveAdjustment = value;
257
+ }
258
+ }
259
+ get _isCDCDevice() {
260
+ return this._parent ? this._parent._isCDCDevice : this.__isCDCDevice;
261
+ }
262
+ set _isCDCDevice(value) {
263
+ if (this._parent) {
264
+ this._parent._isCDCDevice = value;
265
+ }
266
+ else {
267
+ this.__isCDCDevice = value;
268
+ }
269
+ }
71
270
  detectUSBSerialChip(vendorId, productId) {
72
271
  // Common USB-Serial chip vendors and their products
73
272
  const chips = {
@@ -115,6 +314,7 @@ export class ESPLoader extends EventTarget {
115
314
  async initialize() {
116
315
  if (!this._parent) {
117
316
  this.__inputBuffer = [];
317
+ this.__inputBufferReadIndex = 0;
118
318
  this.__totalBytesRead = 0;
119
319
  // Detect and log USB-Serial chip info
120
320
  const portInfo = this.port.getInfo();
@@ -129,6 +329,12 @@ export class ESPLoader extends EventTarget {
129
329
  if (portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x2) {
130
330
  this._isESP32S2NativeUSB = true;
131
331
  }
332
+ // Detect CDC devices for adaptive speed adjustment
333
+ // Espressif Native USB (VID: 0x303a) or CH343 (VID: 0x1a86, PID: 0x55d3)
334
+ if (portInfo.usbVendorId === 0x303a ||
335
+ (portInfo.usbVendorId === 0x1a86 && portInfo.usbProductId === 0x55d3)) {
336
+ this._isCDCDevice = true;
337
+ }
132
338
  }
133
339
  // Don't await this promise so it doesn't block rest of method.
134
340
  this.readLoop();
@@ -137,6 +343,36 @@ export class ESPLoader extends EventTarget {
137
343
  await this.connectWithResetStrategies();
138
344
  // Detect chip type
139
345
  await this.detectChip();
346
+ // Detect if device is using USB-JTAG/Serial or USB-OTG (not external serial chip)
347
+ // This is needed to determine the correct reset strategy for console mode
348
+ try {
349
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2 ||
350
+ this.chipFamily === CHIP_FAMILY_ESP32S3) {
351
+ const isUsingUsbOtg = await this.usingUsbOtg();
352
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
353
+ this._isUsbJtagOrOtg = isUsingUsbOtg || isUsingUsbJtagSerial;
354
+ }
355
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C3 ||
356
+ this.chipFamily === CHIP_FAMILY_ESP32C5 ||
357
+ this.chipFamily === CHIP_FAMILY_ESP32C6) {
358
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
359
+ this._isUsbJtagOrOtg = isUsingUsbJtagSerial;
360
+ }
361
+ else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
362
+ const isUsingUsbOtg = await this.usingUsbOtg();
363
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
364
+ this._isUsbJtagOrOtg = isUsingUsbOtg || isUsingUsbJtagSerial;
365
+ }
366
+ else {
367
+ // Other chips don't have USB-JTAG/OTG
368
+ this._isUsbJtagOrOtg = false;
369
+ }
370
+ this.logger.debug(`USB connection type: ${this._isUsbJtagOrOtg ? "USB-JTAG/OTG" : "External Serial Chip"}`);
371
+ }
372
+ catch (err) {
373
+ this.logger.debug(`Could not detect USB connection type: ${err}`);
374
+ // Leave as undefined if detection fails
375
+ }
140
376
  // Read the OTP data for this chip and store into this.efuses array
141
377
  const FlAddr = getSpiFlashAddresses(this.getChipFamily());
142
378
  const AddrMAC = FlAddr.macFuse;
@@ -160,7 +396,7 @@ export class ESPLoader extends EventTarget {
160
396
  if (chipInfo) {
161
397
  this.chipName = chipInfo.name;
162
398
  this.chipFamily = chipInfo.family;
163
- // Get chip revision for ESP32-P4
399
+ // Get chip revision for ESP32-P4 and ESP32-C3
164
400
  if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
165
401
  this.chipRevision = await this.getChipRevision();
166
402
  this.logger.debug(`ESP32-P4 revision: ${this.chipRevision}`);
@@ -173,6 +409,10 @@ export class ESPLoader extends EventTarget {
173
409
  }
174
410
  this.logger.debug(`ESP32-P4 variant: ${this.chipVariant}`);
175
411
  }
412
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
413
+ this.chipRevision = await this.getChipRevision();
414
+ this.logger.debug(`ESP32-C3 revision: ${this.chipRevision}`);
415
+ }
176
416
  this.logger.debug(`Detected chip via IMAGE_CHIP_ID: ${chipId} (${this.chipName})`);
177
417
  return;
178
418
  }
@@ -185,7 +425,7 @@ export class ESPLoader extends EventTarget {
185
425
  // This ensures all error responses are cleared before continuing
186
426
  await this.drainInputBuffer(200);
187
427
  // Clear input buffer and re-sync to recover from failed command
188
- this._inputBuffer.length = 0;
428
+ this._clearInputBuffer();
189
429
  await sleep(SYNC_TIMEOUT);
190
430
  // Re-sync with the chip to ensure clean communication
191
431
  try {
@@ -214,24 +454,31 @@ export class ESPLoader extends EventTarget {
214
454
  }
215
455
  this.logger.debug(`ESP32-P4 variant: ${this.chipVariant}`);
216
456
  }
457
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
458
+ this.chipRevision = await this.getChipRevision();
459
+ this.logger.debug(`ESP32-C3 revision: ${this.chipRevision}`);
460
+ }
217
461
  this.logger.debug(`Detected chip via magic value: ${toHex(chipMagicValue >>> 0, 8)} (${this.chipName})`);
218
462
  }
219
463
  /**
220
464
  * Get chip revision for ESP32-P4
221
465
  */
222
466
  async getChipRevision() {
223
- if (this.chipFamily !== CHIP_FAMILY_ESP32P4) {
224
- return 0;
467
+ if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
468
+ // Read from EFUSE_BLOCK1 to get chip revision
469
+ // Word 2 contains revision info for ESP32-P4
470
+ const word2 = await this.readRegister(ESP32P4_EFUSE_BLOCK1_ADDR + 8);
471
+ // Minor revision: bits [3:0]
472
+ const minorRev = word2 & 0x0f;
473
+ // Major revision: bits [23] << 2 | bits [5:4]
474
+ const majorRev = (((word2 >> 23) & 1) << 2) | ((word2 >> 4) & 0x03);
475
+ // Revision is major * 100 + minor
476
+ return majorRev * 100 + minorRev;
477
+ }
478
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
479
+ return await this.getChipRevisionC3();
225
480
  }
226
- // Read from EFUSE_BLOCK1 to get chip revision
227
- // Word 2 contains revision info for ESP32-P4
228
- const word2 = await this.readRegister(ESP32P4_EFUSE_BLOCK1_ADDR + 8);
229
- // Minor revision: bits [3:0]
230
- const minorRev = word2 & 0x0f;
231
- // Major revision: bits [23] << 2 | bits [5:4]
232
- const majorRev = (((word2 >> 23) & 1) << 2) | ((word2 >> 4) & 0x03);
233
- // Revision is major * 100 + minor
234
- return majorRev * 100 + minorRev;
481
+ return 0;
235
482
  }
236
483
  /**
237
484
  * Get security info including chip ID (ESP32-C3 and later)
@@ -262,6 +509,18 @@ export class ESPLoader extends EventTarget {
262
509
  apiVersion,
263
510
  };
264
511
  }
512
+ /**
513
+ * Get MAC address from efuses
514
+ */
515
+ async getMacAddress() {
516
+ if (!this._initializationSucceeded) {
517
+ throw new Error("getMacAddress() requires initialize() to have completed successfully");
518
+ }
519
+ const macBytes = this.macAddr(); // chip-family-aware
520
+ return macBytes
521
+ .map((b) => b.toString(16).padStart(2, "0").toUpperCase())
522
+ .join(":");
523
+ }
265
524
  /**
266
525
  * @name readLoop
267
526
  * Reads data from the input stream and places it in the inputBuffer
@@ -292,7 +551,27 @@ export class ESPLoader extends EventTarget {
292
551
  }
293
552
  }
294
553
  catch {
295
- this.logger.error("Read loop got disconnected");
554
+ // Don't log error if this is an expected disconnect during console mode transition
555
+ if (!this._consoleMode) {
556
+ this.logger.error("Read loop got disconnected");
557
+ }
558
+ }
559
+ finally {
560
+ // Always reset reconfiguring flag when read loop ends
561
+ // This prevents "Cannot write during port reconfiguration" errors
562
+ // when the read loop dies unexpectedly
563
+ this._isReconfiguring = false;
564
+ // Release reader if still locked
565
+ if (this._reader) {
566
+ try {
567
+ this._reader.releaseLock();
568
+ this.logger.debug("Reader released in readLoop cleanup");
569
+ }
570
+ catch (err) {
571
+ this.logger.debug(`Reader release error in readLoop: ${err}`);
572
+ }
573
+ this._reader = undefined;
574
+ }
296
575
  }
297
576
  // Disconnected!
298
577
  this.connected = false;
@@ -304,12 +583,19 @@ export class ESPLoader extends EventTarget {
304
583
  detail: { message: "ESP32-S2 Native USB requires port reselection" },
305
584
  }));
306
585
  }
307
- this.dispatchEvent(new Event("disconnect"));
586
+ // Only dispatch disconnect event if not suppressed
587
+ if (!this._suppressDisconnect) {
588
+ this.dispatchEvent(new Event("disconnect"));
589
+ }
590
+ this._suppressDisconnect = false;
308
591
  this.logger.debug("Finished read loop");
309
592
  }
310
593
  sleep(ms = 100) {
311
594
  return new Promise((resolve) => setTimeout(resolve, ms));
312
595
  }
596
+ // ============================================================================
597
+ // Web Serial (Desktop) - DTR/RTS Signal Handling & Reset Strategies
598
+ // ============================================================================
313
599
  async setRTS(state) {
314
600
  await this.port.setSignals({ requestToSend: state });
315
601
  // Work-around for adapters on Windows using the usbser.sys driver:
@@ -322,24 +608,928 @@ export class ESPLoader extends EventTarget {
322
608
  this.state_DTR = state;
323
609
  await this.port.setSignals({ dataTerminalReady: state });
324
610
  }
611
+ async setDTRandRTS(dtr, rts) {
612
+ this.state_DTR = dtr;
613
+ this.state_RTS = rts;
614
+ await this.port.setSignals({
615
+ dataTerminalReady: dtr,
616
+ requestToSend: rts,
617
+ });
618
+ }
619
+ /**
620
+ * @name hardResetUSBJTAGSerial
621
+ * USB-JTAG/Serial reset for Web Serial (Desktop)
622
+ */
623
+ async hardResetUSBJTAGSerial() {
624
+ await this.setRTS(false);
625
+ await this.setDTR(false); // Idle
626
+ await this.sleep(100);
627
+ await this.setDTR(true); // Set IO0
628
+ await this.setRTS(false);
629
+ await this.sleep(100);
630
+ await this.setRTS(true); // Reset
631
+ await this.setDTR(false);
632
+ await this.setRTS(true);
633
+ await this.sleep(100);
634
+ await this.setDTR(false);
635
+ await this.setRTS(false); // Chip out of reset
636
+ await this.sleep(200);
637
+ }
638
+ /**
639
+ * @name hardResetClassic
640
+ * Classic reset for Web Serial (Desktop) DTR = IO0, RTS = EN
641
+ */
642
+ async hardResetClassic() {
643
+ await this.setDTR(false); // IO0=HIGH
644
+ await this.setRTS(true); // EN=LOW, chip in reset
645
+ await this.sleep(100);
646
+ await this.setDTR(true); // IO0=LOW
647
+ await this.setRTS(false); // EN=HIGH, chip out of reset
648
+ await this.sleep(50);
649
+ await this.setDTR(false); // IO0=HIGH, done
650
+ await this.sleep(200);
651
+ }
652
+ /**
653
+ * Reset to firmware mode (not bootloader) for Web Serial
654
+ * Keeps IO0=HIGH during reset so chip boots into firmware
655
+ */
656
+ async hardResetToFirmware() {
657
+ await this.setDTR(false); // IO0=HIGH
658
+ await this.setRTS(true); // EN=LOW, chip in reset
659
+ await this.sleep(100);
660
+ await this.setRTS(false); // EN=HIGH, chip out of reset (IO0 stays HIGH)
661
+ await this.sleep(50);
662
+ await this.sleep(200);
663
+ }
664
+ /**
665
+ * Reset to firmware mode (not bootloader) for WebUSB
666
+ * Keeps IO0=HIGH during reset so chip boots into firmware
667
+ */
668
+ async hardResetToFirmwareWebUSB() {
669
+ await this.setDTRWebUSB(false); // IO0=HIGH
670
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
671
+ await this.sleep(100);
672
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset (IO0 stays HIGH)
673
+ await this.sleep(50);
674
+ await this.sleep(200);
675
+ }
676
+ /**
677
+ * @name hardResetUnixTight
678
+ * Unix Tight reset for Web Serial (Desktop) - sets DTR and RTS simultaneously
679
+ */
680
+ async hardResetUnixTight() {
681
+ await this.setDTRandRTS(true, true);
682
+ await this.setDTRandRTS(false, false);
683
+ await this.setDTRandRTS(false, true); // IO0=HIGH & EN=LOW, chip in reset
684
+ await this.sleep(100);
685
+ await this.setDTRandRTS(true, false); // IO0=LOW & EN=HIGH, chip out of reset
686
+ await this.sleep(50);
687
+ await this.setDTRandRTS(false, false); // IO0=HIGH, done
688
+ await this.setDTR(false); // Needed in some environments to ensure IO0=HIGH
689
+ await this.sleep(200);
690
+ }
691
+ // ============================================================================
692
+ // WebUSB (Android) - DTR/RTS Signal Handling & Reset Strategies
693
+ // ============================================================================
694
+ async setRTSWebUSB(state) {
695
+ this.state_RTS = state;
696
+ // Always specify both signals to avoid flipping the other line
697
+ // The WebUSB setSignals() now preserves unspecified signals, but being explicit is safer
698
+ await this.port.setSignals({
699
+ requestToSend: state,
700
+ dataTerminalReady: this.state_DTR,
701
+ });
702
+ }
703
+ async setDTRWebUSB(state) {
704
+ this.state_DTR = state;
705
+ // Always specify both signals to avoid flipping the other line
706
+ await this.port.setSignals({
707
+ dataTerminalReady: state,
708
+ requestToSend: this.state_RTS, // Explicitly preserve current RTS state
709
+ });
710
+ }
711
+ async setDTRandRTSWebUSB(dtr, rts) {
712
+ this.state_DTR = dtr;
713
+ this.state_RTS = rts;
714
+ await this.port.setSignals({
715
+ dataTerminalReady: dtr,
716
+ requestToSend: rts,
717
+ });
718
+ }
719
+ /**
720
+ * @name hardResetUSBJTAGSerialWebUSB
721
+ * USB-JTAG/Serial reset for WebUSB (Android)
722
+ */
723
+ async hardResetUSBJTAGSerialWebUSB() {
724
+ await this.setRTSWebUSB(false);
725
+ await this.setDTRWebUSB(false); // Idle
726
+ await this.sleep(100);
727
+ await this.setDTRWebUSB(true); // Set IO0
728
+ await this.setRTSWebUSB(false);
729
+ await this.sleep(100);
730
+ await this.setRTSWebUSB(true); // Reset
731
+ await this.setDTRWebUSB(false);
732
+ await this.setRTSWebUSB(true);
733
+ await this.sleep(100);
734
+ await this.setDTRWebUSB(false);
735
+ await this.setRTSWebUSB(false); // Chip out of reset
736
+ await this.sleep(200);
737
+ }
738
+ /**
739
+ * @name hardResetUSBJTAGSerialInvertedDTRWebUSB
740
+ * USB-JTAG/Serial reset with inverted DTR for WebUSB (Android)
741
+ */
742
+ async hardResetUSBJTAGSerialInvertedDTRWebUSB() {
743
+ await this.setRTSWebUSB(false);
744
+ await this.setDTRWebUSB(true); // Idle (DTR inverted)
745
+ await this.sleep(100);
746
+ await this.setDTRWebUSB(false); // Set IO0 (DTR inverted)
747
+ await this.setRTSWebUSB(false);
748
+ await this.sleep(100);
749
+ await this.setRTSWebUSB(true); // Reset
750
+ await this.setDTRWebUSB(true); // (DTR inverted)
751
+ await this.setRTSWebUSB(true);
752
+ await this.sleep(100);
753
+ await this.setDTRWebUSB(true); // (DTR inverted)
754
+ await this.setRTSWebUSB(false); // Chip out of reset
755
+ await this.sleep(200);
756
+ }
757
+ /**
758
+ * @name hardResetClassicWebUSB
759
+ * Classic reset for WebUSB (Android)
760
+ */
761
+ async hardResetClassicWebUSB() {
762
+ await this.setDTRWebUSB(false); // IO0=HIGH
763
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
764
+ await this.sleep(100);
765
+ await this.setDTRWebUSB(true); // IO0=LOW
766
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
767
+ await this.sleep(50);
768
+ await this.setDTRWebUSB(false); // IO0=HIGH, done
769
+ await this.sleep(200);
770
+ }
771
+ /**
772
+ * @name hardResetUnixTightWebUSB
773
+ * Unix Tight reset for WebUSB (Android) - sets DTR and RTS simultaneously
774
+ */
775
+ async hardResetUnixTightWebUSB() {
776
+ await this.setDTRandRTSWebUSB(false, false);
777
+ await this.setDTRandRTSWebUSB(true, true);
778
+ await this.setDTRandRTSWebUSB(false, true); // IO0=HIGH & EN=LOW, chip in reset
779
+ await this.sleep(100);
780
+ await this.setDTRandRTSWebUSB(true, false); // IO0=LOW & EN=HIGH, chip out of reset
781
+ await this.sleep(50);
782
+ await this.setDTRandRTSWebUSB(false, false); // IO0=HIGH, done
783
+ await this.setDTRWebUSB(false); // Ensure IO0=HIGH
784
+ await this.sleep(200);
785
+ }
786
+ /**
787
+ * @name hardResetClassicLongDelayWebUSB
788
+ * Classic reset with longer delays for WebUSB (Android)
789
+ * Specifically for CP2102/CH340 which may need more time
790
+ */
791
+ async hardResetClassicLongDelayWebUSB() {
792
+ await this.setDTRWebUSB(false); // IO0=HIGH
793
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
794
+ await this.sleep(500); // Extra long delay
795
+ await this.setDTRWebUSB(true); // IO0=LOW
796
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
797
+ await this.sleep(200);
798
+ await this.setDTRWebUSB(false); // IO0=HIGH, done
799
+ await this.sleep(500); // Extra long delay
800
+ }
801
+ /**
802
+ * @name hardResetClassicShortDelayWebUSB
803
+ * Classic reset with shorter delays for WebUSB (Android)
804
+ */
805
+ async hardResetClassicShortDelayWebUSB() {
806
+ await this.setDTRWebUSB(false); // IO0=HIGH
807
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
808
+ await this.sleep(50);
809
+ await this.setDTRWebUSB(true); // IO0=LOW
810
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
811
+ await this.sleep(25);
812
+ await this.setDTRWebUSB(false); // IO0=HIGH, done
813
+ await this.sleep(100);
814
+ }
815
+ /**
816
+ * @name hardResetInvertedWebUSB
817
+ * Inverted reset sequence for WebUSB (Android) - both signals inverted
818
+ */
819
+ async hardResetInvertedWebUSB() {
820
+ await this.setDTRWebUSB(true); // IO0=HIGH (inverted)
821
+ await this.setRTSWebUSB(false); // EN=LOW, chip in reset (inverted)
822
+ await this.sleep(100);
823
+ await this.setDTRWebUSB(false); // IO0=LOW (inverted)
824
+ await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (inverted)
825
+ await this.sleep(50);
826
+ await this.setDTRWebUSB(true); // IO0=HIGH, done (inverted)
827
+ await this.sleep(200);
828
+ }
829
+ /**
830
+ * @name hardResetInvertedDTRWebUSB
831
+ * Only DTR inverted for WebUSB (Android)
832
+ */
833
+ async hardResetInvertedDTRWebUSB() {
834
+ await this.setDTRWebUSB(true); // IO0=HIGH (DTR inverted)
835
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset (RTS normal)
836
+ await this.sleep(100);
837
+ await this.setDTRWebUSB(false); // IO0=LOW (DTR inverted)
838
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset (RTS normal)
839
+ await this.sleep(50);
840
+ await this.setDTRWebUSB(true); // IO0=HIGH, done (DTR inverted)
841
+ await this.sleep(200);
842
+ }
843
+ /**
844
+ * @name hardResetInvertedRTSWebUSB
845
+ * Only RTS inverted for WebUSB (Android)
846
+ */
847
+ async hardResetInvertedRTSWebUSB() {
848
+ await this.setDTRWebUSB(false); // IO0=HIGH (DTR normal)
849
+ await this.setRTSWebUSB(false); // EN=LOW, chip in reset (RTS inverted)
850
+ await this.sleep(100);
851
+ await this.setDTRWebUSB(true); // IO0=LOW (DTR normal)
852
+ await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (RTS inverted)
853
+ await this.sleep(50);
854
+ await this.setDTRWebUSB(false); // IO0=HIGH, done (DTR normal)
855
+ await this.sleep(200);
856
+ }
857
+ /**
858
+ * Check if we're using WebUSB (Android) or Web Serial (Desktop)
859
+ */
860
+ isWebUSB() {
861
+ // WebUSBSerial class has isWebUSB flag - this is the most reliable check
862
+ return this.port.isWebUSB === true;
863
+ }
864
+ /**
865
+ * @name connectWithResetStrategies
866
+ * Try different reset strategies to enter bootloader mode
867
+ * Similar to esptool.py's connect() method with multiple reset strategies
868
+ */
869
+ async connectWithResetStrategies() {
870
+ const portInfo = this.port.getInfo();
871
+ const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
872
+ const isEspressifUSB = portInfo.usbVendorId === 0x303a;
873
+ // this.logger.log(
874
+ // `Detected USB: VID=0x${portInfo.usbVendorId?.toString(16) || "unknown"}, PID=0x${portInfo.usbProductId?.toString(16) || "unknown"}`,
875
+ // );
876
+ // Define reset strategies to try in order
877
+ const resetStrategies = [];
878
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
879
+ const self = this;
880
+ // Detect if this is a USB-Serial chip (needs different sync approach)
881
+ const isUSBSerialChip = !isUSBJTAGSerial && !isEspressifUSB;
882
+ // WebUSB (Android) uses different reset methods than Web Serial (Desktop)
883
+ if (this.isWebUSB()) {
884
+ // For USB-Serial chips (CP2102, CH340, etc.), try inverted strategies first
885
+ // Detect specific chip types once
886
+ const isCP2102 = portInfo.usbVendorId === 0x10c4;
887
+ const isCH34x = portInfo.usbVendorId === 0x1a86;
888
+ // Check for ESP32-S2 Native USB (VID: 0x303a, PID: 0x0002)
889
+ const isESP32S2NativeUSB = portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x0002;
890
+ // WebUSB Strategy 1: USB-JTAG/Serial reset (for Native USB only)
891
+ if (isUSBJTAGSerial || isEspressifUSB) {
892
+ if (isESP32S2NativeUSB) {
893
+ // ESP32-S2 Native USB: Try multiple strategies
894
+ // The device might be in JTAG mode OR CDC mode
895
+ // Strategy 1: USB-JTAG/Serial (works in CDC mode on Desktop)
896
+ resetStrategies.push({
897
+ name: "USB-JTAG/Serial (WebUSB) - ESP32-S2",
898
+ fn: async () => {
899
+ return await self.hardResetUSBJTAGSerialWebUSB();
900
+ },
901
+ });
902
+ // Strategy 2: USB-JTAG/Serial Inverted DTR (works in JTAG mode)
903
+ resetStrategies.push({
904
+ name: "USB-JTAG/Serial Inverted DTR (WebUSB) - ESP32-S2",
905
+ fn: async () => {
906
+ return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
907
+ },
908
+ });
909
+ // Strategy 3: UnixTight (CDC fallback)
910
+ resetStrategies.push({
911
+ name: "UnixTight (WebUSB) - ESP32-S2 CDC",
912
+ fn: async () => {
913
+ return await self.hardResetUnixTightWebUSB();
914
+ },
915
+ });
916
+ // Strategy 4: Classic reset (CDC fallback)
917
+ resetStrategies.push({
918
+ name: "Classic (WebUSB) - ESP32-S2 CDC",
919
+ fn: async () => {
920
+ return await self.hardResetClassicWebUSB();
921
+ },
922
+ });
923
+ }
924
+ else {
925
+ // Other USB-JTAG chips: Try Inverted DTR first - works best for ESP32-H2 and other JTAG chips
926
+ resetStrategies.push({
927
+ name: "USB-JTAG/Serial Inverted DTR (WebUSB)",
928
+ fn: async () => {
929
+ return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
930
+ },
931
+ });
932
+ resetStrategies.push({
933
+ name: "USB-JTAG/Serial (WebUSB)",
934
+ fn: async () => {
935
+ return await self.hardResetUSBJTAGSerialWebUSB();
936
+ },
937
+ });
938
+ resetStrategies.push({
939
+ name: "Inverted DTR Classic (WebUSB)",
940
+ fn: async () => {
941
+ return await self.hardResetInvertedDTRWebUSB();
942
+ },
943
+ });
944
+ }
945
+ }
946
+ // For USB-Serial chips, try inverted strategies first
947
+ if (isUSBSerialChip) {
948
+ if (isCH34x) {
949
+ // CH340/CH343: UnixTight works best (like CP2102)
950
+ resetStrategies.push({
951
+ name: "UnixTight (WebUSB) - CH34x",
952
+ fn: async () => {
953
+ return await self.hardResetUnixTightWebUSB();
954
+ },
955
+ });
956
+ resetStrategies.push({
957
+ name: "Classic (WebUSB) - CH34x",
958
+ fn: async () => {
959
+ return await self.hardResetClassicWebUSB();
960
+ },
961
+ });
962
+ resetStrategies.push({
963
+ name: "Inverted Both (WebUSB) - CH34x",
964
+ fn: async () => {
965
+ return await self.hardResetInvertedWebUSB();
966
+ },
967
+ });
968
+ resetStrategies.push({
969
+ name: "Inverted RTS (WebUSB) - CH34x",
970
+ fn: async () => {
971
+ return await self.hardResetInvertedRTSWebUSB();
972
+ },
973
+ });
974
+ resetStrategies.push({
975
+ name: "Inverted DTR (WebUSB) - CH34x",
976
+ fn: async () => {
977
+ return await self.hardResetInvertedDTRWebUSB();
978
+ },
979
+ });
980
+ }
981
+ else if (isCP2102) {
982
+ // CP2102: UnixTight works best (tested and confirmed)
983
+ // Try it first, then fallback to other strategies
984
+ resetStrategies.push({
985
+ name: "UnixTight (WebUSB) - CP2102",
986
+ fn: async () => {
987
+ return await self.hardResetUnixTightWebUSB();
988
+ },
989
+ });
990
+ resetStrategies.push({
991
+ name: "Classic (WebUSB) - CP2102",
992
+ fn: async () => {
993
+ return await self.hardResetClassicWebUSB();
994
+ },
995
+ });
996
+ resetStrategies.push({
997
+ name: "Inverted Both (WebUSB) - CP2102",
998
+ fn: async () => {
999
+ return await self.hardResetInvertedWebUSB();
1000
+ },
1001
+ });
1002
+ resetStrategies.push({
1003
+ name: "Inverted RTS (WebUSB) - CP2102",
1004
+ fn: async () => {
1005
+ return await self.hardResetInvertedRTSWebUSB();
1006
+ },
1007
+ });
1008
+ resetStrategies.push({
1009
+ name: "Inverted DTR (WebUSB) - CP2102",
1010
+ fn: async () => {
1011
+ return await self.hardResetInvertedDTRWebUSB();
1012
+ },
1013
+ });
1014
+ }
1015
+ else {
1016
+ // For other USB-Serial chips, try UnixTight first, then multiple strategies
1017
+ resetStrategies.push({
1018
+ name: "UnixTight (WebUSB)",
1019
+ fn: async () => {
1020
+ return await self.hardResetUnixTightWebUSB();
1021
+ },
1022
+ });
1023
+ resetStrategies.push({
1024
+ name: "Classic (WebUSB)",
1025
+ fn: async function () {
1026
+ return await self.hardResetClassicWebUSB();
1027
+ },
1028
+ });
1029
+ resetStrategies.push({
1030
+ name: "Inverted Both (WebUSB)",
1031
+ fn: async function () {
1032
+ return await self.hardResetInvertedWebUSB();
1033
+ },
1034
+ });
1035
+ resetStrategies.push({
1036
+ name: "Inverted RTS (WebUSB)",
1037
+ fn: async function () {
1038
+ return await self.hardResetInvertedRTSWebUSB();
1039
+ },
1040
+ });
1041
+ resetStrategies.push({
1042
+ name: "Inverted DTR (WebUSB)",
1043
+ fn: async function () {
1044
+ return await self.hardResetInvertedDTRWebUSB();
1045
+ },
1046
+ });
1047
+ }
1048
+ }
1049
+ // Add general fallback strategies only for non-CP2102 and non-ESP32-S2 Native USB chips
1050
+ if (!isCP2102 && !isESP32S2NativeUSB) {
1051
+ // Classic reset (for chips not handled above)
1052
+ if (portInfo.usbVendorId !== 0x1a86) {
1053
+ resetStrategies.push({
1054
+ name: "Classic (WebUSB)",
1055
+ fn: async function () {
1056
+ return await self.hardResetClassicWebUSB();
1057
+ },
1058
+ });
1059
+ }
1060
+ // UnixTight reset (sets DTR/RTS simultaneously)
1061
+ resetStrategies.push({
1062
+ name: "UnixTight (WebUSB)",
1063
+ fn: async function () {
1064
+ return await self.hardResetUnixTightWebUSB();
1065
+ },
1066
+ });
1067
+ // WebUSB Strategy: Classic with long delays
1068
+ resetStrategies.push({
1069
+ name: "Classic Long Delay (WebUSB)",
1070
+ fn: async function () {
1071
+ return await self.hardResetClassicLongDelayWebUSB();
1072
+ },
1073
+ });
1074
+ // WebUSB Strategy: Classic with short delays
1075
+ resetStrategies.push({
1076
+ name: "Classic Short Delay (WebUSB)",
1077
+ fn: async function () {
1078
+ return await self.hardResetClassicShortDelayWebUSB();
1079
+ },
1080
+ });
1081
+ // WebUSB Strategy: USB-JTAG/Serial fallback
1082
+ if (!isUSBJTAGSerial && !isEspressifUSB) {
1083
+ resetStrategies.push({
1084
+ name: "USB-JTAG/Serial fallback (WebUSB)",
1085
+ fn: async function () {
1086
+ return await self.hardResetUSBJTAGSerialWebUSB();
1087
+ },
1088
+ });
1089
+ }
1090
+ }
1091
+ }
1092
+ else {
1093
+ // Strategy: USB-JTAG/Serial reset
1094
+ if (isUSBJTAGSerial || isEspressifUSB) {
1095
+ resetStrategies.push({
1096
+ name: "USB-JTAG/Serial",
1097
+ fn: async function () {
1098
+ return await self.hardResetUSBJTAGSerial();
1099
+ },
1100
+ });
1101
+ }
1102
+ // Strategy: UnixTight reset
1103
+ resetStrategies.push({
1104
+ name: "UnixTight",
1105
+ fn: async function () {
1106
+ return await self.hardResetUnixTight();
1107
+ },
1108
+ });
1109
+ // Strategy: USB-JTAG/Serial fallback
1110
+ if (!isUSBJTAGSerial && !isEspressifUSB) {
1111
+ resetStrategies.push({
1112
+ name: "USB-JTAG/Serial (fallback)",
1113
+ fn: async function () {
1114
+ return await self.hardResetUSBJTAGSerial();
1115
+ },
1116
+ });
1117
+ }
1118
+ }
1119
+ let lastError = null;
1120
+ // Try each reset strategy with timeout
1121
+ for (const strategy of resetStrategies) {
1122
+ try {
1123
+ // Check if port is still open, if not, skip this strategy
1124
+ if (!this.connected || !this.port.writable) {
1125
+ this.logger.debug(`Port disconnected, skipping ${strategy.name} reset`);
1126
+ continue;
1127
+ }
1128
+ // Clear abandon flag before starting new strategy
1129
+ this._abandonCurrentOperation = false;
1130
+ await strategy.fn();
1131
+ // Try to sync after reset
1132
+ // USB-Serial / native USB chips needs different sync approaches
1133
+ if (isUSBSerialChip) {
1134
+ // USB-Serial chips: Use timeout strategy (2 seconds)
1135
+ // this.logger.log(`USB-Serial chip detected, using sync with timeout.`);
1136
+ const syncSuccess = await this.syncWithTimeout(2000);
1137
+ if (syncSuccess) {
1138
+ // Sync succeeded
1139
+ this.logger.log(`Connected USB Serial successfully with ${strategy.name} reset.`);
1140
+ return;
1141
+ }
1142
+ else {
1143
+ throw new Error("Sync timeout or abandoned");
1144
+ }
1145
+ }
1146
+ else {
1147
+ // Native USB chips
1148
+ // Note: We use Promise.race with sync() directly instead of syncWithTimeout()
1149
+ // because syncWithTimeout causes CDC/JTAG devices to hang for unknown reasons.
1150
+ // The abandon flag in readPacket() prevents overlapping I/O.
1151
+ // this.logger.log(`Native USB chip detected, using CDC/JTAG sync.`);
1152
+ const syncPromise = this.sync();
1153
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Sync timeout")), 1000));
1154
+ try {
1155
+ await Promise.race([syncPromise, timeoutPromise]);
1156
+ // Sync succeeded
1157
+ this.logger.log(`Connected CDC/JTAG successfully with ${strategy.name} reset.`);
1158
+ return;
1159
+ }
1160
+ catch (error) {
1161
+ throw new Error("Sync timeout or abandoned");
1162
+ }
1163
+ }
1164
+ }
1165
+ catch (error) {
1166
+ lastError = error;
1167
+ this.logger.debug(`${strategy.name} reset failed: ${error.message}`);
1168
+ // Set abandon flag to stop any in-flight operations
1169
+ this._abandonCurrentOperation = true;
1170
+ // Wait a bit for in-flight operations to abort
1171
+ await sleep(100);
1172
+ // If port got disconnected, we can't try more strategies
1173
+ if (!this.connected || !this.port.writable) {
1174
+ this.logger.log(`Port disconnected during reset attempt`);
1175
+ break;
1176
+ }
1177
+ // Clear buffers before trying next strategy
1178
+ this._clearInputBuffer();
1179
+ await this.drainInputBuffer(200);
1180
+ await this.flushSerialBuffers();
1181
+ }
1182
+ }
1183
+ // All strategies failed - reset abandon flag before throwing
1184
+ this._abandonCurrentOperation = false;
1185
+ throw new Error(`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
1186
+ }
1187
+ /**
1188
+ * @name watchdogReset
1189
+ * Watchdog reset for ESP32-S2/S3/C3 with USB-OTG or USB-JTAG/Serial
1190
+ * Uses RTC watchdog timer to reset the chip - works when DTR/RTS signals are not available
1191
+ * This is an alias for rtcWdtResetChipSpecific() for backwards compatibility
1192
+ */
1193
+ async watchdogReset() {
1194
+ await this.rtcWdtResetChipSpecific();
1195
+ }
1196
+ /**
1197
+ * Check if current chip is using USB-OTG
1198
+ * Supports ESP32-S2 and ESP32-S3
1199
+ */
1200
+ async usingUsbOtg() {
1201
+ let uartDevBufNo;
1202
+ let usbOtgValue;
1203
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
1204
+ uartDevBufNo = ESP32S2_UARTDEV_BUF_NO;
1205
+ usbOtgValue = ESP32S2_UARTDEV_BUF_NO_USB_OTG;
1206
+ }
1207
+ else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1208
+ uartDevBufNo = ESP32S3_UARTDEV_BUF_NO;
1209
+ usbOtgValue = ESP32S3_UARTDEV_BUF_NO_USB_OTG;
1210
+ }
1211
+ else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1212
+ // P4: UARTDEV_BUF_NO depends on chip revision
1213
+ if (this.chipRevision === null) {
1214
+ this.chipRevision = await this.getChipRevision();
1215
+ }
1216
+ if (this.chipRevision < 300) {
1217
+ uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV0;
1218
+ }
1219
+ else {
1220
+ uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV300;
1221
+ }
1222
+ usbOtgValue = ESP32P4_UARTDEV_BUF_NO_USB_OTG;
1223
+ }
1224
+ else {
1225
+ return false;
1226
+ }
1227
+ const uartNo = (await this.readRegister(uartDevBufNo)) & 0xff;
1228
+ return uartNo === usbOtgValue;
1229
+ }
1230
+ /**
1231
+ * Check if current chip is using USB-JTAG/Serial
1232
+ * Supports ESP32-S3 and ESP32-C3
1233
+ */
1234
+ async usingUsbJtagSerial() {
1235
+ let uartDevBufNo;
1236
+ let usbJtagSerialValue;
1237
+ if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1238
+ uartDevBufNo = ESP32S3_UARTDEV_BUF_NO;
1239
+ usbJtagSerialValue = ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1240
+ }
1241
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
1242
+ // ESP32-C3: BSS_UART_DEV_ADDR depends on chip revision
1243
+ // Revision < 101: 0x3FCDF064
1244
+ // Revision >= 101: 0x3FCDF060
1245
+ let bssUartDevAddr;
1246
+ // Get chip revision if not already set
1247
+ if (this.chipRevision === null) {
1248
+ this.chipRevision = await this.getChipRevisionC3();
1249
+ }
1250
+ if (this.chipRevision < 101) {
1251
+ bssUartDevAddr = 0x3fcdf064;
1252
+ }
1253
+ else {
1254
+ bssUartDevAddr = 0x3fcdf060;
1255
+ }
1256
+ uartDevBufNo = bssUartDevAddr + ESP32C3_BUF_UART_NO_OFFSET;
1257
+ usbJtagSerialValue = ESP32C3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1258
+ }
1259
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C5) {
1260
+ uartDevBufNo = ESP32C5_UARTDEV_BUF_NO;
1261
+ usbJtagSerialValue = ESP32C5_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1262
+ }
1263
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C6) {
1264
+ uartDevBufNo = ESP32C6_UARTDEV_BUF_NO;
1265
+ usbJtagSerialValue = ESP32C6_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1266
+ }
1267
+ else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1268
+ // P4: UARTDEV_BUF_NO depends on chip revision
1269
+ // Revision < 300: 0x4FF3FEC8
1270
+ // Revision >= 300: 0x4FFBFEC8
1271
+ if (this.chipRevision === null) {
1272
+ this.chipRevision = await this.getChipRevision();
1273
+ }
1274
+ if (this.chipRevision < 300) {
1275
+ uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV0;
1276
+ }
1277
+ else {
1278
+ uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV300;
1279
+ }
1280
+ usbJtagSerialValue = ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1281
+ }
1282
+ else if (this.chipFamily === CHIP_FAMILY_ESP32H2) {
1283
+ uartDevBufNo = ESP32H2_UARTDEV_BUF_NO;
1284
+ usbJtagSerialValue = ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1285
+ }
1286
+ else {
1287
+ return false;
1288
+ }
1289
+ const uartNo = (await this.readRegister(uartDevBufNo)) & 0xff;
1290
+ return uartNo === usbJtagSerialValue;
1291
+ }
1292
+ /**
1293
+ * Get chip revision for ESP32-C3
1294
+ * Reads from EFUSE registers and calculates revision
1295
+ */
1296
+ async getChipRevisionC3() {
1297
+ if (this.chipFamily !== CHIP_FAMILY_ESP32C3) {
1298
+ return 0;
1299
+ }
1300
+ // Read EFUSE_RD_MAC_SPI_SYS_3_REG (bits [20:18] = lower 3 bits of revision)
1301
+ const word3 = await this.readRegister(ESP32C3_EFUSE_RD_MAC_SPI_SYS_3_REG);
1302
+ const low = (word3 >> 18) & 0x07;
1303
+ // Read EFUSE_RD_MAC_SPI_SYS_5_REG (bits [25:23] = upper 3 bits of revision)
1304
+ const word5 = await this.readRegister(ESP32C3_EFUSE_RD_MAC_SPI_SYS_5_REG);
1305
+ const hi = (word5 >> 23) & 0x07;
1306
+ // Combine: upper 3 bits from word5, lower 3 bits from word3
1307
+ const revision = (hi << 3) | low;
1308
+ this.logger.debug(`ESP32-C3 revision: ${revision}`);
1309
+ return revision;
1310
+ }
1311
+ /**
1312
+ * RTC watchdog timer reset for ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C5, ESP32-C6, and ESP32-P4
1313
+ * Uses specific registers for each chip family
1314
+ * Note: ESP32-H2 does NOT support WDT reset
1315
+ */
1316
+ async rtcWdtResetChipSpecific() {
1317
+ this.logger.debug("Hard resetting with watchdog timer...");
1318
+ let WDTWPROTECT_REG;
1319
+ let WDTCONFIG0_REG;
1320
+ let WDTCONFIG1_REG;
1321
+ let WDT_WKEY;
1322
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
1323
+ WDTWPROTECT_REG = ESP32S2_RTC_CNTL_WDTWPROTECT_REG;
1324
+ WDTCONFIG0_REG = ESP32S2_RTC_CNTL_WDTCONFIG0_REG;
1325
+ WDTCONFIG1_REG = ESP32S2_RTC_CNTL_WDTCONFIG1_REG;
1326
+ WDT_WKEY = ESP32S2_RTC_CNTL_WDT_WKEY;
1327
+ }
1328
+ else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1329
+ WDTWPROTECT_REG = ESP32S3_RTC_CNTL_WDTWPROTECT_REG;
1330
+ WDTCONFIG0_REG = ESP32S3_RTC_CNTL_WDTCONFIG0_REG;
1331
+ WDTCONFIG1_REG = ESP32S3_RTC_CNTL_WDTCONFIG1_REG;
1332
+ WDT_WKEY = ESP32S3_RTC_CNTL_WDT_WKEY;
1333
+ }
1334
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
1335
+ WDTWPROTECT_REG = ESP32C3_RTC_CNTL_WDTWPROTECT_REG;
1336
+ WDTCONFIG0_REG = ESP32C3_RTC_CNTL_WDTCONFIG0_REG;
1337
+ WDTCONFIG1_REG = ESP32C3_RTC_CNTL_WDTCONFIG1_REG;
1338
+ WDT_WKEY = ESP32C3_RTC_CNTL_WDT_WKEY;
1339
+ }
1340
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C5 ||
1341
+ this.chipFamily === CHIP_FAMILY_ESP32C6) {
1342
+ // C5 and C6 use LP_WDT (Low Power Watchdog Timer)
1343
+ WDTWPROTECT_REG = ESP32C5_C6_RTC_CNTL_WDTWPROTECT_REG;
1344
+ WDTCONFIG0_REG = ESP32C5_C6_RTC_CNTL_WDTCONFIG0_REG;
1345
+ WDTCONFIG1_REG = ESP32C5_C6_RTC_CNTL_WDTCONFIG1_REG;
1346
+ WDT_WKEY = ESP32C5_C6_RTC_CNTL_WDT_WKEY;
1347
+ }
1348
+ else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1349
+ // P4 uses LP_WDT (Low Power Watchdog Timer)
1350
+ WDTWPROTECT_REG = ESP32P4_RTC_CNTL_WDTWPROTECT_REG;
1351
+ WDTCONFIG0_REG = ESP32P4_RTC_CNTL_WDTCONFIG0_REG;
1352
+ WDTCONFIG1_REG = ESP32P4_RTC_CNTL_WDTCONFIG1_REG;
1353
+ WDT_WKEY = ESP32P4_RTC_CNTL_WDT_WKEY;
1354
+ }
1355
+ else {
1356
+ throw new Error(`rtcWdtResetChipSpecific() is not supported for ${this.chipFamily}`);
1357
+ }
1358
+ // Unlock watchdog registers
1359
+ await this.writeRegister(WDTWPROTECT_REG, WDT_WKEY, undefined, 0);
1360
+ // Clear force download boot register (if applicable) BEFORE triggering WDT reset
1361
+ // This ensures the chip boots into firmware mode after reset
1362
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
1363
+ try {
1364
+ await this.writeRegister(ESP32S2_RTC_CNTL_OPTION1_REG, 0, ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, 0);
1365
+ this.logger.debug("Cleared force download boot mask");
1366
+ }
1367
+ catch (err) {
1368
+ this.logger.debug(`Expected error clearing force download boot mask: ${err}`);
1369
+ }
1370
+ }
1371
+ else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1372
+ try {
1373
+ await this.writeRegister(ESP32S3_RTC_CNTL_OPTION1_REG, 0, ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, 0);
1374
+ this.logger.debug("Cleared force download boot mask");
1375
+ }
1376
+ catch (err) {
1377
+ this.logger.debug(`Expected error clearing force download boot mask: ${err}`);
1378
+ }
1379
+ }
1380
+ else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1381
+ try {
1382
+ await this.writeRegister(ESP32P4_RTC_CNTL_OPTION1_REG, 0, ESP32P4_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, 0);
1383
+ this.logger.debug("Cleared force download boot mask");
1384
+ }
1385
+ catch (err) {
1386
+ this.logger.debug(`Expected error clearing force download boot mask: ${err}`);
1387
+ }
1388
+ }
1389
+ // Set WDT timeout to 2000ms (matches Python esptool)
1390
+ await this.writeRegister(WDTCONFIG1_REG, 2000, undefined, 0);
1391
+ // Enable WDT: bit 31 = enable, bits 28-30 = stage, bit 8 = sys reset, bits 0-2 = prescaler
1392
+ const wdtConfig = (1 << 31) | (5 << 28) | (1 << 8) | 2;
1393
+ await this.writeRegister(WDTCONFIG0_REG, wdtConfig, undefined, 0);
1394
+ // Lock watchdog registers
1395
+ await this.writeRegister(WDTWPROTECT_REG, 0, undefined, 0);
1396
+ // Wait for reset to take effect
1397
+ await this.sleep(500);
1398
+ }
1399
+ /**
1400
+ * Helper: Check if USB-based WDT reset should be used for S2/S3
1401
+ * Returns true if WDT reset was performed, false otherwise
1402
+ */
1403
+ async tryUsbWdtReset(chipName, GPIO_STRAP_REG, GPIO_STRAP_SPI_BOOT_MASK, RTC_CNTL_OPTION1_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK) {
1404
+ const isUsingUsbOtg = await this.usingUsbOtg();
1405
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
1406
+ if (isUsingUsbOtg || isUsingUsbJtagSerial) {
1407
+ const strapReg = await this.readRegister(GPIO_STRAP_REG);
1408
+ const forceDlReg = await this.readRegister(RTC_CNTL_OPTION1_REG);
1409
+ // Only use watchdog reset if GPIO0 is low AND force download boot mode is not set
1410
+ if ((strapReg & GPIO_STRAP_SPI_BOOT_MASK) === 0 &&
1411
+ (forceDlReg & RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK) === 0) {
1412
+ await this.rtcWdtResetChipSpecific();
1413
+ this.logger.debug(`${chipName}: RTC WDT reset (USB detected, GPIO0 low)`);
1414
+ return true;
1415
+ }
1416
+ }
1417
+ return false;
1418
+ }
1419
+ /**
1420
+ * Chip-specific hard reset for ESP32-S2
1421
+ * Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
1422
+ */
1423
+ async hardResetS2() {
1424
+ const isUsingUsbOtg = await this.usingUsbOtg();
1425
+ if (isUsingUsbOtg) {
1426
+ await this.rtcWdtResetChipSpecific();
1427
+ this.logger.debug("ESP32-S2: RTC WDT reset (USB-OTG detected)");
1428
+ }
1429
+ else {
1430
+ // Use standard hardware reset
1431
+ await this.hardResetClassic();
1432
+ this.logger.debug("ESP32-S2: Classic reset");
1433
+ }
1434
+ }
1435
+ /**
1436
+ * Chip-specific hard reset for ESP32-S3
1437
+ * Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
1438
+ */
1439
+ async hardResetS3() {
1440
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
1441
+ if (isUsingUsbJtagSerial) {
1442
+ await this.rtcWdtResetChipSpecific();
1443
+ this.logger.debug("ESP32-S3: RTC WDT reset (USB-JTAG/Serial detected)");
1444
+ }
1445
+ else {
1446
+ // Use standard hardware reset
1447
+ await this.hardResetClassic();
1448
+ this.logger.debug("ESP32-S3: Classic reset");
1449
+ }
1450
+ }
1451
+ /**
1452
+ * Chip-specific hard reset for ESP32-C3
1453
+ * Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
1454
+ */
1455
+ async hardResetC3() {
1456
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
1457
+ if (isUsingUsbJtagSerial) {
1458
+ await this.rtcWdtResetChipSpecific();
1459
+ this.logger.debug("ESP32-C3: RTC WDT reset (USB-JTAG/Serial detected)");
1460
+ }
1461
+ else {
1462
+ // Use standard hardware reset
1463
+ await this.hardResetClassic();
1464
+ this.logger.debug("ESP32-C3: Classic reset");
1465
+ }
1466
+ }
325
1467
  async hardReset(bootloader = false) {
1468
+ // In console mode, only allow simple hardware reset (no bootloader entry)
1469
+ if (this._consoleMode) {
1470
+ if (bootloader) {
1471
+ this.logger.debug("Skipping bootloader reset - device is in console mode");
1472
+ return;
1473
+ }
1474
+ // Simple hardware reset to restart firmware (IO0=HIGH)
1475
+ this.logger.debug("Performing hardware reset (console mode)...");
1476
+ if (this.isWebUSB()) {
1477
+ await this.hardResetToFirmwareWebUSB();
1478
+ }
1479
+ else {
1480
+ await this.hardResetToFirmware();
1481
+ }
1482
+ this.logger.debug("Hardware reset complete");
1483
+ return;
1484
+ }
326
1485
  if (bootloader) {
327
1486
  // enter flash mode
328
1487
  if (this.port.getInfo().usbProductId === USB_JTAG_SERIAL_PID) {
329
1488
  await this.hardResetUSBJTAGSerial();
330
- this.logger.log("USB-JTAG/Serial reset.");
1489
+ this.logger.debug("USB-JTAG/Serial reset.");
331
1490
  }
332
1491
  else {
333
- await this.hardResetClassic();
334
- this.logger.log("Classic reset.");
1492
+ // Use different reset strategy for WebUSB (Android) vs Web Serial (Desktop)
1493
+ if (this.isWebUSB()) {
1494
+ await this.hardResetClassicWebUSB();
1495
+ this.logger.debug("Classic reset (WebUSB/Android).");
1496
+ }
1497
+ else {
1498
+ await this.hardResetClassic();
1499
+ this.logger.debug("Classic reset.");
1500
+ }
335
1501
  }
336
1502
  }
337
1503
  else {
338
- // just reset
339
- await this.setRTS(true); // EN->LOW
340
- await this.sleep(100);
341
- await this.setRTS(false);
342
- this.logger.log("Hard reset.");
1504
+ // just reset (no bootloader mode)
1505
+ // For ESP32-S2/S3 with USB-OTG or USB-JTAG/Serial, check if watchdog reset is needed
1506
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2 && !this._consoleMode) {
1507
+ const wdtResetUsed = await this.tryUsbWdtReset("ESP32-S2", ESP32S2_GPIO_STRAP_REG, ESP32S2_GPIO_STRAP_SPI_BOOT_MASK, ESP32S2_RTC_CNTL_OPTION1_REG, ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK);
1508
+ if (wdtResetUsed)
1509
+ return;
1510
+ }
1511
+ else if (this.chipFamily === CHIP_FAMILY_ESP32S3 &&
1512
+ !this._consoleMode) {
1513
+ const wdtResetUsed = await this.tryUsbWdtReset("ESP32-S3", ESP32S3_GPIO_STRAP_REG, ESP32S3_GPIO_STRAP_SPI_BOOT_MASK, ESP32S3_RTC_CNTL_OPTION1_REG, ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK);
1514
+ if (wdtResetUsed)
1515
+ return;
1516
+ }
1517
+ // Standard reset for all other cases
1518
+ if (this.isWebUSB()) {
1519
+ // WebUSB: Use longer delays for better compatibility
1520
+ await this.setRTSWebUSB(true); // EN->LOW
1521
+ await this.sleep(200);
1522
+ await this.setRTSWebUSB(false);
1523
+ await this.sleep(200);
1524
+ this.logger.debug("Hard reset (WebUSB).");
1525
+ }
1526
+ else {
1527
+ // Web Serial: Standard reset
1528
+ await this.setRTS(true); // EN->LOW
1529
+ await this.sleep(100);
1530
+ await this.setRTS(false);
1531
+ this.logger.debug("Hard reset.");
1532
+ }
343
1533
  }
344
1534
  await new Promise((resolve) => setTimeout(resolve, 1000));
345
1535
  }
@@ -464,6 +1654,12 @@ export class ESPLoader extends EventTarget {
464
1654
  else if ([2, 4].includes(data.length)) {
465
1655
  statusLen = data.length;
466
1656
  }
1657
+ else {
1658
+ // Default to 2-byte status if we can't determine
1659
+ // This prevents silent data corruption when statusLen would be 0
1660
+ statusLen = 2;
1661
+ this.logger.debug(`Unknown chip family, defaulting to 2-byte status (opcode: ${toHex(opcode)}, data.length: ${data.length})`);
1662
+ }
467
1663
  }
468
1664
  if (data.length < statusLen) {
469
1665
  throw new Error("Didn't get enough status bytes");
@@ -512,76 +1708,169 @@ export class ESPLoader extends EventTarget {
512
1708
  * @name readPacket
513
1709
  * Generator to read SLIP packets from a serial port.
514
1710
  * Yields one full SLIP packet at a time, raises exception on timeout or invalid data.
1711
+ *
1712
+ * Two implementations:
1713
+ * - Burst: CDC devices (Native USB) and CH343 - very fast processing
1714
+ * - Byte-by-byte: CH340, CP2102, and other USB-Serial adapters - stable fast processing
515
1715
  */
516
1716
  async readPacket(timeout) {
517
1717
  let partialPacket = null;
518
1718
  let inEscape = false;
519
- let readBytes = [];
520
- while (true) {
521
- const stamp = Date.now();
522
- readBytes = [];
523
- while (Date.now() - stamp < timeout) {
524
- if (this._inputBuffer.length > 0) {
525
- readBytes.push(this._inputBuffer.shift());
526
- break;
1719
+ // CDC devices use burst processing, non-CDC use byte-by-byte
1720
+ if (this._isCDCDevice) {
1721
+ // Burst version: Process all available bytes in one pass for ultra-high-speed transfers
1722
+ // Used for: CDC devices (all platforms) and CH343
1723
+ const startTime = Date.now();
1724
+ while (true) {
1725
+ // Check abandon flag (for reset strategy timeout)
1726
+ if (this._abandonCurrentOperation) {
1727
+ throw new SlipReadError("Operation abandoned (reset strategy timeout)");
527
1728
  }
528
- else {
529
- // Reduced sleep time for faster response during high-speed transfers
1729
+ // Check timeout
1730
+ if (Date.now() - startTime > timeout) {
1731
+ const waitingFor = partialPacket === null ? "header" : "content";
1732
+ throw new SlipReadError("Timed out waiting for packet " + waitingFor);
1733
+ }
1734
+ // If no data available, wait a bit
1735
+ if (this._inputBufferAvailable === 0) {
530
1736
  await sleep(1);
1737
+ continue;
531
1738
  }
532
- }
533
- if (readBytes.length == 0) {
534
- const waitingFor = partialPacket === null ? "header" : "content";
535
- throw new SlipReadError("Timed out waiting for packet " + waitingFor);
536
- }
537
- if (this.debug)
538
- this.logger.debug("Read " + readBytes.length + " bytes: " + hexFormatter(readBytes));
539
- for (const b of readBytes) {
540
- if (partialPacket === null) {
541
- // waiting for packet header
542
- if (b == 0xc0) {
543
- partialPacket = [];
1739
+ // Process all available bytes without going back to outer loop
1740
+ // This is critical for handling high-speed burst transfers
1741
+ while (this._inputBufferAvailable > 0) {
1742
+ // Periodic timeout check to prevent hang on slow data
1743
+ if (Date.now() - startTime > timeout) {
1744
+ const waitingFor = partialPacket === null ? "header" : "content";
1745
+ throw new SlipReadError("Timed out waiting for packet " + waitingFor);
544
1746
  }
545
- else {
546
- if (this.debug) {
547
- this.logger.debug("Read invalid data: " + toHex(b));
548
- this.logger.debug("Remaining data in serial buffer: " +
549
- hexFormatter(this._inputBuffer));
1747
+ const b = this._readByte();
1748
+ if (partialPacket === null) {
1749
+ // waiting for packet header
1750
+ if (b == 0xc0) {
1751
+ partialPacket = [];
1752
+ }
1753
+ else {
1754
+ if (this.debug) {
1755
+ this.logger.debug("Read invalid data: " + toHex(b));
1756
+ this.logger.debug("Remaining data in serial buffer: " +
1757
+ hexFormatter(this._inputBuffer));
1758
+ }
1759
+ throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
550
1760
  }
551
- throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
552
1761
  }
553
- }
554
- else if (inEscape) {
555
- // part-way through escape sequence
556
- inEscape = false;
557
- if (b == 0xdc) {
558
- partialPacket.push(0xc0);
1762
+ else if (inEscape) {
1763
+ // part-way through escape sequence
1764
+ inEscape = false;
1765
+ if (b == 0xdc) {
1766
+ partialPacket.push(0xc0);
1767
+ }
1768
+ else if (b == 0xdd) {
1769
+ partialPacket.push(0xdb);
1770
+ }
1771
+ else {
1772
+ if (this.debug) {
1773
+ this.logger.debug("Read invalid data: " + toHex(b));
1774
+ this.logger.debug("Remaining data in serial buffer: " +
1775
+ hexFormatter(this._inputBuffer));
1776
+ }
1777
+ throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
1778
+ }
559
1779
  }
560
- else if (b == 0xdd) {
561
- partialPacket.push(0xdb);
1780
+ else if (b == 0xdb) {
1781
+ // start of escape sequence
1782
+ inEscape = true;
1783
+ }
1784
+ else if (b == 0xc0) {
1785
+ // end of packet
1786
+ if (this.debug)
1787
+ this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
1788
+ // Compact buffer periodically to prevent memory growth
1789
+ this._compactInputBuffer();
1790
+ return partialPacket;
562
1791
  }
563
1792
  else {
564
- if (this.debug) {
565
- this.logger.debug("Read invalid data: " + toHex(b));
566
- this.logger.debug("Remaining data in serial buffer: " +
567
- hexFormatter(this._inputBuffer));
568
- }
569
- throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
1793
+ // normal byte in packet
1794
+ partialPacket.push(b);
570
1795
  }
571
1796
  }
572
- else if (b == 0xdb) {
573
- // start of escape sequence
574
- inEscape = true;
1797
+ }
1798
+ }
1799
+ else {
1800
+ // Byte-by-byte version: Stable for non CDC USB-Serial adapters (CH340, CP2102, etc.)
1801
+ let readBytes = [];
1802
+ while (true) {
1803
+ // Check abandon flag (for reset strategy timeout)
1804
+ if (this._abandonCurrentOperation) {
1805
+ throw new SlipReadError("Operation abandoned (reset strategy timeout)");
575
1806
  }
576
- else if (b == 0xc0) {
577
- // end of packet
578
- if (this.debug)
579
- this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
580
- return partialPacket;
1807
+ const stamp = Date.now();
1808
+ readBytes = [];
1809
+ while (Date.now() - stamp < timeout) {
1810
+ if (this._inputBufferAvailable > 0) {
1811
+ readBytes.push(this._readByte());
1812
+ break;
1813
+ }
1814
+ else {
1815
+ // Reduced sleep time for faster response during high-speed transfers
1816
+ await sleep(1);
1817
+ }
581
1818
  }
582
- else {
583
- // normal byte in packet
584
- partialPacket.push(b);
1819
+ if (readBytes.length == 0) {
1820
+ const waitingFor = partialPacket === null ? "header" : "content";
1821
+ throw new SlipReadError("Timed out waiting for packet " + waitingFor);
1822
+ }
1823
+ if (this.debug)
1824
+ this.logger.debug("Read " + readBytes.length + " bytes: " + hexFormatter(readBytes));
1825
+ for (const b of readBytes) {
1826
+ if (partialPacket === null) {
1827
+ // waiting for packet header
1828
+ if (b == 0xc0) {
1829
+ partialPacket = [];
1830
+ }
1831
+ else {
1832
+ if (this.debug) {
1833
+ this.logger.debug("Read invalid data: " + toHex(b));
1834
+ this.logger.debug("Remaining data in serial buffer: " +
1835
+ hexFormatter(this._inputBuffer));
1836
+ }
1837
+ throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
1838
+ }
1839
+ }
1840
+ else if (inEscape) {
1841
+ // part-way through escape sequence
1842
+ inEscape = false;
1843
+ if (b == 0xdc) {
1844
+ partialPacket.push(0xc0);
1845
+ }
1846
+ else if (b == 0xdd) {
1847
+ partialPacket.push(0xdb);
1848
+ }
1849
+ else {
1850
+ if (this.debug) {
1851
+ this.logger.debug("Read invalid data: " + toHex(b));
1852
+ this.logger.debug("Remaining data in serial buffer: " +
1853
+ hexFormatter(this._inputBuffer));
1854
+ }
1855
+ throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
1856
+ }
1857
+ }
1858
+ else if (b == 0xdb) {
1859
+ // start of escape sequence
1860
+ inEscape = true;
1861
+ }
1862
+ else if (b == 0xc0) {
1863
+ // end of packet
1864
+ if (this.debug)
1865
+ this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
1866
+ // Compact buffer periodically to prevent memory growth
1867
+ this._compactInputBuffer();
1868
+ return partialPacket;
1869
+ }
1870
+ else {
1871
+ // normal byte in packet
1872
+ partialPacket.push(b);
1873
+ }
585
1874
  }
586
1875
  }
587
1876
  }
@@ -613,7 +1902,7 @@ export class ESPLoader extends EventTarget {
613
1902
  throw new Error(`Invalid (unsupported) command ${toHex(opcode)}`);
614
1903
  }
615
1904
  }
616
- throw "Response doesn't match request";
1905
+ throw new Error("Response doesn't match request");
617
1906
  }
618
1907
  /**
619
1908
  * @name checksum
@@ -626,9 +1915,6 @@ export class ESPLoader extends EventTarget {
626
1915
  return state;
627
1916
  }
628
1917
  async setBaudrate(baud) {
629
- if (this.chipFamily == CHIP_FAMILY_ESP8266) {
630
- throw new Error("Changing baud rate is not supported on the ESP8266");
631
- }
632
1918
  try {
633
1919
  // Send ESP_ROM_BAUD(115200) as the old one if running STUB otherwise 0
634
1920
  const buffer = pack("<II", baud, this.IS_STUB ? ESP_ROM_BAUD : 0);
@@ -648,10 +1934,10 @@ export class ESPLoader extends EventTarget {
648
1934
  await sleep(SYNC_TIMEOUT);
649
1935
  // Track current baudrate for reconnect
650
1936
  if (this._parent) {
651
- this._parent._currentBaudRate = baud;
1937
+ this._parent.currentBaudRate = baud;
652
1938
  }
653
1939
  else {
654
- this._currentBaudRate = baud;
1940
+ this.currentBaudRate = baud;
655
1941
  }
656
1942
  // Warn if baudrate exceeds USB-Serial chip capability
657
1943
  const maxBaud = this._parent
@@ -665,6 +1951,8 @@ export class ESPLoader extends EventTarget {
665
1951
  }
666
1952
  async reconfigurePort(baud) {
667
1953
  var _a;
1954
+ // Block new writes during the entire reconfiguration (all paths)
1955
+ this._isReconfiguring = true;
668
1956
  try {
669
1957
  // Wait for pending writes to complete
670
1958
  try {
@@ -673,8 +1961,30 @@ export class ESPLoader extends EventTarget {
673
1961
  catch (err) {
674
1962
  this.logger.debug(`Pending write error during reconfigure: ${err}`);
675
1963
  }
676
- // Block new writes during port close/open
677
- this._isReconfiguring = true;
1964
+ // WebUSB: Check if we should use setBaudRate() or close/reopen
1965
+ if (this.isWebUSB()) {
1966
+ const portInfo = this.port.getInfo();
1967
+ const isCH343 = portInfo.usbVendorId === 0x1a86 && portInfo.usbProductId === 0x55d3;
1968
+ // CH343 is a CDC device and MUST use close/reopen
1969
+ // Other chips (CH340, CP2102, FTDI) MUST use setBaudRate()
1970
+ if (!isCH343 &&
1971
+ typeof this.port.setBaudRate === "function") {
1972
+ // this.logger.log(
1973
+ // `[WebUSB] Changing baudrate to ${baud} using setBaudRate()...`,
1974
+ // );
1975
+ await this.port.setBaudRate(baud);
1976
+ // this.logger.log(`[WebUSB] Baudrate changed to ${baud}`);
1977
+ // Give the chip time to adjust to new baudrate
1978
+ await sleep(100);
1979
+ return;
1980
+ }
1981
+ else if (isCH343) {
1982
+ // this.logger.log(
1983
+ // `[WebUSB] CH343 detected - using close/reopen for baudrate change`,
1984
+ // );
1985
+ }
1986
+ }
1987
+ // Web Serial or CH343: Close and reopen port
678
1988
  // Release persistent writer before closing
679
1989
  if (this._writer) {
680
1990
  try {
@@ -693,120 +2003,54 @@ export class ESPLoader extends EventTarget {
693
2003
  await this.port.close();
694
2004
  // Reopen Port
695
2005
  await this.port.open({ baudRate: baud });
696
- // Port is now open - allow writes again
697
- this._isReconfiguring = false;
698
2006
  // Clear buffer again
699
2007
  await this.flushSerialBuffers();
700
2008
  // Restart Readloop
701
2009
  this.readLoop();
702
2010
  }
703
2011
  catch (e) {
704
- this._isReconfiguring = false;
705
2012
  this.logger.error(`Reconfigure port error: ${e}`);
706
2013
  throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
707
2014
  }
2015
+ finally {
2016
+ // Always reset flag, even on error or early return
2017
+ this._isReconfiguring = false;
2018
+ }
708
2019
  }
709
2020
  /**
710
- * @name connectWithResetStrategies
711
- * Try different reset strategies to enter bootloader mode
712
- * Similar to esptool.py's connect() method with multiple reset strategies
713
- */
714
- async connectWithResetStrategies() {
715
- var _a, _b;
716
- const portInfo = this.port.getInfo();
717
- const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
718
- const isEspressifUSB = portInfo.usbVendorId === 0x303a;
719
- this.logger.log(`Detected USB: VID=0x${((_a = portInfo.usbVendorId) === null || _a === void 0 ? void 0 : _a.toString(16)) || "unknown"}, PID=0x${((_b = portInfo.usbProductId) === null || _b === void 0 ? void 0 : _b.toString(16)) || "unknown"}`);
720
- // Define reset strategies to try in order
721
- const resetStrategies = [];
722
- // Strategy 1: USB-JTAG/Serial reset (for ESP32-C3, C6, S3, etc.)
723
- // Try this first if we detect Espressif USB VID or the specific PID
724
- if (isUSBJTAGSerial || isEspressifUSB) {
725
- resetStrategies.push({
726
- name: "USB-JTAG/Serial",
727
- fn: async () => await this.hardResetUSBJTAGSerial(),
728
- });
729
- }
730
- // Strategy 2: Classic reset (for USB-to-Serial bridges)
731
- resetStrategies.push({
732
- name: "Classic",
733
- fn: async () => await this.hardResetClassic(),
734
- });
735
- // Strategy 3: If USB-JTAG/Serial was not tried yet, try it as fallback
736
- if (!isUSBJTAGSerial && !isEspressifUSB) {
737
- resetStrategies.push({
738
- name: "USB-JTAG/Serial (fallback)",
739
- fn: async () => await this.hardResetUSBJTAGSerial(),
740
- });
741
- }
742
- let lastError = null;
743
- // Try each reset strategy
744
- for (const strategy of resetStrategies) {
2021
+ * @name syncWithTimeout
2022
+ * Sync with timeout that can be abandoned (for reset strategy loop)
2023
+ * This is internally time-bounded and checks the abandon flag
2024
+ */
2025
+ async syncWithTimeout(timeoutMs) {
2026
+ const startTime = Date.now();
2027
+ for (let i = 0; i < 5; i++) {
2028
+ // Check if we've exceeded the timeout
2029
+ if (Date.now() - startTime > timeoutMs) {
2030
+ return false;
2031
+ }
2032
+ // Check abandon flag
2033
+ if (this._abandonCurrentOperation) {
2034
+ return false;
2035
+ }
2036
+ this._clearInputBuffer();
745
2037
  try {
746
- this.logger.log(`Trying ${strategy.name} reset...`);
747
- // Check if port is still open, if not, skip this strategy
748
- if (!this.connected || !this.port.writable) {
749
- this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
750
- continue;
2038
+ const response = await this._sync();
2039
+ if (response) {
2040
+ await sleep(SYNC_TIMEOUT);
2041
+ return true;
751
2042
  }
752
- await strategy.fn();
753
- // Try to sync after reset
754
- await this.sync();
755
- // If we get here, sync succeeded
756
- this.logger.log(`Connected successfully with ${strategy.name} reset.`);
757
- return;
2043
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
758
2044
  }
759
- catch (error) {
760
- lastError = error;
761
- this.logger.log(`${strategy.name} reset failed: ${error.message}`);
762
- // If port got disconnected, we can't try more strategies
763
- if (!this.connected || !this.port.writable) {
764
- this.logger.log(`Port disconnected during reset attempt`);
765
- break;
2045
+ catch (e) {
2046
+ // Check abandon flag after error
2047
+ if (this._abandonCurrentOperation) {
2048
+ return false;
766
2049
  }
767
- // Clear buffers before trying next strategy
768
- this._inputBuffer.length = 0;
769
- await this.drainInputBuffer(200);
770
- await this.flushSerialBuffers();
771
2050
  }
2051
+ await sleep(SYNC_TIMEOUT);
772
2052
  }
773
- // All strategies failed
774
- throw new Error(`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
775
- }
776
- /**
777
- * @name hardResetUSBJTAGSerial
778
- * USB-JTAG/Serial reset sequence for ESP32-C3, ESP32-S3, ESP32-C6, etc.
779
- */
780
- async hardResetUSBJTAGSerial() {
781
- await this.setRTS(false);
782
- await this.setDTR(false); // Idle
783
- await this.sleep(100);
784
- await this.setDTR(true); // Set IO0
785
- await this.setRTS(false);
786
- await this.sleep(100);
787
- await this.setRTS(true); // Reset. Calls inverted to go through (1,1) instead of (0,0)
788
- await this.setDTR(false);
789
- await this.setRTS(true); // RTS set as Windows only propagates DTR on RTS setting
790
- await this.sleep(100);
791
- await this.setDTR(false);
792
- await this.setRTS(false); // Chip out of reset
793
- // Wait for chip to boot into bootloader
794
- await this.sleep(200);
795
- }
796
- /**
797
- * @name hardResetClassic
798
- * Classic reset sequence for USB-to-Serial bridge chips (CH340, CP2102, etc.)
799
- */
800
- async hardResetClassic() {
801
- await this.setDTR(false); // IO0=HIGH
802
- await this.setRTS(true); // EN=LOW, chip in reset
803
- await this.sleep(100);
804
- await this.setDTR(true); // IO0=LOW
805
- await this.setRTS(false); // EN=HIGH, chip out of reset
806
- await this.sleep(50);
807
- await this.setDTR(false); // IO0=HIGH, done
808
- // Wait for chip to boot into bootloader
809
- await this.sleep(200);
2053
+ return false;
810
2054
  }
811
2055
  /**
812
2056
  * @name sync
@@ -815,7 +2059,7 @@ export class ESPLoader extends EventTarget {
815
2059
  */
816
2060
  async sync() {
817
2061
  for (let i = 0; i < 5; i++) {
818
- this._inputBuffer.length = 0;
2062
+ this._clearInputBuffer();
819
2063
  const response = await this._sync();
820
2064
  if (response) {
821
2065
  await sleep(SYNC_TIMEOUT);
@@ -839,8 +2083,10 @@ export class ESPLoader extends EventTarget {
839
2083
  return true;
840
2084
  }
841
2085
  }
842
- catch {
843
- // If read packet fails.
2086
+ catch (e) {
2087
+ if (this.debug) {
2088
+ this.logger.debug(`Sync attempt ${i + 1} failed: ${e}`);
2089
+ }
844
2090
  }
845
2091
  }
846
2092
  return false;
@@ -975,7 +2221,7 @@ export class ESPLoader extends EventTarget {
975
2221
  await this.checkCommand(ESP_SPI_ATTACH, new Array(8).fill(0));
976
2222
  }
977
2223
  const numBlocks = Math.floor((size + flashWriteSize - 1) / flashWriteSize);
978
- if (this.chipFamily == CHIP_FAMILY_ESP8266) {
2224
+ if (this.chipFamily == CHIP_FAMILY_ESP8266 && !this.IS_STUB) {
979
2225
  eraseSize = this.getEraseSize(offset, size);
980
2226
  }
981
2227
  else {
@@ -1315,12 +2561,19 @@ export class ESPLoader extends EventTarget {
1315
2561
  await this._writer.write(new Uint8Array(data));
1316
2562
  }, async () => {
1317
2563
  // Previous write failed, but still attempt this write
2564
+ this.logger.debug("Previous write failed, attempting recovery for current write");
1318
2565
  if (!this.port.writable) {
1319
2566
  throw new Error("Port became unavailable during write");
1320
2567
  }
1321
2568
  // Writer was likely cleaned up by previous error, create new one
1322
2569
  if (!this._writer) {
1323
- this._writer = this.port.writable.getWriter();
2570
+ try {
2571
+ this._writer = this.port.writable.getWriter();
2572
+ }
2573
+ catch (err) {
2574
+ this.logger.debug(`Failed to get writer in recovery: ${err}`);
2575
+ throw new Error("Cannot acquire writer lock");
2576
+ }
1324
2577
  }
1325
2578
  await this._writer.write(new Uint8Array(data));
1326
2579
  })
@@ -1331,7 +2584,7 @@ export class ESPLoader extends EventTarget {
1331
2584
  try {
1332
2585
  this._writer.releaseLock();
1333
2586
  }
1334
- catch (e) {
2587
+ catch {
1335
2588
  // Ignore release errors
1336
2589
  }
1337
2590
  this._writer = undefined;
@@ -1351,51 +2604,239 @@ export class ESPLoader extends EventTarget {
1351
2604
  this.logger.debug("Port already closed, skipping disconnect");
1352
2605
  return;
1353
2606
  }
2607
+ // Wait for pending writes to complete
1354
2608
  try {
1355
- // Wait for pending writes to complete
2609
+ await this._writeChain;
2610
+ }
2611
+ catch (err) {
2612
+ this.logger.debug(`Pending write error during disconnect: ${err}`);
2613
+ }
2614
+ // Release persistent writer before closing
2615
+ if (this._writer) {
1356
2616
  try {
1357
- await this._writeChain;
2617
+ await this._writer.close();
2618
+ this._writer.releaseLock();
1358
2619
  }
1359
2620
  catch (err) {
1360
- this.logger.debug(`Pending write error during disconnect: ${err}`);
2621
+ this.logger.debug(`Writer close/release error: ${err}`);
1361
2622
  }
1362
- // Block new writes during disconnect
1363
- this._isReconfiguring = true;
1364
- // Release persistent writer before closing
1365
- if (this._writer) {
2623
+ this._writer = undefined;
2624
+ }
2625
+ else {
2626
+ // No persistent writer exists, close stream directly
2627
+ // This path is taken when no writes have been queued
2628
+ try {
2629
+ const writer = this.port.writable.getWriter();
2630
+ await writer.close();
2631
+ writer.releaseLock();
2632
+ }
2633
+ catch (err) {
2634
+ this.logger.debug(`Direct writer close error: ${err}`);
2635
+ }
2636
+ }
2637
+ await new Promise((resolve) => {
2638
+ if (!this._reader) {
2639
+ resolve(undefined);
2640
+ return;
2641
+ }
2642
+ // Set a timeout to prevent hanging (important for node-usb)
2643
+ const timeout = setTimeout(() => {
2644
+ this.logger.debug("Disconnect timeout - forcing resolution");
2645
+ resolve(undefined);
2646
+ }, 1000);
2647
+ this.addEventListener("disconnect", () => {
2648
+ clearTimeout(timeout);
2649
+ resolve(undefined);
2650
+ }, { once: true });
2651
+ // Only cancel if reader is still active
2652
+ try {
2653
+ this._reader.cancel();
2654
+ }
2655
+ catch (err) {
2656
+ this.logger.debug(`Reader cancel error: ${err}`);
2657
+ // Reader already released, resolve immediately
2658
+ clearTimeout(timeout);
2659
+ resolve(undefined);
2660
+ }
2661
+ });
2662
+ this.connected = false;
2663
+ // Close the port (important for node-usb adapter)
2664
+ try {
2665
+ await this.port.close();
2666
+ this.logger.debug("Port closed successfully");
2667
+ }
2668
+ catch (err) {
2669
+ this.logger.debug(`Port close error: ${err}`);
2670
+ }
2671
+ }
2672
+ /**
2673
+ * @name releaseReaderWriter
2674
+ * Release reader and writer locks without closing the port
2675
+ * Used when switching to console mode
2676
+ */
2677
+ async releaseReaderWriter() {
2678
+ if (this._parent) {
2679
+ await this._parent.releaseReaderWriter();
2680
+ return;
2681
+ }
2682
+ // Check if device is in JTAG mode and needs reset to boot into firmware
2683
+ const didReconnect = await this._resetToFirmwareIfNeeded();
2684
+ // If we reconnected for console, the reader/writer are already released and restarted
2685
+ if (didReconnect) {
2686
+ return;
2687
+ }
2688
+ // Wait for pending writes to complete
2689
+ try {
2690
+ await this._writeChain;
2691
+ }
2692
+ catch (err) {
2693
+ this.logger.debug(`Pending write error during release: ${err}`);
2694
+ }
2695
+ // Release writer
2696
+ if (this._writer) {
2697
+ try {
2698
+ this._writer.releaseLock();
2699
+ this.logger.debug("Writer released");
2700
+ }
2701
+ catch (err) {
2702
+ this.logger.debug(`Writer release error: ${err}`);
2703
+ }
2704
+ this._writer = undefined;
2705
+ }
2706
+ // Cancel and release reader
2707
+ if (this._reader) {
2708
+ const reader = this._reader;
2709
+ try {
2710
+ // Suppress disconnect event during console mode switching
2711
+ this._suppressDisconnect = true;
2712
+ await reader.cancel();
2713
+ this.logger.debug("Reader cancelled");
2714
+ }
2715
+ catch (err) {
2716
+ this.logger.debug(`Reader cancel error: ${err}`);
2717
+ }
2718
+ finally {
1366
2719
  try {
1367
- await this._writer.close();
1368
- this._writer.releaseLock();
2720
+ reader.releaseLock();
1369
2721
  }
1370
2722
  catch (err) {
1371
- this.logger.debug(`Writer close/release error: ${err}`);
2723
+ this.logger.debug(`Reader release error: ${err}`);
1372
2724
  }
1373
- this._writer = undefined;
1374
2725
  }
1375
- else {
1376
- // No persistent writer exists, close stream directly
1377
- // This path is taken when no writes have been queued
2726
+ if (this._reader === reader) {
2727
+ this._reader = undefined;
2728
+ }
2729
+ }
2730
+ }
2731
+ /**
2732
+ * @name resetToFirmware
2733
+ * Public method to reset device from bootloader to firmware for console mode
2734
+ * Automatically detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
2735
+ * @returns true if reset was performed, false if not needed
2736
+ */
2737
+ async resetToFirmware() {
2738
+ return await this._resetToFirmwareIfNeeded();
2739
+ }
2740
+ /**
2741
+ * @name enterConsoleMode
2742
+ * Prepare device for console mode by resetting to firmware
2743
+ * Handles both USB-JTAG/OTG devices (closes port) and external serial chips (keeps port open)
2744
+ * @returns true if port was closed (USB-JTAG), false if port stays open (serial chip)
2745
+ */
2746
+ async enterConsoleMode() {
2747
+ // Set console mode flag
2748
+ this._consoleMode = true;
2749
+ // Check device type
2750
+ const isUsbJtag = this.isUsbJtagOrOtg === true;
2751
+ if (isUsbJtag) {
2752
+ // USB-JTAG/OTG devices: Use watchdog reset which closes port
2753
+ const wasReset = await this._resetToFirmwareIfNeeded();
2754
+ return wasReset; // true = port closed, caller must reopen
2755
+ }
2756
+ else {
2757
+ // External serial chip devices: Release locks and do simple reset
2758
+ try {
2759
+ await this.releaseReaderWriter();
2760
+ await this.sleep(100);
2761
+ }
2762
+ catch (err) {
2763
+ this.logger.debug(`Failed to release locks: ${err}`);
2764
+ }
2765
+ // Hardware reset to firmware mode (IO0=HIGH)
2766
+ try {
2767
+ await this.hardReset(false);
2768
+ this.logger.log("Device reset to firmware mode");
2769
+ }
2770
+ catch (err) {
2771
+ this.logger.debug(`Could not reset device: ${err}`);
2772
+ }
2773
+ return false; // Port stays open
2774
+ }
2775
+ }
2776
+ /**
2777
+ * @name _resetToFirmwareIfNeeded
2778
+ * Reset device from bootloader to firmware when switching to console mode
2779
+ * Detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
2780
+ * @returns true if reconnect was performed, false otherwise
2781
+ */
2782
+ async _resetToFirmwareIfNeeded() {
2783
+ try {
2784
+ // Check if device is using USB-JTAG/Serial or USB-OTG
2785
+ // Value should already be set during main() connection
2786
+ // Use getter to access parent's value if this is a stub
2787
+ const needsReset = this.isUsbJtagOrOtg === true;
2788
+ if (needsReset) {
2789
+ const resetMethod = this.chipFamily === CHIP_FAMILY_ESP32S2 ||
2790
+ this.chipFamily === CHIP_FAMILY_ESP32S3
2791
+ ? "USB-JTAG/Serial or USB-OTG"
2792
+ : "USB-JTAG/Serial";
2793
+ this.logger.log(`Resetting ${this.chipFamily} (${resetMethod}) to boot into firmware...`);
2794
+ // Set console mode flag before reset to prevent subsequent hardReset calls
2795
+ this._consoleMode = true;
2796
+ // For S2/S3: Clear force download boot mask before WDT reset
2797
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2 ||
2798
+ this.chipFamily === CHIP_FAMILY_ESP32S3) {
2799
+ const OPTION1_REG = this.chipFamily === CHIP_FAMILY_ESP32S2
2800
+ ? ESP32S2_RTC_CNTL_OPTION1_REG
2801
+ : ESP32S3_RTC_CNTL_OPTION1_REG;
2802
+ const FORCE_DOWNLOAD_BOOT_MASK = this.chipFamily === CHIP_FAMILY_ESP32S2
2803
+ ? ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK
2804
+ : ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK;
2805
+ try {
2806
+ // Clear force download boot mode to avoid chip being stuck in download mode
2807
+ await this.writeRegister(OPTION1_REG, 0, FORCE_DOWNLOAD_BOOT_MASK, 0);
2808
+ this.logger.debug("Cleared force download boot mask");
2809
+ }
2810
+ catch (err) {
2811
+ this.logger.debug(`Expected error clearing force download boot mask: ${err}`);
2812
+ }
2813
+ }
2814
+ // Perform watchdog reset to reboot into firmware
1378
2815
  try {
1379
- const writer = this.port.writable.getWriter();
1380
- await writer.close();
1381
- writer.releaseLock();
2816
+ await this.rtcWdtResetChipSpecific();
2817
+ this.logger.debug("Watchdog reset triggered successfully");
1382
2818
  }
1383
2819
  catch (err) {
1384
- this.logger.debug(`Direct writer close error: ${err}`);
2820
+ // Error is expected - device resets before responding
2821
+ this.logger.debug(`Watchdog reset initiated (connection lost as expected: ${err})`);
1385
2822
  }
2823
+ // Wait for device to fully boot into firmware
2824
+ this.logger.log("Waiting for device to boot into firmware...");
2825
+ await this.sleep(1000);
2826
+ // After WDT reset, streams are dead/locked - don't try to manipulate them
2827
+ // Just mark everything as disconnected and let browser clean up
2828
+ this.connected = false;
2829
+ this._writer = undefined;
2830
+ this._reader = undefined;
2831
+ this.logger.debug("Device reset to firmware mode (port closed)");
2832
+ return true;
1386
2833
  }
1387
- await new Promise((resolve) => {
1388
- if (!this._reader) {
1389
- resolve(undefined);
1390
- }
1391
- this.addEventListener("disconnect", resolve, { once: true });
1392
- this._reader.cancel();
1393
- });
1394
- this.connected = false;
1395
2834
  }
1396
- finally {
1397
- this._isReconfiguring = false;
2835
+ catch (err) {
2836
+ this.logger.debug(`Could not reset device to firmware mode: ${err}`);
2837
+ // Continue anyway - console mode might still work
1398
2838
  }
2839
+ return false;
1399
2840
  }
1400
2841
  /**
1401
2842
  * @name reconnectAndResume
@@ -1408,8 +2849,10 @@ export class ESPLoader extends EventTarget {
1408
2849
  }
1409
2850
  try {
1410
2851
  this.logger.log("Reconnecting serial port...");
2852
+ const savedBaudRate = this.currentBaudRate;
1411
2853
  this.connected = false;
1412
2854
  this.__inputBuffer = [];
2855
+ this.__inputBufferReadIndex = 0;
1413
2856
  // Wait for pending writes to complete
1414
2857
  try {
1415
2858
  await this._writeChain;
@@ -1442,7 +2885,7 @@ export class ESPLoader extends EventTarget {
1442
2885
  // Close port
1443
2886
  try {
1444
2887
  await this.port.close();
1445
- this.logger.log("Port closed");
2888
+ this.logger.debug("Port closed");
1446
2889
  }
1447
2890
  catch (err) {
1448
2891
  this.logger.debug(`Port close error: ${err}`);
@@ -1452,6 +2895,7 @@ export class ESPLoader extends EventTarget {
1452
2895
  try {
1453
2896
  await this.port.open({ baudRate: ESP_ROM_BAUD });
1454
2897
  this.connected = true;
2898
+ this.currentBaudRate = ESP_ROM_BAUD;
1455
2899
  }
1456
2900
  catch (err) {
1457
2901
  throw new Error(`Failed to open port: ${err}`);
@@ -1472,6 +2916,7 @@ export class ESPLoader extends EventTarget {
1472
2916
  await this.hardReset(true);
1473
2917
  if (!this._parent) {
1474
2918
  this.__inputBuffer = [];
2919
+ this.__inputBufferReadIndex = 0;
1475
2920
  this.__totalBytesRead = 0;
1476
2921
  this.readLoop();
1477
2922
  }
@@ -1492,17 +2937,17 @@ export class ESPLoader extends EventTarget {
1492
2937
  const stubLoader = await this.runStub(true);
1493
2938
  this.logger.debug("Stub loaded");
1494
2939
  // Restore baudrate if it was changed
1495
- if (this._currentBaudRate !== ESP_ROM_BAUD) {
1496
- await stubLoader.setBaudrate(this._currentBaudRate);
2940
+ if (savedBaudRate !== ESP_ROM_BAUD) {
2941
+ await stubLoader.setBaudrate(savedBaudRate);
1497
2942
  // Verify port is still ready after baudrate change
1498
2943
  if (!this.port.writable || !this.port.readable) {
1499
2944
  throw new Error(`Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
1500
2945
  }
1501
2946
  }
1502
- // Copy stub state to this instance if we're a stub loader
1503
- if (this.IS_STUB) {
1504
- Object.assign(this, stubLoader);
1505
- }
2947
+ // The stub is now running on the chip
2948
+ // stubLoader has this instance as _parent, so all operations go through this
2949
+ // We just need to mark this instance as running stub code
2950
+ this.IS_STUB = true;
1506
2951
  this.logger.debug("Reconnection successful");
1507
2952
  }
1508
2953
  catch (err) {
@@ -1511,6 +2956,103 @@ export class ESPLoader extends EventTarget {
1511
2956
  throw err;
1512
2957
  }
1513
2958
  }
2959
+ /**
2960
+ * @name reconnectToBootloader
2961
+ * Close and reopen the port, then reset ESP to bootloader mode
2962
+ * This is needed after Improv or other operations that leave ESP in firmware mode
2963
+ */
2964
+ async reconnectToBootloader() {
2965
+ if (this._parent) {
2966
+ await this._parent.reconnectToBootloader();
2967
+ return;
2968
+ }
2969
+ try {
2970
+ this.logger.log("Reconnecting to bootloader mode...");
2971
+ // Clear console mode flag when reconnecting to bootloader
2972
+ this._consoleMode = false;
2973
+ this.connected = false;
2974
+ this.__inputBuffer = [];
2975
+ this.__inputBufferReadIndex = 0;
2976
+ // Wait for pending writes to complete
2977
+ try {
2978
+ await this._writeChain;
2979
+ }
2980
+ catch (err) {
2981
+ this.logger.debug(`Pending write error during reconnect: ${err}`);
2982
+ }
2983
+ // Block new writes during port close/open
2984
+ this._isReconfiguring = true;
2985
+ // Release persistent writer
2986
+ if (this._writer) {
2987
+ try {
2988
+ this._writer.releaseLock();
2989
+ }
2990
+ catch (err) {
2991
+ this.logger.debug(`Writer release error during reconnect: ${err}`);
2992
+ }
2993
+ this._writer = undefined;
2994
+ }
2995
+ // Cancel reader
2996
+ if (this._reader) {
2997
+ try {
2998
+ await this._reader.cancel();
2999
+ }
3000
+ catch (err) {
3001
+ this.logger.debug(`Reader cancel error: ${err}`);
3002
+ }
3003
+ this._reader = undefined;
3004
+ }
3005
+ // Close port
3006
+ try {
3007
+ await this.port.close();
3008
+ this.logger.debug("Port closed");
3009
+ }
3010
+ catch (err) {
3011
+ this.logger.debug(`Port close error: ${err}`);
3012
+ }
3013
+ // Open the port
3014
+ this.logger.debug("Opening port...");
3015
+ try {
3016
+ await this.port.open({ baudRate: ESP_ROM_BAUD });
3017
+ this.connected = true;
3018
+ this.currentBaudRate = ESP_ROM_BAUD;
3019
+ }
3020
+ catch (err) {
3021
+ throw new Error(`Failed to open port: ${err}`);
3022
+ }
3023
+ // Verify port streams are available
3024
+ if (!this.port.readable || !this.port.writable) {
3025
+ throw new Error(`Port streams not available after open (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
3026
+ }
3027
+ // Port is now open and ready - allow writes for initialization
3028
+ this._isReconfiguring = false;
3029
+ // Reset chip info and stub state
3030
+ this.__chipFamily = undefined;
3031
+ this.chipName = "Unknown Chip";
3032
+ this.chipRevision = null;
3033
+ this.chipVariant = null;
3034
+ this.IS_STUB = false;
3035
+ // Start read loop
3036
+ if (!this._parent) {
3037
+ this.__inputBuffer = [];
3038
+ this.__inputBufferReadIndex = 0;
3039
+ this.__totalBytesRead = 0;
3040
+ this.readLoop();
3041
+ }
3042
+ // Wait for readLoop to start
3043
+ await sleep(100);
3044
+ // Reset to bootloader mode using multiple strategies
3045
+ await this.connectWithResetStrategies();
3046
+ // Detect chip type
3047
+ await this.detectChip();
3048
+ this.logger.log(`Reconnected to bootloader: ${this.chipName}`);
3049
+ }
3050
+ catch (err) {
3051
+ // Ensure flag is reset on error
3052
+ this._isReconfiguring = false;
3053
+ throw err;
3054
+ }
3055
+ }
1514
3056
  /**
1515
3057
  * @name drainInputBuffer
1516
3058
  * Actively drain the input buffer by reading data for a specified time.
@@ -1534,8 +3076,8 @@ export class ESPLoader extends EventTarget {
1534
3076
  const drainStart = Date.now();
1535
3077
  const drainTimeout = 100; // Short timeout for draining
1536
3078
  while (drained < bytesToDrain && Date.now() - drainStart < drainTimeout) {
1537
- if (this._inputBuffer.length > 0) {
1538
- const byte = this._inputBuffer.shift();
3079
+ if (this._inputBufferAvailable > 0) {
3080
+ const byte = this._readByte();
1539
3081
  if (byte !== undefined) {
1540
3082
  drained++;
1541
3083
  }
@@ -1551,6 +3093,7 @@ export class ESPLoader extends EventTarget {
1551
3093
  // Final clear of application buffer
1552
3094
  if (!this._parent) {
1553
3095
  this.__inputBuffer = [];
3096
+ this.__inputBufferReadIndex = 0;
1554
3097
  }
1555
3098
  }
1556
3099
  /**
@@ -1562,12 +3105,14 @@ export class ESPLoader extends EventTarget {
1562
3105
  // Clear application buffer
1563
3106
  if (!this._parent) {
1564
3107
  this.__inputBuffer = [];
3108
+ this.__inputBufferReadIndex = 0;
1565
3109
  }
1566
3110
  // Wait for any pending data
1567
3111
  await sleep(SYNC_TIMEOUT);
1568
3112
  // Final clear
1569
3113
  if (!this._parent) {
1570
3114
  this.__inputBuffer = [];
3115
+ this.__inputBufferReadIndex = 0;
1571
3116
  }
1572
3117
  this.logger.debug("Serial buffers flushed");
1573
3118
  }
@@ -1577,16 +3122,53 @@ export class ESPLoader extends EventTarget {
1577
3122
  * @param addr - Address to read from
1578
3123
  * @param size - Number of bytes to read
1579
3124
  * @param onPacketReceived - Optional callback function called when packet is received
3125
+ * @param options - Optional parameters for advanced control
3126
+ * - chunkSize: Amount of data to request from ESP in one command (bytes)
3127
+ * - blockSize: Size of each data block sent by ESP (bytes)
3128
+ * - maxInFlight: Maximum unacknowledged bytes (bytes)
1580
3129
  * @returns Uint8Array containing the flash data
1581
3130
  */
1582
- async readFlash(addr, size, onPacketReceived) {
3131
+ async readFlash(addr, size, onPacketReceived, options) {
1583
3132
  if (!this.IS_STUB) {
1584
3133
  throw new Error("Reading flash is only supported in stub mode. Please run runStub() first.");
1585
3134
  }
1586
3135
  // Flush serial buffers before flash read operation
1587
3136
  await this.flushSerialBuffers();
1588
3137
  this.logger.log(`Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`);
1589
- const CHUNK_SIZE = 0x10000; // 64KB chunks
3138
+ // Initialize adaptive speed multipliers for WebUSB devices
3139
+ if (this.isWebUSB()) {
3140
+ if (this._isCDCDevice) {
3141
+ // CDC devices (CH343): Start with maximum, adaptive adjustment enabled
3142
+ this._adaptiveBlockMultiplier = 8; // blockSize = 248 bytes
3143
+ this._adaptiveMaxInFlightMultiplier = 8; // maxInFlight = 248 bytes
3144
+ this._consecutiveSuccessfulChunks = 0;
3145
+ this.logger.debug(`CDC device - Initialized: blockMultiplier=${this._adaptiveBlockMultiplier}, maxInFlightMultiplier=${this._adaptiveMaxInFlightMultiplier}`);
3146
+ }
3147
+ else {
3148
+ // Non-CDC devices (CH340, CP2102): Fixed values, no adaptive adjustment
3149
+ this._adaptiveBlockMultiplier = 1; // blockSize = 31 bytes (fixed)
3150
+ this._adaptiveMaxInFlightMultiplier = 1; // maxInFlight = 31 bytes (fixed)
3151
+ this._consecutiveSuccessfulChunks = 0;
3152
+ this.logger.debug(`Non-CDC device - Fixed values: blockSize=31, maxInFlight=31`);
3153
+ }
3154
+ }
3155
+ // Chunk size: Amount of data to request from ESP in one command
3156
+ // For WebUSB (Android), use smaller chunks to avoid timeouts and buffer issues
3157
+ // For Web Serial (Desktop), use larger chunks for better performance
3158
+ let CHUNK_SIZE;
3159
+ if ((options === null || options === void 0 ? void 0 : options.chunkSize) !== undefined) {
3160
+ // Use user-provided chunkSize if in advanced mode
3161
+ CHUNK_SIZE = options.chunkSize;
3162
+ this.logger.log(`Using custom chunk size: 0x${CHUNK_SIZE.toString(16)} bytes`);
3163
+ }
3164
+ else if (this.isWebUSB()) {
3165
+ // WebUSB: Use smaller chunks to avoid SLIP timeout issues
3166
+ CHUNK_SIZE = 0x4 * 0x1000; // 4KB = 16384 bytes
3167
+ }
3168
+ else {
3169
+ // Web Serial: Use larger chunks for better performance
3170
+ CHUNK_SIZE = 0x40 * 0x1000;
3171
+ }
1590
3172
  let allData = new Uint8Array(0);
1591
3173
  let currentAddr = addr;
1592
3174
  let remainingSize = size;
@@ -1599,14 +3181,39 @@ export class ESPLoader extends EventTarget {
1599
3181
  // Retry loop for this chunk
1600
3182
  while (!chunkSuccess && retryCount <= MAX_RETRIES) {
1601
3183
  let resp = new Uint8Array(0);
3184
+ let lastAckedLength = 0; // Track last acknowledged length
1602
3185
  try {
1603
3186
  // Only log on first attempt or retries
1604
3187
  if (retryCount === 0) {
1605
3188
  this.logger.debug(`Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`);
1606
3189
  }
1607
- // Send read flash command for this chunk
1608
- // This must be inside the retry loop so we send a fresh command after errors
1609
- const pkt = pack("<IIII", currentAddr, chunkSize, 0x1000, 1024);
3190
+ let blockSize;
3191
+ let maxInFlight;
3192
+ if ((options === null || options === void 0 ? void 0 : options.blockSize) !== undefined &&
3193
+ (options === null || options === void 0 ? void 0 : options.maxInFlight) !== undefined) {
3194
+ // Use user-provided values if in advanced mode
3195
+ blockSize = options.blockSize;
3196
+ maxInFlight = options.maxInFlight;
3197
+ if (retryCount === 0) {
3198
+ this.logger.debug(`Using custom parameters: blockSize=${blockSize}, maxInFlight=${maxInFlight}`);
3199
+ }
3200
+ }
3201
+ else if (this.isWebUSB()) {
3202
+ // WebUSB (Android): All devices use adaptive speed
3203
+ // All have maxTransferSize=64, baseBlockSize=31
3204
+ const maxTransferSize = this.port.maxTransferSize || 64;
3205
+ const baseBlockSize = Math.floor((maxTransferSize - 2) / 2); // 31 bytes
3206
+ // Use current adaptive multipliers (initialized at start of readFlash)
3207
+ blockSize = baseBlockSize * this._adaptiveBlockMultiplier;
3208
+ maxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
3209
+ }
3210
+ else {
3211
+ // Web Serial (Desktop): Use multiples of 63 for consistency
3212
+ const base = 63;
3213
+ blockSize = base * 65; // 63 * 65 = 4095 (close to 0x1000)
3214
+ maxInFlight = base * 130; // 63 * 130 = 8190 (close to blockSize * 2)
3215
+ }
3216
+ const pkt = pack("<IIII", currentAddr, chunkSize, blockSize, maxInFlight);
1610
3217
  const [res] = await this.checkCommand(ESP_READ_FLASH, pkt);
1611
3218
  if (res != 0) {
1612
3219
  throw new Error("Failed to read memory: " + res);
@@ -1649,10 +3256,19 @@ export class ESPLoader extends EventTarget {
1649
3256
  newResp.set(resp);
1650
3257
  newResp.set(packetData, resp.length);
1651
3258
  resp = newResp;
1652
- // Send acknowledgment
1653
- const ackData = pack("<I", resp.length);
1654
- const slipEncodedAck = slipEncode(ackData);
1655
- await this.writeToStream(slipEncodedAck);
3259
+ // Send acknowledgment when we've received maxInFlight bytes
3260
+ // The stub sends packets until (num_sent - num_acked) >= max_in_flight
3261
+ // We MUST wait for all packets before sending ACK
3262
+ const shouldAck = resp.length >= chunkSize || // End of chunk
3263
+ resp.length >= lastAckedLength + maxInFlight; // Received all packets
3264
+ if (shouldAck) {
3265
+ const ackData = pack("<I", resp.length);
3266
+ const slipEncodedAck = slipEncode(ackData);
3267
+ await this.writeToStream(slipEncodedAck);
3268
+ // Update lastAckedLength to current response length
3269
+ // This ensures next ACK is sent at the right time
3270
+ lastAckedLength = resp.length;
3271
+ }
1656
3272
  }
1657
3273
  }
1658
3274
  // Chunk read successfully - append to all data
@@ -1661,13 +3277,66 @@ export class ESPLoader extends EventTarget {
1661
3277
  newAllData.set(resp, allData.length);
1662
3278
  allData = newAllData;
1663
3279
  chunkSuccess = true;
3280
+ // ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
3281
+ // Non-CDC devices (CH340, CP2102) stay at fixed blockSize=31, maxInFlight=31
3282
+ if (this.isWebUSB() && this._isCDCDevice && retryCount === 0) {
3283
+ this._consecutiveSuccessfulChunks++;
3284
+ // After 2 consecutive successful chunks, increase speed gradually
3285
+ if (this._consecutiveSuccessfulChunks >= 2) {
3286
+ const maxTransferSize = this.port.maxTransferSize || 64;
3287
+ const baseBlockSize = Math.floor((maxTransferSize - 2) / 2); // 31 bytes
3288
+ // Maximum: blockSize=248 (8 * 31), maxInFlight=248 (8 * 31)
3289
+ const MAX_BLOCK_MULTIPLIER = 8; // 248 bytes - tested stable
3290
+ const MAX_INFLIGHT_MULTIPLIER = 8; // 248 bytes - tested stable
3291
+ let adjusted = false;
3292
+ // Increase blockSize first (up to 248), then maxInFlight
3293
+ if (this._adaptiveBlockMultiplier < MAX_BLOCK_MULTIPLIER) {
3294
+ this._adaptiveBlockMultiplier = Math.min(this._adaptiveBlockMultiplier * 2, MAX_BLOCK_MULTIPLIER);
3295
+ adjusted = true;
3296
+ }
3297
+ // Once blockSize is at maximum, increase maxInFlight
3298
+ else if (this._adaptiveMaxInFlightMultiplier < MAX_INFLIGHT_MULTIPLIER) {
3299
+ this._adaptiveMaxInFlightMultiplier = Math.min(this._adaptiveMaxInFlightMultiplier * 2, MAX_INFLIGHT_MULTIPLIER);
3300
+ adjusted = true;
3301
+ }
3302
+ if (adjusted) {
3303
+ const newBlockSize = baseBlockSize * this._adaptiveBlockMultiplier;
3304
+ const newMaxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
3305
+ this.logger.debug(`Speed increased: blockSize=${newBlockSize}, maxInFlight=${newMaxInFlight}`);
3306
+ this._lastAdaptiveAdjustment = Date.now();
3307
+ }
3308
+ // Reset counter
3309
+ this._consecutiveSuccessfulChunks = 0;
3310
+ }
3311
+ }
1664
3312
  }
1665
3313
  catch (err) {
1666
3314
  retryCount++;
3315
+ // ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
3316
+ // Non-CDC devices stay at fixed values
3317
+ if (this.isWebUSB() && this._isCDCDevice && retryCount === 1) {
3318
+ // Only reduce if we're above minimum
3319
+ if (this._adaptiveBlockMultiplier > 1 ||
3320
+ this._adaptiveMaxInFlightMultiplier > 1) {
3321
+ // Reduce to minimum on error
3322
+ this._adaptiveBlockMultiplier = 1; // 31 bytes (for CH343)
3323
+ this._adaptiveMaxInFlightMultiplier = 1; // 31 bytes
3324
+ this._consecutiveSuccessfulChunks = 0; // Reset success counter
3325
+ const maxTransferSize = this.port.maxTransferSize || 64;
3326
+ const baseBlockSize = Math.floor((maxTransferSize - 2) / 2);
3327
+ const newBlockSize = baseBlockSize * this._adaptiveBlockMultiplier;
3328
+ const newMaxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
3329
+ this.logger.debug(`Error at higher speed - reduced to minimum: blockSize=${newBlockSize}, maxInFlight=${newMaxInFlight}`);
3330
+ }
3331
+ else {
3332
+ // Already at minimum and still failing - this is a real error
3333
+ this.logger.debug(`Error at minimum speed (blockSize=31, maxInFlight=31) - not a speed issue`);
3334
+ }
3335
+ }
1667
3336
  // Check if it's a timeout error or SLIP error
1668
3337
  if (err instanceof SlipReadError) {
1669
3338
  if (retryCount <= MAX_RETRIES) {
1670
- this.logger.log(`${err.message} at 0x${currentAddr.toString(16)}. Draining buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`);
3339
+ this.logger.debug(`${err.message} at 0x${currentAddr.toString(16)}. Draining buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`);
1671
3340
  try {
1672
3341
  await this.drainInputBuffer(200);
1673
3342
  // Clear application buffer
@@ -1681,10 +3350,11 @@ export class ESPLoader extends EventTarget {
1681
3350
  }
1682
3351
  }
1683
3352
  else {
1684
- // All retries exhausted - attempt deep recovery by reconnecting and reloading stub
3353
+ // All retries exhausted - attempt recovery by reloading stub
3354
+ // IMPORTANT: Do NOT close port to keep ESP32 in bootloader mode
1685
3355
  if (!deepRecoveryAttempted) {
1686
3356
  deepRecoveryAttempted = true;
1687
- this.logger.log(`All retries exhausted at 0x${currentAddr.toString(16)}. Attempting deep recovery (reconnect + reload stub)...`);
3357
+ this.logger.log(`All retries exhausted at 0x${currentAddr.toString(16)}. Attempting recovery (close and reopen port)...`);
1688
3358
  try {
1689
3359
  // Reconnect will close port, reopen, and reload stub
1690
3360
  await this.reconnect();
@@ -1693,13 +3363,13 @@ export class ESPLoader extends EventTarget {
1693
3363
  retryCount = 0;
1694
3364
  continue;
1695
3365
  }
1696
- catch (reconnectErr) {
1697
- throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and deep recovery failed: ${reconnectErr}`);
3366
+ catch (recoveryErr) {
3367
+ throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery failed: ${recoveryErr}`);
1698
3368
  }
1699
3369
  }
1700
3370
  else {
1701
- // Deep recovery already attempted, give up
1702
- throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and deep recovery attempt`);
3371
+ // Recovery already attempted, give up
3372
+ throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery attempt`);
1703
3373
  }
1704
3374
  }
1705
3375
  }
@@ -1717,7 +3387,6 @@ export class ESPLoader extends EventTarget {
1717
3387
  remainingSize -= chunkSize;
1718
3388
  this.logger.debug(`Total progress: 0x${allData.length.toString(16)} from 0x${size.toString(16)} bytes`);
1719
3389
  }
1720
- this.logger.debug(`Successfully read ${allData.length} bytes from flash`);
1721
3390
  return allData;
1722
3391
  }
1723
3392
  }
@@ -1765,10 +3434,50 @@ class EspStubLoader extends ESPLoader {
1765
3434
  return [0, []];
1766
3435
  }
1767
3436
  /**
1768
- * @name getEraseSize
1769
- * depending on flash chip model the erase may take this long (maybe longer!)
3437
+ * @name eraseFlash
3438
+ * Erase entire flash chip
1770
3439
  */
1771
3440
  async eraseFlash() {
1772
3441
  await this.checkCommand(ESP_ERASE_FLASH, [], 0, CHIP_ERASE_TIMEOUT);
1773
3442
  }
3443
+ /**
3444
+ * @name eraseRegion
3445
+ * Erase a specific region of flash
3446
+ */
3447
+ async eraseRegion(offset, size) {
3448
+ // Validate inputs
3449
+ if (offset < 0) {
3450
+ throw new Error(`Invalid offset: ${offset} (must be non-negative)`);
3451
+ }
3452
+ if (size < 0) {
3453
+ throw new Error(`Invalid size: ${size} (must be non-negative)`);
3454
+ }
3455
+ // No-op for zero size
3456
+ if (size === 0) {
3457
+ this.logger.log("eraseRegion: size is 0, skipping erase");
3458
+ return;
3459
+ }
3460
+ // Check for sector alignment
3461
+ if (offset % FLASH_SECTOR_SIZE !== 0) {
3462
+ 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)})`);
3463
+ }
3464
+ if (size % FLASH_SECTOR_SIZE !== 0) {
3465
+ 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)})`);
3466
+ }
3467
+ // Check for reasonable bounds (prevent wrapping in pack)
3468
+ const maxValue = 0xffffffff; // 32-bit unsigned max
3469
+ if (offset > maxValue) {
3470
+ throw new Error(`Offset ${offset} exceeds maximum value ${maxValue}`);
3471
+ }
3472
+ if (size > maxValue) {
3473
+ throw new Error(`Size ${size} exceeds maximum value ${maxValue}`);
3474
+ }
3475
+ // Check for wrap-around
3476
+ if (offset + size > maxValue) {
3477
+ throw new Error(`Region end (offset + size = ${offset + size}) exceeds maximum addressable range ${maxValue}`);
3478
+ }
3479
+ const timeout = timeoutPerMb(ERASE_REGION_TIMEOUT_PER_MB, size);
3480
+ const buffer = pack("<II", offset, size);
3481
+ await this.checkCommand(ESP_ERASE_REGION, buffer, 0, timeout);
3482
+ }
1774
3483
  }