tasmota-webserial-esptool 9.2.8 → 9.2.9

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.
@@ -1,10 +1,17 @@
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_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, } 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
5
  import { deflate } from "pako";
6
6
  import { pack, unpack } from "./struct";
7
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
+ }
8
15
  constructor(port, logger, _parent) {
9
16
  super();
10
17
  this.port = port;
@@ -25,6 +32,9 @@ export class ESPLoader extends EventTarget {
25
32
  this.__commandLock = Promise.resolve([0, []]);
26
33
  this.__isReconfiguring = false;
27
34
  this.__abandonCurrentOperation = false;
35
+ this._suppressDisconnect = false;
36
+ this.__consoleMode = false;
37
+ this._isUsbJtagOrOtg = undefined;
28
38
  // Adaptive speed adjustment for flash read operations
29
39
  this.__adaptiveBlockMultiplier = 1;
30
40
  this.__adaptiveMaxInFlightMultiplier = 1;
@@ -81,6 +91,22 @@ export class ESPLoader extends EventTarget {
81
91
  this.__chipVariant = value;
82
92
  }
83
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
+ }
84
110
  get _inputBuffer() {
85
111
  if (this._parent) {
86
112
  return this._parent._inputBuffer;
@@ -317,6 +343,16 @@ export class ESPLoader extends EventTarget {
317
343
  await this.connectWithResetStrategies();
318
344
  // Detect chip type
319
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
+ this._isUsbJtagOrOtg = await this.detectUsbConnectionType();
350
+ this.logger.debug(`USB connection type: ${this._isUsbJtagOrOtg ? "USB-JTAG/OTG" : "External Serial Chip"}`);
351
+ }
352
+ catch (err) {
353
+ this.logger.debug(`Could not detect USB connection type: ${err}`);
354
+ // Leave as undefined if detection fails
355
+ }
320
356
  // Read the OTP data for this chip and store into this.efuses array
321
357
  const FlAddr = getSpiFlashAddresses(this.getChipFamily());
322
358
  const AddrMAC = FlAddr.macFuse;
@@ -340,7 +376,7 @@ export class ESPLoader extends EventTarget {
340
376
  if (chipInfo) {
341
377
  this.chipName = chipInfo.name;
342
378
  this.chipFamily = chipInfo.family;
343
- // Get chip revision for ESP32-P4
379
+ // Get chip revision for ESP32-P4 and ESP32-C3
344
380
  if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
345
381
  this.chipRevision = await this.getChipRevision();
346
382
  this.logger.debug(`ESP32-P4 revision: ${this.chipRevision}`);
@@ -353,6 +389,10 @@ export class ESPLoader extends EventTarget {
353
389
  }
354
390
  this.logger.debug(`ESP32-P4 variant: ${this.chipVariant}`);
355
391
  }
392
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
393
+ this.chipRevision = await this.getChipRevision();
394
+ this.logger.debug(`ESP32-C3 revision: ${this.chipRevision}`);
395
+ }
356
396
  this.logger.debug(`Detected chip via IMAGE_CHIP_ID: ${chipId} (${this.chipName})`);
357
397
  return;
358
398
  }
@@ -385,7 +425,6 @@ export class ESPLoader extends EventTarget {
385
425
  this.chipFamily = chip.family;
386
426
  if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
387
427
  this.chipRevision = await this.getChipRevision();
388
- this.logger.debug(`ESP32-P4 revision: ${this.chipRevision}`);
389
428
  if (this.chipRevision >= 300) {
390
429
  this.chipVariant = "rev300";
391
430
  }
@@ -394,24 +433,30 @@ export class ESPLoader extends EventTarget {
394
433
  }
395
434
  this.logger.debug(`ESP32-P4 variant: ${this.chipVariant}`);
396
435
  }
436
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
437
+ this.chipRevision = await this.getChipRevision();
438
+ }
397
439
  this.logger.debug(`Detected chip via magic value: ${toHex(chipMagicValue >>> 0, 8)} (${this.chipName})`);
398
440
  }
399
441
  /**
400
442
  * Get chip revision for ESP32-P4
401
443
  */
402
444
  async getChipRevision() {
403
- if (this.chipFamily !== CHIP_FAMILY_ESP32P4) {
404
- return 0;
445
+ if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
446
+ // Read from EFUSE_BLOCK1 to get chip revision
447
+ // Word 2 contains revision info for ESP32-P4
448
+ const word2 = await this.readRegister(ESP32P4_EFUSE_BLOCK1_ADDR + 8);
449
+ // Minor revision: bits [3:0]
450
+ const minorRev = word2 & 0x0f;
451
+ // Major revision: bits [23] << 2 | bits [5:4]
452
+ const majorRev = (((word2 >> 23) & 1) << 2) | ((word2 >> 4) & 0x03);
453
+ // Revision is major * 100 + minor
454
+ return majorRev * 100 + minorRev;
405
455
  }
406
- // Read from EFUSE_BLOCK1 to get chip revision
407
- // Word 2 contains revision info for ESP32-P4
408
- const word2 = await this.readRegister(ESP32P4_EFUSE_BLOCK1_ADDR + 8);
409
- // Minor revision: bits [3:0]
410
- const minorRev = word2 & 0x0f;
411
- // Major revision: bits [23] << 2 | bits [5:4]
412
- const majorRev = (((word2 >> 23) & 1) << 2) | ((word2 >> 4) & 0x03);
413
- // Revision is major * 100 + minor
414
- return majorRev * 100 + minorRev;
456
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
457
+ return await this.getChipRevisionC3();
458
+ }
459
+ return 0;
415
460
  }
416
461
  /**
417
462
  * Get security info including chip ID (ESP32-C3 and later)
@@ -484,13 +529,27 @@ export class ESPLoader extends EventTarget {
484
529
  }
485
530
  }
486
531
  catch {
487
- this.logger.error("Read loop got disconnected");
532
+ // Don't log error if this is an expected disconnect during console mode transition
533
+ if (!this._consoleMode) {
534
+ this.logger.error("Read loop got disconnected");
535
+ }
488
536
  }
489
537
  finally {
490
538
  // Always reset reconfiguring flag when read loop ends
491
539
  // This prevents "Cannot write during port reconfiguration" errors
492
540
  // when the read loop dies unexpectedly
493
541
  this._isReconfiguring = false;
542
+ // Release reader if still locked
543
+ if (this._reader) {
544
+ try {
545
+ this._reader.releaseLock();
546
+ this.logger.debug("Reader released in readLoop cleanup");
547
+ }
548
+ catch (err) {
549
+ this.logger.debug(`Reader release error in readLoop: ${err}`);
550
+ }
551
+ this._reader = undefined;
552
+ }
494
553
  }
495
554
  // Disconnected!
496
555
  this.connected = false;
@@ -502,7 +561,11 @@ export class ESPLoader extends EventTarget {
502
561
  detail: { message: "ESP32-S2 Native USB requires port reselection" },
503
562
  }));
504
563
  }
505
- this.dispatchEvent(new Event("disconnect"));
564
+ // Only dispatch disconnect event if not suppressed
565
+ if (!this._suppressDisconnect) {
566
+ this.dispatchEvent(new Event("disconnect"));
567
+ }
568
+ this._suppressDisconnect = false;
506
569
  this.logger.debug("Finished read loop");
507
570
  }
508
571
  sleep(ms = 100) {
@@ -564,6 +627,30 @@ export class ESPLoader extends EventTarget {
564
627
  await this.setDTR(false); // IO0=HIGH, done
565
628
  await this.sleep(200);
566
629
  }
630
+ /**
631
+ * Reset to firmware mode (not bootloader) for Web Serial
632
+ * Keeps IO0=HIGH during reset so chip boots into firmware
633
+ */
634
+ async hardResetToFirmware() {
635
+ await this.setDTR(false); // IO0=HIGH
636
+ await this.setRTS(true); // EN=LOW, chip in reset
637
+ await this.sleep(100);
638
+ await this.setRTS(false); // EN=HIGH, chip out of reset (IO0 stays HIGH)
639
+ await this.sleep(50);
640
+ await this.sleep(200);
641
+ }
642
+ /**
643
+ * Reset to firmware mode (not bootloader) for WebUSB
644
+ * Keeps IO0=HIGH during reset so chip boots into firmware
645
+ */
646
+ async hardResetToFirmwareWebUSB() {
647
+ await this.setDTRWebUSB(false); // IO0=HIGH
648
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
649
+ await this.sleep(100);
650
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset (IO0 stays HIGH)
651
+ await this.sleep(50);
652
+ await this.sleep(200);
653
+ }
567
654
  /**
568
655
  * @name hardResetUnixTight
569
656
  * Unix Tight reset for Web Serial (Desktop) - sets DTR and RTS simultaneously
@@ -1013,7 +1100,7 @@ export class ESPLoader extends EventTarget {
1013
1100
  try {
1014
1101
  // Check if port is still open, if not, skip this strategy
1015
1102
  if (!this.connected || !this.port.writable) {
1016
- this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
1103
+ this.logger.debug(`Port disconnected, skipping ${strategy.name} reset`);
1017
1104
  continue;
1018
1105
  }
1019
1106
  // Clear abandon flag before starting new strategy
@@ -1055,7 +1142,7 @@ export class ESPLoader extends EventTarget {
1055
1142
  }
1056
1143
  catch (error) {
1057
1144
  lastError = error;
1058
- this.logger.log(`${strategy.name} reset failed: ${error.message}`);
1145
+ this.logger.debug(`${strategy.name} reset failed: ${error.message}`);
1059
1146
  // Set abandon flag to stop any in-flight operations
1060
1147
  this._abandonCurrentOperation = true;
1061
1148
  // Wait a bit for in-flight operations to abort
@@ -1077,12 +1164,134 @@ export class ESPLoader extends EventTarget {
1077
1164
  }
1078
1165
  /**
1079
1166
  * @name watchdogReset
1080
- * Watchdog reset for ESP32-S2/S3 with USB-OTG
1167
+ * Watchdog reset for ESP32-S2/S3/C3 with USB-OTG or USB-JTAG/Serial
1081
1168
  * Uses RTC watchdog timer to reset the chip - works when DTR/RTS signals are not available
1169
+ * This is an alias for rtcWdtResetChipSpecific() for backwards compatibility
1082
1170
  */
1083
1171
  async watchdogReset() {
1084
- this.logger.log("Hard resetting with watchdog timer...");
1085
- // Select correct register addresses based on chip family
1172
+ await this.rtcWdtResetChipSpecific();
1173
+ }
1174
+ /**
1175
+ * Check if current chip is using USB-OTG
1176
+ * Supports ESP32-S2 and ESP32-S3
1177
+ */
1178
+ async usingUsbOtg() {
1179
+ let uartDevBufNo;
1180
+ let usbOtgValue;
1181
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
1182
+ uartDevBufNo = ESP32S2_UARTDEV_BUF_NO;
1183
+ usbOtgValue = ESP32S2_UARTDEV_BUF_NO_USB_OTG;
1184
+ }
1185
+ else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1186
+ uartDevBufNo = ESP32S3_UARTDEV_BUF_NO;
1187
+ usbOtgValue = ESP32S3_UARTDEV_BUF_NO_USB_OTG;
1188
+ }
1189
+ else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1190
+ // P4: UARTDEV_BUF_NO depends on chip revision
1191
+ if (this.chipRevision === null) {
1192
+ this.chipRevision = await this.getChipRevision();
1193
+ }
1194
+ if (this.chipRevision < 300) {
1195
+ uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV0;
1196
+ }
1197
+ else {
1198
+ uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV300;
1199
+ }
1200
+ usbOtgValue = ESP32P4_UARTDEV_BUF_NO_USB_OTG;
1201
+ }
1202
+ else {
1203
+ return false;
1204
+ }
1205
+ const uartNo = (await this.readRegister(uartDevBufNo)) & 0xff;
1206
+ return uartNo === usbOtgValue;
1207
+ }
1208
+ /**
1209
+ * Check if current chip is using USB-JTAG/Serial
1210
+ * Supports ESP32-S3 and ESP32-C3
1211
+ */
1212
+ async usingUsbJtagSerial() {
1213
+ let uartDevBufNo;
1214
+ let usbJtagSerialValue;
1215
+ if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1216
+ uartDevBufNo = ESP32S3_UARTDEV_BUF_NO;
1217
+ usbJtagSerialValue = ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1218
+ }
1219
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
1220
+ // ESP32-C3: BSS_UART_DEV_ADDR depends on chip revision
1221
+ // Revision < 101: 0x3FCDF064
1222
+ // Revision >= 101: 0x3FCDF060
1223
+ let bssUartDevAddr;
1224
+ // Get chip revision if not already set
1225
+ if (this.chipRevision === null) {
1226
+ this.chipRevision = await this.getChipRevisionC3();
1227
+ }
1228
+ if (this.chipRevision < 101) {
1229
+ bssUartDevAddr = 0x3fcdf064;
1230
+ }
1231
+ else {
1232
+ bssUartDevAddr = 0x3fcdf060;
1233
+ }
1234
+ uartDevBufNo = bssUartDevAddr + ESP32C3_BUF_UART_NO_OFFSET;
1235
+ usbJtagSerialValue = ESP32C3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1236
+ }
1237
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C5) {
1238
+ uartDevBufNo = ESP32C5_UARTDEV_BUF_NO;
1239
+ usbJtagSerialValue = ESP32C5_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1240
+ }
1241
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C6) {
1242
+ uartDevBufNo = ESP32C6_UARTDEV_BUF_NO;
1243
+ usbJtagSerialValue = ESP32C6_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1244
+ }
1245
+ else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1246
+ // P4: UARTDEV_BUF_NO depends on chip revision
1247
+ // Revision < 300: 0x4FF3FEC8
1248
+ // Revision >= 300: 0x4FFBFEC8
1249
+ if (this.chipRevision === null) {
1250
+ this.chipRevision = await this.getChipRevision();
1251
+ }
1252
+ if (this.chipRevision < 300) {
1253
+ uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV0;
1254
+ }
1255
+ else {
1256
+ uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV300;
1257
+ }
1258
+ usbJtagSerialValue = ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1259
+ }
1260
+ else if (this.chipFamily === CHIP_FAMILY_ESP32H2) {
1261
+ uartDevBufNo = ESP32H2_UARTDEV_BUF_NO;
1262
+ usbJtagSerialValue = ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1263
+ }
1264
+ else {
1265
+ return false;
1266
+ }
1267
+ const uartNo = (await this.readRegister(uartDevBufNo)) & 0xff;
1268
+ return uartNo === usbJtagSerialValue;
1269
+ }
1270
+ /**
1271
+ * Get chip revision for ESP32-C3
1272
+ * Reads from EFUSE registers and calculates revision
1273
+ */
1274
+ async getChipRevisionC3() {
1275
+ if (this.chipFamily !== CHIP_FAMILY_ESP32C3) {
1276
+ return 0;
1277
+ }
1278
+ // Read EFUSE_RD_MAC_SPI_SYS_3_REG (bits [20:18] = lower 3 bits of revision)
1279
+ const word3 = await this.readRegister(ESP32C3_EFUSE_RD_MAC_SPI_SYS_3_REG);
1280
+ const low = (word3 >> 18) & 0x07;
1281
+ // Read EFUSE_RD_MAC_SPI_SYS_5_REG (bits [25:23] = upper 3 bits of revision)
1282
+ const word5 = await this.readRegister(ESP32C3_EFUSE_RD_MAC_SPI_SYS_5_REG);
1283
+ const hi = (word5 >> 23) & 0x07;
1284
+ // Combine: upper 3 bits from word5, lower 3 bits from word3
1285
+ const revision = (hi << 3) | low;
1286
+ return revision;
1287
+ }
1288
+ /**
1289
+ * RTC watchdog timer reset for ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C5, ESP32-C6, and ESP32-P4
1290
+ * Uses specific registers for each chip family
1291
+ * Note: ESP32-H2 does NOT support WDT reset
1292
+ */
1293
+ async rtcWdtResetChipSpecific() {
1294
+ this.logger.debug("Hard resetting with watchdog timer...");
1086
1295
  let WDTWPROTECT_REG;
1087
1296
  let WDTCONFIG0_REG;
1088
1297
  let WDTCONFIG1_REG;
@@ -1099,12 +1308,62 @@ export class ESPLoader extends EventTarget {
1099
1308
  WDTCONFIG1_REG = ESP32S3_RTC_CNTL_WDTCONFIG1_REG;
1100
1309
  WDT_WKEY = ESP32S3_RTC_CNTL_WDT_WKEY;
1101
1310
  }
1311
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
1312
+ WDTWPROTECT_REG = ESP32C3_RTC_CNTL_WDTWPROTECT_REG;
1313
+ WDTCONFIG0_REG = ESP32C3_RTC_CNTL_WDTCONFIG0_REG;
1314
+ WDTCONFIG1_REG = ESP32C3_RTC_CNTL_WDTCONFIG1_REG;
1315
+ WDT_WKEY = ESP32C3_RTC_CNTL_WDT_WKEY;
1316
+ }
1317
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C5 ||
1318
+ this.chipFamily === CHIP_FAMILY_ESP32C6) {
1319
+ // C5 and C6 use LP_WDT (Low Power Watchdog Timer)
1320
+ WDTWPROTECT_REG = ESP32C5_C6_RTC_CNTL_WDTWPROTECT_REG;
1321
+ WDTCONFIG0_REG = ESP32C5_C6_RTC_CNTL_WDTCONFIG0_REG;
1322
+ WDTCONFIG1_REG = ESP32C5_C6_RTC_CNTL_WDTCONFIG1_REG;
1323
+ WDT_WKEY = ESP32C5_C6_RTC_CNTL_WDT_WKEY;
1324
+ }
1325
+ else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1326
+ // P4 uses LP_WDT (Low Power Watchdog Timer)
1327
+ WDTWPROTECT_REG = ESP32P4_RTC_CNTL_WDTWPROTECT_REG;
1328
+ WDTCONFIG0_REG = ESP32P4_RTC_CNTL_WDTCONFIG0_REG;
1329
+ WDTCONFIG1_REG = ESP32P4_RTC_CNTL_WDTCONFIG1_REG;
1330
+ WDT_WKEY = ESP32P4_RTC_CNTL_WDT_WKEY;
1331
+ }
1102
1332
  else {
1103
- throw new Error(`watchdogReset() is only supported for ESP32-S2 and ESP32-S3, not ${this.chipFamily}`);
1333
+ throw new Error(`rtcWdtResetChipSpecific() is not supported for ${this.chipFamily}`);
1104
1334
  }
1105
1335
  // Unlock watchdog registers
1106
1336
  await this.writeRegister(WDTWPROTECT_REG, WDT_WKEY, undefined, 0);
1107
- // Set WDT timeout to 2000ms
1337
+ // Clear force download boot register (if applicable) BEFORE triggering WDT reset
1338
+ // This ensures the chip boots into firmware mode after reset
1339
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
1340
+ try {
1341
+ await this.writeRegister(ESP32S2_RTC_CNTL_OPTION1_REG, 0, ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, 0);
1342
+ this.logger.debug("Cleared force download boot mask");
1343
+ }
1344
+ catch (err) {
1345
+ this.logger.debug(`Expected error clearing force download boot mask: ${err}`);
1346
+ }
1347
+ }
1348
+ else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1349
+ try {
1350
+ await this.writeRegister(ESP32S3_RTC_CNTL_OPTION1_REG, 0, ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, 0);
1351
+ this.logger.debug("Cleared force download boot mask");
1352
+ }
1353
+ catch (err) {
1354
+ this.logger.debug(`Expected error clearing force download boot mask: ${err}`);
1355
+ }
1356
+ }
1357
+ else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1358
+ try {
1359
+ await this.writeRegister(ESP32P4_RTC_CNTL_OPTION1_REG, 0, ESP32P4_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, 0);
1360
+ this.logger.debug("Cleared force download boot mask");
1361
+ }
1362
+ catch (err) {
1363
+ this.logger.debug(`Expected error clearing force download boot mask: ${err}`);
1364
+ }
1365
+ }
1366
+ // Set WDT timeout to 2000ms (matches Python esptool)
1108
1367
  await this.writeRegister(WDTCONFIG1_REG, 2000, undefined, 0);
1109
1368
  // Enable WDT: bit 31 = enable, bits 28-30 = stage, bit 8 = sys reset, bits 0-2 = prescaler
1110
1369
  const wdtConfig = (1 << 31) | (5 << 28) | (1 << 8) | 2;
@@ -1114,102 +1373,139 @@ export class ESPLoader extends EventTarget {
1114
1373
  // Wait for reset to take effect
1115
1374
  await this.sleep(500);
1116
1375
  }
1376
+ /**
1377
+ * Helper: Check if USB-based WDT reset should be used for S2/S3
1378
+ * Returns true if WDT reset was performed, false otherwise
1379
+ */
1380
+ async tryUsbWdtReset(chipName, GPIO_STRAP_REG, GPIO_STRAP_SPI_BOOT_MASK, RTC_CNTL_OPTION1_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK) {
1381
+ const isUsingUsbOtg = await this.usingUsbOtg();
1382
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
1383
+ if (isUsingUsbOtg || isUsingUsbJtagSerial) {
1384
+ const strapReg = await this.readRegister(GPIO_STRAP_REG);
1385
+ const forceDlReg = await this.readRegister(RTC_CNTL_OPTION1_REG);
1386
+ // Only use watchdog reset if GPIO0 is low AND force download boot mode is not set
1387
+ if ((strapReg & GPIO_STRAP_SPI_BOOT_MASK) === 0 &&
1388
+ (forceDlReg & RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK) === 0) {
1389
+ await this.rtcWdtResetChipSpecific();
1390
+ this.logger.debug(`${chipName}: RTC WDT reset (USB detected, GPIO0 low)`);
1391
+ return true;
1392
+ }
1393
+ }
1394
+ return false;
1395
+ }
1396
+ /**
1397
+ * Chip-specific hard reset for ESP32-S2
1398
+ * Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
1399
+ */
1400
+ async hardResetS2() {
1401
+ const isUsingUsbOtg = await this.usingUsbOtg();
1402
+ if (isUsingUsbOtg) {
1403
+ await this.rtcWdtResetChipSpecific();
1404
+ this.logger.debug("ESP32-S2: RTC WDT reset (USB-OTG detected)");
1405
+ }
1406
+ else {
1407
+ // Use standard hardware reset
1408
+ await this.hardResetClassic();
1409
+ this.logger.debug("ESP32-S2: Classic reset");
1410
+ }
1411
+ }
1412
+ /**
1413
+ * Chip-specific hard reset for ESP32-S3
1414
+ * Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
1415
+ */
1416
+ async hardResetS3() {
1417
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
1418
+ if (isUsingUsbJtagSerial) {
1419
+ await this.rtcWdtResetChipSpecific();
1420
+ this.logger.debug("ESP32-S3: RTC WDT reset (USB-JTAG/Serial detected)");
1421
+ }
1422
+ else {
1423
+ // Use standard hardware reset
1424
+ await this.hardResetClassic();
1425
+ this.logger.debug("ESP32-S3: Classic reset");
1426
+ }
1427
+ }
1428
+ /**
1429
+ * Chip-specific hard reset for ESP32-C3
1430
+ * Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
1431
+ */
1432
+ async hardResetC3() {
1433
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
1434
+ if (isUsingUsbJtagSerial) {
1435
+ await this.rtcWdtResetChipSpecific();
1436
+ this.logger.debug("ESP32-C3: RTC WDT reset (USB-JTAG/Serial detected)");
1437
+ }
1438
+ else {
1439
+ // Use standard hardware reset
1440
+ await this.hardResetClassic();
1441
+ this.logger.debug("ESP32-C3: Classic reset");
1442
+ }
1443
+ }
1117
1444
  async hardReset(bootloader = false) {
1445
+ // In console mode, only allow simple hardware reset (no bootloader entry)
1446
+ if (this._consoleMode) {
1447
+ if (bootloader) {
1448
+ this.logger.debug("Skipping bootloader reset - device is in console mode");
1449
+ return;
1450
+ }
1451
+ // Simple hardware reset to restart firmware (IO0=HIGH)
1452
+ this.logger.debug("Performing hardware reset (console mode)...");
1453
+ if (this.isWebUSB()) {
1454
+ await this.hardResetToFirmwareWebUSB();
1455
+ }
1456
+ else {
1457
+ await this.hardResetToFirmware();
1458
+ }
1459
+ this.logger.debug("Hardware reset complete");
1460
+ return;
1461
+ }
1118
1462
  if (bootloader) {
1119
1463
  // enter flash mode
1120
1464
  if (this.port.getInfo().usbProductId === USB_JTAG_SERIAL_PID) {
1121
1465
  await this.hardResetUSBJTAGSerial();
1122
- this.logger.log("USB-JTAG/Serial reset.");
1466
+ this.logger.debug("USB-JTAG/Serial reset.");
1123
1467
  }
1124
1468
  else {
1125
1469
  // Use different reset strategy for WebUSB (Android) vs Web Serial (Desktop)
1126
1470
  if (this.isWebUSB()) {
1127
1471
  await this.hardResetClassicWebUSB();
1128
- this.logger.log("Classic reset (WebUSB/Android).");
1472
+ this.logger.debug("Classic reset (WebUSB/Android).");
1129
1473
  }
1130
1474
  else {
1131
1475
  await this.hardResetClassic();
1132
- this.logger.log("Classic reset.");
1476
+ this.logger.debug("Classic reset.");
1133
1477
  }
1134
1478
  }
1135
1479
  }
1136
1480
  else {
1137
1481
  // just reset (no bootloader mode)
1138
- // For ESP32-S2/S3 with USB-OTG, check if watchdog reset is needed
1139
- if (this.port.getInfo().usbProductId === USB_JTAG_SERIAL_PID &&
1140
- (this.chipFamily === CHIP_FAMILY_ESP32S2 ||
1141
- this.chipFamily === CHIP_FAMILY_ESP32S3)) {
1142
- // ESP32-S2/S3: Clear force download boot mode first
1143
- try {
1144
- // Clear force download boot mode to avoid chip being stuck in download mode
1145
- // after reset. Workaround for issue:
1146
- // https://github.com/espressif/arduino-esp32/issues/6762
1147
- const RTC_CNTL_OPTION1_REG = this.chipFamily === CHIP_FAMILY_ESP32S2
1148
- ? ESP32S2_RTC_CNTL_OPTION1_REG
1149
- : ESP32S3_RTC_CNTL_OPTION1_REG;
1150
- const RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK = this.chipFamily === CHIP_FAMILY_ESP32S2
1151
- ? ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK
1152
- : ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK;
1153
- await this.writeRegister(RTC_CNTL_OPTION1_REG, 0, RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, 0);
1154
- }
1155
- catch (e) {
1156
- // Skip invalid response and continue reset (can happen when monitoring during reset)
1157
- this.logger.log("Warning: Could not clear force download boot mode:", e);
1158
- }
1159
- // Check the strapping register to see if we can perform a watchdog reset
1160
- // Only use watchdog reset if GPIO0 is low AND force download boot mode is not set
1161
- let useWatchdogReset = false;
1162
- try {
1163
- const GPIO_STRAP_REG = this.chipFamily === CHIP_FAMILY_ESP32S2
1164
- ? ESP32S2_GPIO_STRAP_REG
1165
- : ESP32S3_GPIO_STRAP_REG;
1166
- const GPIO_STRAP_SPI_BOOT_MASK = this.chipFamily === CHIP_FAMILY_ESP32S2
1167
- ? ESP32S2_GPIO_STRAP_SPI_BOOT_MASK
1168
- : ESP32S3_GPIO_STRAP_SPI_BOOT_MASK;
1169
- const RTC_CNTL_OPTION1_REG = this.chipFamily === CHIP_FAMILY_ESP32S2
1170
- ? ESP32S2_RTC_CNTL_OPTION1_REG
1171
- : ESP32S3_RTC_CNTL_OPTION1_REG;
1172
- const RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK = this.chipFamily === CHIP_FAMILY_ESP32S2
1173
- ? ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK
1174
- : ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK;
1175
- const strapReg = await this.readRegister(GPIO_STRAP_REG);
1176
- const forceDlReg = await this.readRegister(RTC_CNTL_OPTION1_REG);
1177
- // GPIO0 low (download mode) AND force download boot not set
1178
- if ((strapReg & GPIO_STRAP_SPI_BOOT_MASK) === 0 &&
1179
- (forceDlReg & RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK) === 0) {
1180
- useWatchdogReset = true;
1181
- }
1182
- }
1183
- catch (e) {
1184
- // If we can't read the registers, use watchdog reset as fallback
1185
- this.logger.log("Warning: Could not read strap/option registers, using watchdog reset:", e);
1186
- useWatchdogReset = true;
1187
- }
1188
- if (useWatchdogReset) {
1189
- await this.watchdogReset();
1190
- this.logger.log("Watchdog reset (USB-OTG).");
1191
- }
1192
- else {
1193
- // Not in download mode, can use DTR/RTS reset
1194
- // But USB-OTG doesn't have DTR/RTS, so fall back to watchdog
1195
- await this.watchdogReset();
1196
- this.logger.log("Watchdog reset (USB-OTG, normal boot).");
1197
- }
1482
+ // For ESP32-S2/S3 with USB-OTG or USB-JTAG/Serial, check if watchdog reset is needed
1483
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2 && !this._consoleMode) {
1484
+ 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);
1485
+ if (wdtResetUsed)
1486
+ return;
1198
1487
  }
1199
- else if (this.isWebUSB()) {
1488
+ else if (this.chipFamily === CHIP_FAMILY_ESP32S3 &&
1489
+ !this._consoleMode) {
1490
+ 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);
1491
+ if (wdtResetUsed)
1492
+ return;
1493
+ }
1494
+ // Standard reset for all other cases
1495
+ if (this.isWebUSB()) {
1200
1496
  // WebUSB: Use longer delays for better compatibility
1201
1497
  await this.setRTSWebUSB(true); // EN->LOW
1202
1498
  await this.sleep(200);
1203
1499
  await this.setRTSWebUSB(false);
1204
1500
  await this.sleep(200);
1205
- this.logger.log("Hard reset (WebUSB).");
1501
+ this.logger.debug("Hard reset (WebUSB).");
1206
1502
  }
1207
1503
  else {
1208
1504
  // Web Serial: Standard reset
1209
1505
  await this.setRTS(true); // EN->LOW
1210
1506
  await this.sleep(100);
1211
1507
  await this.setRTS(false);
1212
- this.logger.log("Hard reset.");
1508
+ this.logger.debug("Hard reset.");
1213
1509
  }
1214
1510
  }
1215
1511
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -2374,6 +2670,221 @@ export class ESPLoader extends EventTarget {
2374
2670
  this.logger.debug(`Port close error: ${err}`);
2375
2671
  }
2376
2672
  }
2673
+ /**
2674
+ * @name releaseReaderWriter
2675
+ * Release reader and writer locks without closing the port
2676
+ * Used when switching to console mode
2677
+ */
2678
+ async releaseReaderWriter() {
2679
+ if (this._parent) {
2680
+ await this._parent.releaseReaderWriter();
2681
+ return;
2682
+ }
2683
+ // Check if device is in JTAG mode and needs reset to boot into firmware
2684
+ const didReconnect = await this._resetToFirmwareIfNeeded();
2685
+ // If we reconnected for console, the reader/writer are already released and restarted
2686
+ if (didReconnect) {
2687
+ return;
2688
+ }
2689
+ // Wait for pending writes to complete
2690
+ try {
2691
+ await this._writeChain;
2692
+ }
2693
+ catch (err) {
2694
+ this.logger.debug(`Pending write error during release: ${err}`);
2695
+ }
2696
+ // Release writer
2697
+ if (this._writer) {
2698
+ try {
2699
+ this._writer.releaseLock();
2700
+ this.logger.debug("Writer released");
2701
+ }
2702
+ catch (err) {
2703
+ this.logger.debug(`Writer release error: ${err}`);
2704
+ }
2705
+ this._writer = undefined;
2706
+ }
2707
+ // Cancel and release reader
2708
+ if (this._reader) {
2709
+ const reader = this._reader;
2710
+ try {
2711
+ // Suppress disconnect event during console mode switching
2712
+ this._suppressDisconnect = true;
2713
+ await reader.cancel();
2714
+ this.logger.debug("Reader cancelled");
2715
+ }
2716
+ catch (err) {
2717
+ this.logger.debug(`Reader cancel error: ${err}`);
2718
+ }
2719
+ finally {
2720
+ try {
2721
+ reader.releaseLock();
2722
+ }
2723
+ catch (err) {
2724
+ this.logger.debug(`Reader release error: ${err}`);
2725
+ }
2726
+ }
2727
+ if (this._reader === reader) {
2728
+ this._reader = undefined;
2729
+ }
2730
+ }
2731
+ }
2732
+ /**
2733
+ * @name resetToFirmware
2734
+ * Public method to reset device from bootloader to firmware for console mode
2735
+ * Automatically detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
2736
+ * @returns true if reset was performed, false if not needed
2737
+ */
2738
+ async resetToFirmware() {
2739
+ return await this._resetToFirmwareIfNeeded();
2740
+ }
2741
+ /**
2742
+ * @name detectUsbConnectionType
2743
+ * Detect if device is using USB-JTAG/Serial or USB-OTG (not external serial chip)
2744
+ * This helper extracts the detection logic from initialize() for reuse
2745
+ * @returns true if USB-JTAG or USB-OTG, false if external serial chip
2746
+ * @throws Error if detection fails and chipFamily is not set
2747
+ */
2748
+ async detectUsbConnectionType() {
2749
+ if (!this.chipFamily) {
2750
+ throw new Error("Cannot detect USB connection type: chipFamily not set");
2751
+ }
2752
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2 ||
2753
+ this.chipFamily === CHIP_FAMILY_ESP32S3) {
2754
+ const isUsingUsbOtg = await this.usingUsbOtg();
2755
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
2756
+ return isUsingUsbOtg || isUsingUsbJtagSerial;
2757
+ }
2758
+ else if (this.chipFamily === CHIP_FAMILY_ESP32C3 ||
2759
+ this.chipFamily === CHIP_FAMILY_ESP32C5 ||
2760
+ this.chipFamily === CHIP_FAMILY_ESP32C6) {
2761
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
2762
+ return isUsingUsbJtagSerial;
2763
+ }
2764
+ else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
2765
+ const isUsingUsbOtg = await this.usingUsbOtg();
2766
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
2767
+ return isUsingUsbOtg || isUsingUsbJtagSerial;
2768
+ }
2769
+ else {
2770
+ // Other chips don't have USB-JTAG/OTG
2771
+ return false;
2772
+ }
2773
+ }
2774
+ /**
2775
+ * @name enterConsoleMode
2776
+ * Prepare device for console mode by resetting to firmware
2777
+ * Handles both USB-JTAG/OTG devices (closes port) and external serial chips (keeps port open)
2778
+ * @returns true if port was closed (USB-JTAG), false if port stays open (serial chip)
2779
+ */
2780
+ async enterConsoleMode() {
2781
+ // Set console mode flag
2782
+ this._consoleMode = true;
2783
+ // Re-detect USB connection type to ensure we have a definitive value
2784
+ // This handles cases where isUsbJtagOrOtg might be undefined
2785
+ let isUsbJtag;
2786
+ try {
2787
+ isUsbJtag = await this.detectUsbConnectionType();
2788
+ this.logger.debug(`USB connection type detected: ${isUsbJtag ? "USB-JTAG/OTG" : "External Serial Chip"}`);
2789
+ }
2790
+ catch (err) {
2791
+ // If detection fails, fall back to cached value or fail-fast
2792
+ if (this.isUsbJtagOrOtg === undefined) {
2793
+ throw new Error(`Cannot enter console mode: USB connection type unknown and detection failed: ${err}`);
2794
+ }
2795
+ this.logger.debug(`USB detection failed, using cached value: ${this.isUsbJtagOrOtg}`);
2796
+ isUsbJtag = this.isUsbJtagOrOtg;
2797
+ }
2798
+ if (isUsbJtag) {
2799
+ // USB-JTAG/OTG devices: Use watchdog reset which closes port
2800
+ const wasReset = await this._resetToFirmwareIfNeeded();
2801
+ return wasReset; // true = port closed, caller must reopen
2802
+ }
2803
+ else {
2804
+ // External serial chip devices: Release locks and do simple reset
2805
+ try {
2806
+ await this.releaseReaderWriter();
2807
+ await this.sleep(100);
2808
+ }
2809
+ catch (err) {
2810
+ this.logger.debug(`Failed to release locks: ${err}`);
2811
+ }
2812
+ // Hardware reset to firmware mode (IO0=HIGH)
2813
+ try {
2814
+ await this.hardReset(false);
2815
+ this.logger.log("Device reset to firmware mode");
2816
+ }
2817
+ catch (err) {
2818
+ this.logger.debug(`Could not reset device: ${err}`);
2819
+ }
2820
+ return false; // Port stays open
2821
+ }
2822
+ }
2823
+ /**
2824
+ * @name _resetToFirmwareIfNeeded
2825
+ * Reset device from bootloader to firmware when switching to console mode
2826
+ * Detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
2827
+ * @returns true if reconnect was performed, false otherwise
2828
+ */
2829
+ async _resetToFirmwareIfNeeded() {
2830
+ try {
2831
+ // Check if device is using USB-JTAG/Serial or USB-OTG
2832
+ // Value should already be set during main() connection
2833
+ // Use getter to access parent's value if this is a stub
2834
+ const needsReset = this.isUsbJtagOrOtg === true;
2835
+ if (needsReset) {
2836
+ const resetMethod = this.chipFamily === CHIP_FAMILY_ESP32S2 ||
2837
+ this.chipFamily === CHIP_FAMILY_ESP32S3
2838
+ ? "USB-JTAG/Serial or USB-OTG"
2839
+ : "USB-JTAG/Serial";
2840
+ this.logger.log(`Resetting ${this.chipFamily} (${resetMethod}) to boot into firmware...`);
2841
+ // Set console mode flag before reset to prevent subsequent hardReset calls
2842
+ this._consoleMode = true;
2843
+ // For S2/S3: Clear force download boot mask before WDT reset
2844
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2 ||
2845
+ this.chipFamily === CHIP_FAMILY_ESP32S3) {
2846
+ const OPTION1_REG = this.chipFamily === CHIP_FAMILY_ESP32S2
2847
+ ? ESP32S2_RTC_CNTL_OPTION1_REG
2848
+ : ESP32S3_RTC_CNTL_OPTION1_REG;
2849
+ const FORCE_DOWNLOAD_BOOT_MASK = this.chipFamily === CHIP_FAMILY_ESP32S2
2850
+ ? ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK
2851
+ : ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK;
2852
+ try {
2853
+ // Clear force download boot mode to avoid chip being stuck in download mode
2854
+ await this.writeRegister(OPTION1_REG, 0, FORCE_DOWNLOAD_BOOT_MASK, 0);
2855
+ this.logger.debug("Cleared force download boot mask");
2856
+ }
2857
+ catch (err) {
2858
+ this.logger.debug(`Expected error clearing force download boot mask: ${err}`);
2859
+ }
2860
+ }
2861
+ // Perform watchdog reset to reboot into firmware
2862
+ try {
2863
+ await this.rtcWdtResetChipSpecific();
2864
+ this.logger.debug("Watchdog reset triggered successfully");
2865
+ }
2866
+ catch (err) {
2867
+ // Error is expected - device resets before responding
2868
+ this.logger.debug(`Watchdog reset initiated (connection lost as expected: ${err})`);
2869
+ }
2870
+ // Wait for device to fully boot into firmware
2871
+ this.logger.log("Waiting for device to boot into firmware...");
2872
+ await this.sleep(1000);
2873
+ // After WDT reset, streams are dead/locked - don't try to manipulate them
2874
+ // Just mark everything as disconnected and let browser clean up
2875
+ this.connected = false;
2876
+ this._writer = undefined;
2877
+ this._reader = undefined;
2878
+ this.logger.debug("Device reset to firmware mode (port closed)");
2879
+ return true;
2880
+ }
2881
+ }
2882
+ catch (err) {
2883
+ this.logger.debug(`Could not reset device to firmware mode: ${err}`);
2884
+ // Continue anyway - console mode might still work
2885
+ }
2886
+ return false;
2887
+ }
2377
2888
  /**
2378
2889
  * @name reconnectAndResume
2379
2890
  * Reconnect the serial port to flush browser buffers and reload stub
@@ -2385,6 +2896,7 @@ export class ESPLoader extends EventTarget {
2385
2896
  }
2386
2897
  try {
2387
2898
  this.logger.log("Reconnecting serial port...");
2899
+ const savedBaudRate = this._currentBaudRate;
2388
2900
  this.connected = false;
2389
2901
  this.__inputBuffer = [];
2390
2902
  this.__inputBufferReadIndex = 0;
@@ -2420,7 +2932,7 @@ export class ESPLoader extends EventTarget {
2420
2932
  // Close port
2421
2933
  try {
2422
2934
  await this.port.close();
2423
- this.logger.log("Port closed");
2935
+ this.logger.debug("Port closed");
2424
2936
  }
2425
2937
  catch (err) {
2426
2938
  this.logger.debug(`Port close error: ${err}`);
@@ -2430,6 +2942,7 @@ export class ESPLoader extends EventTarget {
2430
2942
  try {
2431
2943
  await this.port.open({ baudRate: ESP_ROM_BAUD });
2432
2944
  this.connected = true;
2945
+ this._currentBaudRate = ESP_ROM_BAUD;
2433
2946
  }
2434
2947
  catch (err) {
2435
2948
  throw new Error(`Failed to open port: ${err}`);
@@ -2471,8 +2984,8 @@ export class ESPLoader extends EventTarget {
2471
2984
  const stubLoader = await this.runStub(true);
2472
2985
  this.logger.debug("Stub loaded");
2473
2986
  // Restore baudrate if it was changed
2474
- if (this._currentBaudRate !== ESP_ROM_BAUD) {
2475
- await stubLoader.setBaudrate(this._currentBaudRate);
2987
+ if (savedBaudRate !== ESP_ROM_BAUD) {
2988
+ await stubLoader.setBaudrate(savedBaudRate);
2476
2989
  // Verify port is still ready after baudrate change
2477
2990
  if (!this.port.writable || !this.port.readable) {
2478
2991
  throw new Error(`Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
@@ -2502,6 +3015,8 @@ export class ESPLoader extends EventTarget {
2502
3015
  }
2503
3016
  try {
2504
3017
  this.logger.log("Reconnecting to bootloader mode...");
3018
+ // Clear console mode flag when reconnecting to bootloader
3019
+ this._consoleMode = false;
2505
3020
  this.connected = false;
2506
3021
  this.__inputBuffer = [];
2507
3022
  this.__inputBufferReadIndex = 0;
@@ -2537,7 +3052,7 @@ export class ESPLoader extends EventTarget {
2537
3052
  // Close port
2538
3053
  try {
2539
3054
  await this.port.close();
2540
- this.logger.log("Port closed");
3055
+ this.logger.debug("Port closed");
2541
3056
  }
2542
3057
  catch (err) {
2543
3058
  this.logger.debug(`Port close error: ${err}`);
@@ -2547,6 +3062,7 @@ export class ESPLoader extends EventTarget {
2547
3062
  try {
2548
3063
  await this.port.open({ baudRate: ESP_ROM_BAUD });
2549
3064
  this.connected = true;
3065
+ this._currentBaudRate = ESP_ROM_BAUD;
2550
3066
  }
2551
3067
  catch (err) {
2552
3068
  throw new Error(`Failed to open port: ${err}`);
@@ -2560,6 +3076,8 @@ export class ESPLoader extends EventTarget {
2560
3076
  // Reset chip info and stub state
2561
3077
  this.__chipFamily = undefined;
2562
3078
  this.chipName = "Unknown Chip";
3079
+ this.chipRevision = null;
3080
+ this.chipVariant = null;
2563
3081
  this.IS_STUB = false;
2564
3082
  // Start read loop
2565
3083
  if (!this._parent) {
@@ -2651,9 +3169,13 @@ export class ESPLoader extends EventTarget {
2651
3169
  * @param addr - Address to read from
2652
3170
  * @param size - Number of bytes to read
2653
3171
  * @param onPacketReceived - Optional callback function called when packet is received
3172
+ * @param options - Optional parameters for advanced control
3173
+ * - chunkSize: Amount of data to request from ESP in one command (bytes)
3174
+ * - blockSize: Size of each data block sent by ESP (bytes)
3175
+ * - maxInFlight: Maximum unacknowledged bytes (bytes)
2654
3176
  * @returns Uint8Array containing the flash data
2655
3177
  */
2656
- async readFlash(addr, size, onPacketReceived) {
3178
+ async readFlash(addr, size, onPacketReceived, options) {
2657
3179
  if (!this.IS_STUB) {
2658
3180
  throw new Error("Reading flash is only supported in stub mode. Please run runStub() first.");
2659
3181
  }
@@ -2681,7 +3203,12 @@ export class ESPLoader extends EventTarget {
2681
3203
  // For WebUSB (Android), use smaller chunks to avoid timeouts and buffer issues
2682
3204
  // For Web Serial (Desktop), use larger chunks for better performance
2683
3205
  let CHUNK_SIZE;
2684
- if (this.isWebUSB()) {
3206
+ if ((options === null || options === void 0 ? void 0 : options.chunkSize) !== undefined) {
3207
+ // Use user-provided chunkSize if in advanced mode
3208
+ CHUNK_SIZE = options.chunkSize;
3209
+ this.logger.log(`Using custom chunk size: 0x${CHUNK_SIZE.toString(16)} bytes`);
3210
+ }
3211
+ else if (this.isWebUSB()) {
2685
3212
  // WebUSB: Use smaller chunks to avoid SLIP timeout issues
2686
3213
  CHUNK_SIZE = 0x4 * 0x1000; // 4KB = 16384 bytes
2687
3214
  }
@@ -2709,7 +3236,16 @@ export class ESPLoader extends EventTarget {
2709
3236
  }
2710
3237
  let blockSize;
2711
3238
  let maxInFlight;
2712
- if (this.isWebUSB()) {
3239
+ if ((options === null || options === void 0 ? void 0 : options.blockSize) !== undefined &&
3240
+ (options === null || options === void 0 ? void 0 : options.maxInFlight) !== undefined) {
3241
+ // Use user-provided values if in advanced mode
3242
+ blockSize = options.blockSize;
3243
+ maxInFlight = options.maxInFlight;
3244
+ if (retryCount === 0) {
3245
+ this.logger.debug(`Using custom parameters: blockSize=${blockSize}, maxInFlight=${maxInFlight}`);
3246
+ }
3247
+ }
3248
+ else if (this.isWebUSB()) {
2713
3249
  // WebUSB (Android): All devices use adaptive speed
2714
3250
  // All have maxTransferSize=64, baseBlockSize=31
2715
3251
  const maxTransferSize = this.port.maxTransferSize || 64;
@@ -2847,7 +3383,7 @@ export class ESPLoader extends EventTarget {
2847
3383
  // Check if it's a timeout error or SLIP error
2848
3384
  if (err instanceof SlipReadError) {
2849
3385
  if (retryCount <= MAX_RETRIES) {
2850
- this.logger.log(`${err.message} at 0x${currentAddr.toString(16)}. Draining buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`);
3386
+ this.logger.debug(`${err.message} at 0x${currentAddr.toString(16)}. Draining buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`);
2851
3387
  try {
2852
3388
  await this.drainInputBuffer(200);
2853
3389
  // Clear application buffer