tasmota-webserial-esptool 9.2.7 → 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.
package/src/esp_loader.ts CHANGED
@@ -64,10 +64,51 @@ import {
64
64
  ESP32S2_RTC_CNTL_WDTCONFIG0_REG,
65
65
  ESP32S2_RTC_CNTL_WDTCONFIG1_REG,
66
66
  ESP32S2_RTC_CNTL_WDT_WKEY,
67
+ ESP32S2_GPIO_STRAP_REG,
68
+ ESP32S2_GPIO_STRAP_SPI_BOOT_MASK,
69
+ ESP32S2_RTC_CNTL_OPTION1_REG,
70
+ ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
67
71
  ESP32S3_RTC_CNTL_WDTWPROTECT_REG,
68
72
  ESP32S3_RTC_CNTL_WDTCONFIG0_REG,
69
73
  ESP32S3_RTC_CNTL_WDTCONFIG1_REG,
70
74
  ESP32S3_RTC_CNTL_WDT_WKEY,
75
+ ESP32S3_GPIO_STRAP_REG,
76
+ ESP32S3_GPIO_STRAP_SPI_BOOT_MASK,
77
+ ESP32S3_RTC_CNTL_OPTION1_REG,
78
+ ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
79
+ ESP32S2_UARTDEV_BUF_NO,
80
+ ESP32S2_UARTDEV_BUF_NO_USB_OTG,
81
+ ESP32S3_UARTDEV_BUF_NO,
82
+ ESP32S3_UARTDEV_BUF_NO_USB_OTG,
83
+ ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
84
+ ESP32C3_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
85
+ ESP32C3_BUF_UART_NO_OFFSET,
86
+ ESP32C3_EFUSE_RD_MAC_SPI_SYS_3_REG,
87
+ ESP32C3_EFUSE_RD_MAC_SPI_SYS_5_REG,
88
+ ESP32C3_RTC_CNTL_WDTWPROTECT_REG,
89
+ ESP32C3_RTC_CNTL_WDTCONFIG0_REG,
90
+ ESP32C3_RTC_CNTL_WDTCONFIG1_REG,
91
+ ESP32C3_RTC_CNTL_WDT_WKEY,
92
+ ESP32C5_C6_RTC_CNTL_WDTWPROTECT_REG,
93
+ ESP32C5_C6_RTC_CNTL_WDTCONFIG0_REG,
94
+ ESP32C5_C6_RTC_CNTL_WDTCONFIG1_REG,
95
+ ESP32C5_C6_RTC_CNTL_WDT_WKEY,
96
+ ESP32C5_UARTDEV_BUF_NO,
97
+ ESP32C5_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
98
+ ESP32C6_UARTDEV_BUF_NO,
99
+ ESP32C6_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
100
+ ESP32P4_RTC_CNTL_WDTWPROTECT_REG,
101
+ ESP32P4_RTC_CNTL_WDTCONFIG0_REG,
102
+ ESP32P4_RTC_CNTL_WDTCONFIG1_REG,
103
+ ESP32P4_RTC_CNTL_WDT_WKEY,
104
+ ESP32P4_UARTDEV_BUF_NO_REV0,
105
+ ESP32P4_UARTDEV_BUF_NO_REV300,
106
+ ESP32P4_UARTDEV_BUF_NO_USB_OTG,
107
+ ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
108
+ ESP32P4_RTC_CNTL_OPTION1_REG,
109
+ ESP32P4_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
110
+ ESP32H2_UARTDEV_BUF_NO,
111
+ ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
71
112
  } from "./const";
72
113
  import { getStubCode } from "./stubs";
73
114
  import { hexFormatter, sleep, slipEncode, toHex } from "./util";
@@ -108,6 +149,17 @@ export class ESPLoader extends EventTarget {
108
149
  private __commandLock: Promise<[number, number[]]> = Promise.resolve([0, []]);
109
150
  private __isReconfiguring: boolean = false;
110
151
  private __abandonCurrentOperation: boolean = false;
152
+ private _suppressDisconnect: boolean = false;
153
+ private __consoleMode: boolean = false;
154
+ public _isUsbJtagOrOtg: boolean | undefined = undefined;
155
+
156
+ /**
157
+ * Check if device is using USB-JTAG or USB-OTG (not external serial chip)
158
+ * Returns undefined if not yet determined
159
+ */
160
+ public get isUsbJtagOrOtg(): boolean | undefined {
161
+ return this._parent ? this._parent._isUsbJtagOrOtg : this._isUsbJtagOrOtg;
162
+ }
111
163
 
112
164
  // Adaptive speed adjustment for flash read operations
113
165
  private __adaptiveBlockMultiplier: number = 1;
@@ -174,6 +226,24 @@ export class ESPLoader extends EventTarget {
174
226
  }
175
227
  }
176
228
 
229
+ // Console mode with parent delegation
230
+ private get _consoleMode(): boolean {
231
+ return this._parent ? this._parent._consoleMode : this.__consoleMode;
232
+ }
233
+
234
+ private set _consoleMode(value: boolean) {
235
+ if (this._parent) {
236
+ this._parent._consoleMode = value;
237
+ } else {
238
+ this.__consoleMode = value;
239
+ }
240
+ }
241
+
242
+ // Public setter for console mode (used by script.js)
243
+ public setConsoleMode(value: boolean): void {
244
+ this._consoleMode = value;
245
+ }
246
+
177
247
  private get _inputBuffer(): number[] {
178
248
  if (this._parent) {
179
249
  return this._parent._inputBuffer;
@@ -449,6 +519,18 @@ export class ESPLoader extends EventTarget {
449
519
  // Detect chip type
450
520
  await this.detectChip();
451
521
 
522
+ // Detect if device is using USB-JTAG/Serial or USB-OTG (not external serial chip)
523
+ // This is needed to determine the correct reset strategy for console mode
524
+ try {
525
+ this._isUsbJtagOrOtg = await this.detectUsbConnectionType();
526
+ this.logger.debug(
527
+ `USB connection type: ${this._isUsbJtagOrOtg ? "USB-JTAG/OTG" : "External Serial Chip"}`,
528
+ );
529
+ } catch (err) {
530
+ this.logger.debug(`Could not detect USB connection type: ${err}`);
531
+ // Leave as undefined if detection fails
532
+ }
533
+
452
534
  // Read the OTP data for this chip and store into this.efuses array
453
535
  const FlAddr = getSpiFlashAddresses(this.getChipFamily());
454
536
  const AddrMAC = FlAddr.macFuse;
@@ -478,7 +560,7 @@ export class ESPLoader extends EventTarget {
478
560
  this.chipName = chipInfo.name;
479
561
  this.chipFamily = chipInfo.family;
480
562
 
481
- // Get chip revision for ESP32-P4
563
+ // Get chip revision for ESP32-P4 and ESP32-C3
482
564
  if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
483
565
  this.chipRevision = await this.getChipRevision();
484
566
  this.logger.debug(`ESP32-P4 revision: ${this.chipRevision}`);
@@ -490,6 +572,9 @@ export class ESPLoader extends EventTarget {
490
572
  this.chipVariant = "rev0";
491
573
  }
492
574
  this.logger.debug(`ESP32-P4 variant: ${this.chipVariant}`);
575
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
576
+ this.chipRevision = await this.getChipRevision();
577
+ this.logger.debug(`ESP32-C3 revision: ${this.chipRevision}`);
493
578
  }
494
579
 
495
580
  this.logger.debug(
@@ -541,7 +626,6 @@ export class ESPLoader extends EventTarget {
541
626
 
542
627
  if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
543
628
  this.chipRevision = await this.getChipRevision();
544
- this.logger.debug(`ESP32-P4 revision: ${this.chipRevision}`);
545
629
 
546
630
  if (this.chipRevision >= 300) {
547
631
  this.chipVariant = "rev300";
@@ -549,6 +633,8 @@ export class ESPLoader extends EventTarget {
549
633
  this.chipVariant = "rev0";
550
634
  }
551
635
  this.logger.debug(`ESP32-P4 variant: ${this.chipVariant}`);
636
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
637
+ this.chipRevision = await this.getChipRevision();
552
638
  }
553
639
 
554
640
  this.logger.debug(
@@ -560,22 +646,24 @@ export class ESPLoader extends EventTarget {
560
646
  * Get chip revision for ESP32-P4
561
647
  */
562
648
  async getChipRevision(): Promise<number> {
563
- if (this.chipFamily !== CHIP_FAMILY_ESP32P4) {
564
- return 0;
565
- }
649
+ if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
650
+ // Read from EFUSE_BLOCK1 to get chip revision
651
+ // Word 2 contains revision info for ESP32-P4
652
+ const word2 = await this.readRegister(ESP32P4_EFUSE_BLOCK1_ADDR + 8);
566
653
 
567
- // Read from EFUSE_BLOCK1 to get chip revision
568
- // Word 2 contains revision info for ESP32-P4
569
- const word2 = await this.readRegister(ESP32P4_EFUSE_BLOCK1_ADDR + 8);
654
+ // Minor revision: bits [3:0]
655
+ const minorRev = word2 & 0x0f;
570
656
 
571
- // Minor revision: bits [3:0]
572
- const minorRev = word2 & 0x0f;
657
+ // Major revision: bits [23] << 2 | bits [5:4]
658
+ const majorRev = (((word2 >> 23) & 1) << 2) | ((word2 >> 4) & 0x03);
573
659
 
574
- // Major revision: bits [23] << 2 | bits [5:4]
575
- const majorRev = (((word2 >> 23) & 1) << 2) | ((word2 >> 4) & 0x03);
660
+ // Revision is major * 100 + minor
661
+ return majorRev * 100 + minorRev;
662
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
663
+ return await this.getChipRevisionC3();
664
+ }
576
665
 
577
- // Revision is major * 100 + minor
578
- return majorRev * 100 + minorRev;
666
+ return 0;
579
667
  }
580
668
 
581
669
  /**
@@ -676,12 +764,26 @@ export class ESPLoader extends EventTarget {
676
764
  this._totalBytesRead += value.length;
677
765
  }
678
766
  } catch {
679
- this.logger.error("Read loop got disconnected");
767
+ // Don't log error if this is an expected disconnect during console mode transition
768
+ if (!this._consoleMode) {
769
+ this.logger.error("Read loop got disconnected");
770
+ }
680
771
  } finally {
681
772
  // Always reset reconfiguring flag when read loop ends
682
773
  // This prevents "Cannot write during port reconfiguration" errors
683
774
  // when the read loop dies unexpectedly
684
775
  this._isReconfiguring = false;
776
+
777
+ // Release reader if still locked
778
+ if (this._reader) {
779
+ try {
780
+ this._reader.releaseLock();
781
+ this.logger.debug("Reader released in readLoop cleanup");
782
+ } catch (err) {
783
+ this.logger.debug(`Reader release error in readLoop: ${err}`);
784
+ }
785
+ this._reader = undefined;
786
+ }
685
787
  }
686
788
 
687
789
  // Disconnected!
@@ -700,7 +802,11 @@ export class ESPLoader extends EventTarget {
700
802
  );
701
803
  }
702
804
 
703
- this.dispatchEvent(new Event("disconnect"));
805
+ // Only dispatch disconnect event if not suppressed
806
+ if (!this._suppressDisconnect) {
807
+ this.dispatchEvent(new Event("disconnect"));
808
+ }
809
+ this._suppressDisconnect = false;
704
810
  this.logger.debug("Finished read loop");
705
811
  }
706
812
 
@@ -778,6 +884,34 @@ export class ESPLoader extends EventTarget {
778
884
  await this.sleep(200);
779
885
  }
780
886
 
887
+ /**
888
+ * Reset to firmware mode (not bootloader) for Web Serial
889
+ * Keeps IO0=HIGH during reset so chip boots into firmware
890
+ */
891
+ async hardResetToFirmware() {
892
+ await this.setDTR(false); // IO0=HIGH
893
+ await this.setRTS(true); // EN=LOW, chip in reset
894
+ await this.sleep(100);
895
+ await this.setRTS(false); // EN=HIGH, chip out of reset (IO0 stays HIGH)
896
+ await this.sleep(50);
897
+
898
+ await this.sleep(200);
899
+ }
900
+
901
+ /**
902
+ * Reset to firmware mode (not bootloader) for WebUSB
903
+ * Keeps IO0=HIGH during reset so chip boots into firmware
904
+ */
905
+ async hardResetToFirmwareWebUSB() {
906
+ await this.setDTRWebUSB(false); // IO0=HIGH
907
+ await this.setRTSWebUSB(true); // EN=LOW, chip in reset
908
+ await this.sleep(100);
909
+ await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset (IO0 stays HIGH)
910
+ await this.sleep(50);
911
+
912
+ await this.sleep(200);
913
+ }
914
+
781
915
  /**
782
916
  * @name hardResetUnixTight
783
917
  * Unix Tight reset for Web Serial (Desktop) - sets DTR and RTS simultaneously
@@ -1276,7 +1410,9 @@ export class ESPLoader extends EventTarget {
1276
1410
  try {
1277
1411
  // Check if port is still open, if not, skip this strategy
1278
1412
  if (!this.connected || !this.port.writable) {
1279
- this.logger.log(`Port disconnected, skipping ${strategy.name} reset`);
1413
+ this.logger.debug(
1414
+ `Port disconnected, skipping ${strategy.name} reset`,
1415
+ );
1280
1416
  continue;
1281
1417
  }
1282
1418
 
@@ -1326,7 +1462,7 @@ export class ESPLoader extends EventTarget {
1326
1462
  }
1327
1463
  } catch (error) {
1328
1464
  lastError = error as Error;
1329
- this.logger.log(
1465
+ this.logger.debug(
1330
1466
  `${strategy.name} reset failed: ${(error as Error).message}`,
1331
1467
  );
1332
1468
 
@@ -1359,13 +1495,140 @@ export class ESPLoader extends EventTarget {
1359
1495
 
1360
1496
  /**
1361
1497
  * @name watchdogReset
1362
- * Watchdog reset for ESP32-S2/S3 with USB-OTG
1498
+ * Watchdog reset for ESP32-S2/S3/C3 with USB-OTG or USB-JTAG/Serial
1363
1499
  * Uses RTC watchdog timer to reset the chip - works when DTR/RTS signals are not available
1500
+ * This is an alias for rtcWdtResetChipSpecific() for backwards compatibility
1364
1501
  */
1365
1502
  async watchdogReset() {
1366
- this.logger.log("Hard resetting with watchdog timer...");
1503
+ await this.rtcWdtResetChipSpecific();
1504
+ }
1505
+
1506
+ /**
1507
+ * Check if current chip is using USB-OTG
1508
+ * Supports ESP32-S2 and ESP32-S3
1509
+ */
1510
+ public async usingUsbOtg(): Promise<boolean> {
1511
+ let uartDevBufNo: number;
1512
+ let usbOtgValue: number;
1513
+
1514
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
1515
+ uartDevBufNo = ESP32S2_UARTDEV_BUF_NO;
1516
+ usbOtgValue = ESP32S2_UARTDEV_BUF_NO_USB_OTG;
1517
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1518
+ uartDevBufNo = ESP32S3_UARTDEV_BUF_NO;
1519
+ usbOtgValue = ESP32S3_UARTDEV_BUF_NO_USB_OTG;
1520
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1521
+ // P4: UARTDEV_BUF_NO depends on chip revision
1522
+ if (this.chipRevision === null) {
1523
+ this.chipRevision = await this.getChipRevision();
1524
+ }
1525
+
1526
+ if (this.chipRevision < 300) {
1527
+ uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV0;
1528
+ } else {
1529
+ uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV300;
1530
+ }
1531
+ usbOtgValue = ESP32P4_UARTDEV_BUF_NO_USB_OTG;
1532
+ } else {
1533
+ return false;
1534
+ }
1535
+
1536
+ const uartNo = (await this.readRegister(uartDevBufNo)) & 0xff;
1537
+ return uartNo === usbOtgValue;
1538
+ }
1539
+
1540
+ /**
1541
+ * Check if current chip is using USB-JTAG/Serial
1542
+ * Supports ESP32-S3 and ESP32-C3
1543
+ */
1544
+ public async usingUsbJtagSerial(): Promise<boolean> {
1545
+ let uartDevBufNo: number;
1546
+ let usbJtagSerialValue: number;
1547
+
1548
+ if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1549
+ uartDevBufNo = ESP32S3_UARTDEV_BUF_NO;
1550
+ usbJtagSerialValue = ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1551
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
1552
+ // ESP32-C3: BSS_UART_DEV_ADDR depends on chip revision
1553
+ // Revision < 101: 0x3FCDF064
1554
+ // Revision >= 101: 0x3FCDF060
1555
+ let bssUartDevAddr: number;
1556
+
1557
+ // Get chip revision if not already set
1558
+ if (this.chipRevision === null) {
1559
+ this.chipRevision = await this.getChipRevisionC3();
1560
+ }
1561
+
1562
+ if (this.chipRevision < 101) {
1563
+ bssUartDevAddr = 0x3fcdf064;
1564
+ } else {
1565
+ bssUartDevAddr = 0x3fcdf060;
1566
+ }
1567
+
1568
+ uartDevBufNo = bssUartDevAddr + ESP32C3_BUF_UART_NO_OFFSET;
1569
+ usbJtagSerialValue = ESP32C3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1570
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32C5) {
1571
+ uartDevBufNo = ESP32C5_UARTDEV_BUF_NO;
1572
+ usbJtagSerialValue = ESP32C5_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1573
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32C6) {
1574
+ uartDevBufNo = ESP32C6_UARTDEV_BUF_NO;
1575
+ usbJtagSerialValue = ESP32C6_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1576
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1577
+ // P4: UARTDEV_BUF_NO depends on chip revision
1578
+ // Revision < 300: 0x4FF3FEC8
1579
+ // Revision >= 300: 0x4FFBFEC8
1580
+ if (this.chipRevision === null) {
1581
+ this.chipRevision = await this.getChipRevision();
1582
+ }
1583
+
1584
+ if (this.chipRevision < 300) {
1585
+ uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV0;
1586
+ } else {
1587
+ uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV300;
1588
+ }
1589
+ usbJtagSerialValue = ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1590
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32H2) {
1591
+ uartDevBufNo = ESP32H2_UARTDEV_BUF_NO;
1592
+ usbJtagSerialValue = ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1593
+ } else {
1594
+ return false;
1595
+ }
1596
+
1597
+ const uartNo = (await this.readRegister(uartDevBufNo)) & 0xff;
1598
+ return uartNo === usbJtagSerialValue;
1599
+ }
1600
+
1601
+ /**
1602
+ * Get chip revision for ESP32-C3
1603
+ * Reads from EFUSE registers and calculates revision
1604
+ */
1605
+ async getChipRevisionC3(): Promise<number> {
1606
+ if (this.chipFamily !== CHIP_FAMILY_ESP32C3) {
1607
+ return 0;
1608
+ }
1609
+
1610
+ // Read EFUSE_RD_MAC_SPI_SYS_3_REG (bits [20:18] = lower 3 bits of revision)
1611
+ const word3 = await this.readRegister(ESP32C3_EFUSE_RD_MAC_SPI_SYS_3_REG);
1612
+ const low = (word3 >> 18) & 0x07;
1613
+
1614
+ // Read EFUSE_RD_MAC_SPI_SYS_5_REG (bits [25:23] = upper 3 bits of revision)
1615
+ const word5 = await this.readRegister(ESP32C3_EFUSE_RD_MAC_SPI_SYS_5_REG);
1616
+ const hi = (word5 >> 23) & 0x07;
1617
+
1618
+ // Combine: upper 3 bits from word5, lower 3 bits from word3
1619
+ const revision = (hi << 3) | low;
1620
+
1621
+ return revision;
1622
+ }
1623
+
1624
+ /**
1625
+ * RTC watchdog timer reset for ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C5, ESP32-C6, and ESP32-P4
1626
+ * Uses specific registers for each chip family
1627
+ * Note: ESP32-H2 does NOT support WDT reset
1628
+ */
1629
+ public async rtcWdtResetChipSpecific(): Promise<void> {
1630
+ this.logger.debug("Hard resetting with watchdog timer...");
1367
1631
 
1368
- // Select correct register addresses based on chip family
1369
1632
  let WDTWPROTECT_REG: number;
1370
1633
  let WDTCONFIG0_REG: number;
1371
1634
  let WDTCONFIG1_REG: number;
@@ -1381,16 +1644,82 @@ export class ESPLoader extends EventTarget {
1381
1644
  WDTCONFIG0_REG = ESP32S3_RTC_CNTL_WDTCONFIG0_REG;
1382
1645
  WDTCONFIG1_REG = ESP32S3_RTC_CNTL_WDTCONFIG1_REG;
1383
1646
  WDT_WKEY = ESP32S3_RTC_CNTL_WDT_WKEY;
1647
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
1648
+ WDTWPROTECT_REG = ESP32C3_RTC_CNTL_WDTWPROTECT_REG;
1649
+ WDTCONFIG0_REG = ESP32C3_RTC_CNTL_WDTCONFIG0_REG;
1650
+ WDTCONFIG1_REG = ESP32C3_RTC_CNTL_WDTCONFIG1_REG;
1651
+ WDT_WKEY = ESP32C3_RTC_CNTL_WDT_WKEY;
1652
+ } else if (
1653
+ this.chipFamily === CHIP_FAMILY_ESP32C5 ||
1654
+ this.chipFamily === CHIP_FAMILY_ESP32C6
1655
+ ) {
1656
+ // C5 and C6 use LP_WDT (Low Power Watchdog Timer)
1657
+ WDTWPROTECT_REG = ESP32C5_C6_RTC_CNTL_WDTWPROTECT_REG;
1658
+ WDTCONFIG0_REG = ESP32C5_C6_RTC_CNTL_WDTCONFIG0_REG;
1659
+ WDTCONFIG1_REG = ESP32C5_C6_RTC_CNTL_WDTCONFIG1_REG;
1660
+ WDT_WKEY = ESP32C5_C6_RTC_CNTL_WDT_WKEY;
1661
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1662
+ // P4 uses LP_WDT (Low Power Watchdog Timer)
1663
+ WDTWPROTECT_REG = ESP32P4_RTC_CNTL_WDTWPROTECT_REG;
1664
+ WDTCONFIG0_REG = ESP32P4_RTC_CNTL_WDTCONFIG0_REG;
1665
+ WDTCONFIG1_REG = ESP32P4_RTC_CNTL_WDTCONFIG1_REG;
1666
+ WDT_WKEY = ESP32P4_RTC_CNTL_WDT_WKEY;
1384
1667
  } else {
1385
1668
  throw new Error(
1386
- `watchdogReset() is only supported for ESP32-S2 and ESP32-S3, not ${this.chipFamily}`,
1669
+ `rtcWdtResetChipSpecific() is not supported for ${this.chipFamily}`,
1387
1670
  );
1388
1671
  }
1389
1672
 
1390
1673
  // Unlock watchdog registers
1391
1674
  await this.writeRegister(WDTWPROTECT_REG, WDT_WKEY, undefined, 0);
1392
1675
 
1393
- // Set WDT timeout to 2000ms
1676
+ // Clear force download boot register (if applicable) BEFORE triggering WDT reset
1677
+ // This ensures the chip boots into firmware mode after reset
1678
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
1679
+ try {
1680
+ await this.writeRegister(
1681
+ ESP32S2_RTC_CNTL_OPTION1_REG,
1682
+ 0,
1683
+ ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
1684
+ 0,
1685
+ );
1686
+ this.logger.debug("Cleared force download boot mask");
1687
+ } catch (err) {
1688
+ this.logger.debug(
1689
+ `Expected error clearing force download boot mask: ${err}`,
1690
+ );
1691
+ }
1692
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1693
+ try {
1694
+ await this.writeRegister(
1695
+ ESP32S3_RTC_CNTL_OPTION1_REG,
1696
+ 0,
1697
+ ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
1698
+ 0,
1699
+ );
1700
+ this.logger.debug("Cleared force download boot mask");
1701
+ } catch (err) {
1702
+ this.logger.debug(
1703
+ `Expected error clearing force download boot mask: ${err}`,
1704
+ );
1705
+ }
1706
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1707
+ try {
1708
+ await this.writeRegister(
1709
+ ESP32P4_RTC_CNTL_OPTION1_REG,
1710
+ 0,
1711
+ ESP32P4_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
1712
+ 0,
1713
+ );
1714
+ this.logger.debug("Cleared force download boot mask");
1715
+ } catch (err) {
1716
+ this.logger.debug(
1717
+ `Expected error clearing force download boot mask: ${err}`,
1718
+ );
1719
+ }
1720
+ }
1721
+
1722
+ // Set WDT timeout to 2000ms (matches Python esptool)
1394
1723
  await this.writeRegister(WDTCONFIG1_REG, 2000, undefined, 0);
1395
1724
 
1396
1725
  // Enable WDT: bit 31 = enable, bits 28-30 = stage, bit 8 = sys reset, bits 0-2 = prescaler
@@ -1404,45 +1733,162 @@ export class ESPLoader extends EventTarget {
1404
1733
  await this.sleep(500);
1405
1734
  }
1406
1735
 
1736
+ /**
1737
+ * Helper: Check if USB-based WDT reset should be used for S2/S3
1738
+ * Returns true if WDT reset was performed, false otherwise
1739
+ */
1740
+ private async tryUsbWdtReset(
1741
+ chipName: string,
1742
+ GPIO_STRAP_REG: number,
1743
+ GPIO_STRAP_SPI_BOOT_MASK: number,
1744
+ RTC_CNTL_OPTION1_REG: number,
1745
+ RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK: number,
1746
+ ): Promise<boolean> {
1747
+ const isUsingUsbOtg = await this.usingUsbOtg();
1748
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
1749
+
1750
+ if (isUsingUsbOtg || isUsingUsbJtagSerial) {
1751
+ const strapReg = await this.readRegister(GPIO_STRAP_REG);
1752
+ const forceDlReg = await this.readRegister(RTC_CNTL_OPTION1_REG);
1753
+
1754
+ // Only use watchdog reset if GPIO0 is low AND force download boot mode is not set
1755
+ if (
1756
+ (strapReg & GPIO_STRAP_SPI_BOOT_MASK) === 0 &&
1757
+ (forceDlReg & RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK) === 0
1758
+ ) {
1759
+ await this.rtcWdtResetChipSpecific();
1760
+ this.logger.debug(
1761
+ `${chipName}: RTC WDT reset (USB detected, GPIO0 low)`,
1762
+ );
1763
+ return true;
1764
+ }
1765
+ }
1766
+ return false;
1767
+ }
1768
+
1769
+ /**
1770
+ * Chip-specific hard reset for ESP32-S2
1771
+ * Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
1772
+ */
1773
+ public async hardResetS2(): Promise<void> {
1774
+ const isUsingUsbOtg = await this.usingUsbOtg();
1775
+ if (isUsingUsbOtg) {
1776
+ await this.rtcWdtResetChipSpecific();
1777
+ this.logger.debug("ESP32-S2: RTC WDT reset (USB-OTG detected)");
1778
+ } else {
1779
+ // Use standard hardware reset
1780
+ await this.hardResetClassic();
1781
+ this.logger.debug("ESP32-S2: Classic reset");
1782
+ }
1783
+ }
1784
+
1785
+ /**
1786
+ * Chip-specific hard reset for ESP32-S3
1787
+ * Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
1788
+ */
1789
+ public async hardResetS3(): Promise<void> {
1790
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
1791
+ if (isUsingUsbJtagSerial) {
1792
+ await this.rtcWdtResetChipSpecific();
1793
+ this.logger.debug("ESP32-S3: RTC WDT reset (USB-JTAG/Serial detected)");
1794
+ } else {
1795
+ // Use standard hardware reset
1796
+ await this.hardResetClassic();
1797
+ this.logger.debug("ESP32-S3: Classic reset");
1798
+ }
1799
+ }
1800
+
1801
+ /**
1802
+ * Chip-specific hard reset for ESP32-C3
1803
+ * Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
1804
+ */
1805
+ public async hardResetC3(): Promise<void> {
1806
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
1807
+ if (isUsingUsbJtagSerial) {
1808
+ await this.rtcWdtResetChipSpecific();
1809
+ this.logger.debug("ESP32-C3: RTC WDT reset (USB-JTAG/Serial detected)");
1810
+ } else {
1811
+ // Use standard hardware reset
1812
+ await this.hardResetClassic();
1813
+ this.logger.debug("ESP32-C3: Classic reset");
1814
+ }
1815
+ }
1816
+
1407
1817
  async hardReset(bootloader = false) {
1818
+ // In console mode, only allow simple hardware reset (no bootloader entry)
1819
+ if (this._consoleMode) {
1820
+ if (bootloader) {
1821
+ this.logger.debug(
1822
+ "Skipping bootloader reset - device is in console mode",
1823
+ );
1824
+ return;
1825
+ }
1826
+ // Simple hardware reset to restart firmware (IO0=HIGH)
1827
+ this.logger.debug("Performing hardware reset (console mode)...");
1828
+ if (this.isWebUSB()) {
1829
+ await this.hardResetToFirmwareWebUSB();
1830
+ } else {
1831
+ await this.hardResetToFirmware();
1832
+ }
1833
+ this.logger.debug("Hardware reset complete");
1834
+ return;
1835
+ }
1836
+
1408
1837
  if (bootloader) {
1409
1838
  // enter flash mode
1410
1839
  if (this.port.getInfo().usbProductId === USB_JTAG_SERIAL_PID) {
1411
1840
  await this.hardResetUSBJTAGSerial();
1412
- this.logger.log("USB-JTAG/Serial reset.");
1841
+ this.logger.debug("USB-JTAG/Serial reset.");
1413
1842
  } else {
1414
1843
  // Use different reset strategy for WebUSB (Android) vs Web Serial (Desktop)
1415
1844
  if (this.isWebUSB()) {
1416
1845
  await this.hardResetClassicWebUSB();
1417
- this.logger.log("Classic reset (WebUSB/Android).");
1846
+ this.logger.debug("Classic reset (WebUSB/Android).");
1418
1847
  } else {
1419
1848
  await this.hardResetClassic();
1420
- this.logger.log("Classic reset.");
1849
+ this.logger.debug("Classic reset.");
1421
1850
  }
1422
1851
  }
1423
1852
  } else {
1424
1853
  // just reset (no bootloader mode)
1425
- // For ESP32-S2/S3 with USB-OTG, use watchdog reset instead of DTR/RTS
1426
- if (
1427
- this.port.getInfo().usbProductId === USB_JTAG_SERIAL_PID &&
1428
- (this.chipFamily === CHIP_FAMILY_ESP32S2 ||
1429
- this.chipFamily === CHIP_FAMILY_ESP32S3)
1854
+ // For ESP32-S2/S3 with USB-OTG or USB-JTAG/Serial, check if watchdog reset is needed
1855
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2 && !this._consoleMode) {
1856
+ const wdtResetUsed = await this.tryUsbWdtReset(
1857
+ "ESP32-S2",
1858
+ ESP32S2_GPIO_STRAP_REG,
1859
+ ESP32S2_GPIO_STRAP_SPI_BOOT_MASK,
1860
+ ESP32S2_RTC_CNTL_OPTION1_REG,
1861
+ ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
1862
+ );
1863
+ if (wdtResetUsed) return;
1864
+ } else if (
1865
+ this.chipFamily === CHIP_FAMILY_ESP32S3 &&
1866
+ !this._consoleMode
1430
1867
  ) {
1431
- await this.watchdogReset();
1432
- this.logger.log("Watchdog reset (USB-OTG).");
1433
- } else if (this.isWebUSB()) {
1868
+ const wdtResetUsed = await this.tryUsbWdtReset(
1869
+ "ESP32-S3",
1870
+ ESP32S3_GPIO_STRAP_REG,
1871
+ ESP32S3_GPIO_STRAP_SPI_BOOT_MASK,
1872
+ ESP32S3_RTC_CNTL_OPTION1_REG,
1873
+ ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
1874
+ );
1875
+ if (wdtResetUsed) return;
1876
+ }
1877
+
1878
+ // Standard reset for all other cases
1879
+ if (this.isWebUSB()) {
1434
1880
  // WebUSB: Use longer delays for better compatibility
1435
1881
  await this.setRTSWebUSB(true); // EN->LOW
1436
1882
  await this.sleep(200);
1437
1883
  await this.setRTSWebUSB(false);
1438
1884
  await this.sleep(200);
1439
- this.logger.log("Hard reset (WebUSB).");
1885
+ this.logger.debug("Hard reset (WebUSB).");
1440
1886
  } else {
1441
1887
  // Web Serial: Standard reset
1442
1888
  await this.setRTS(true); // EN->LOW
1443
1889
  await this.sleep(100);
1444
1890
  await this.setRTS(false);
1445
- this.logger.log("Hard reset.");
1891
+ this.logger.debug("Hard reset.");
1446
1892
  }
1447
1893
  }
1448
1894
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -2859,6 +3305,256 @@ export class ESPLoader extends EventTarget {
2859
3305
  }
2860
3306
  }
2861
3307
 
3308
+ /**
3309
+ * @name releaseReaderWriter
3310
+ * Release reader and writer locks without closing the port
3311
+ * Used when switching to console mode
3312
+ */
3313
+ async releaseReaderWriter() {
3314
+ if (this._parent) {
3315
+ await this._parent.releaseReaderWriter();
3316
+ return;
3317
+ }
3318
+
3319
+ // Check if device is in JTAG mode and needs reset to boot into firmware
3320
+ const didReconnect = await this._resetToFirmwareIfNeeded();
3321
+
3322
+ // If we reconnected for console, the reader/writer are already released and restarted
3323
+ if (didReconnect) {
3324
+ return;
3325
+ }
3326
+
3327
+ // Wait for pending writes to complete
3328
+ try {
3329
+ await this._writeChain;
3330
+ } catch (err) {
3331
+ this.logger.debug(`Pending write error during release: ${err}`);
3332
+ }
3333
+
3334
+ // Release writer
3335
+ if (this._writer) {
3336
+ try {
3337
+ this._writer.releaseLock();
3338
+ this.logger.debug("Writer released");
3339
+ } catch (err) {
3340
+ this.logger.debug(`Writer release error: ${err}`);
3341
+ }
3342
+ this._writer = undefined;
3343
+ }
3344
+
3345
+ // Cancel and release reader
3346
+ if (this._reader) {
3347
+ const reader = this._reader;
3348
+ try {
3349
+ // Suppress disconnect event during console mode switching
3350
+ this._suppressDisconnect = true;
3351
+ await reader.cancel();
3352
+ this.logger.debug("Reader cancelled");
3353
+ } catch (err) {
3354
+ this.logger.debug(`Reader cancel error: ${err}`);
3355
+ } finally {
3356
+ try {
3357
+ reader.releaseLock();
3358
+ } catch (err) {
3359
+ this.logger.debug(`Reader release error: ${err}`);
3360
+ }
3361
+ }
3362
+ if (this._reader === reader) {
3363
+ this._reader = undefined;
3364
+ }
3365
+ }
3366
+ }
3367
+
3368
+ /**
3369
+ * @name resetToFirmware
3370
+ * Public method to reset device from bootloader to firmware for console mode
3371
+ * Automatically detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
3372
+ * @returns true if reset was performed, false if not needed
3373
+ */
3374
+ public async resetToFirmware(): Promise<boolean> {
3375
+ return await this._resetToFirmwareIfNeeded();
3376
+ }
3377
+
3378
+ /**
3379
+ * @name detectUsbConnectionType
3380
+ * Detect if device is using USB-JTAG/Serial or USB-OTG (not external serial chip)
3381
+ * This helper extracts the detection logic from initialize() for reuse
3382
+ * @returns true if USB-JTAG or USB-OTG, false if external serial chip
3383
+ * @throws Error if detection fails and chipFamily is not set
3384
+ */
3385
+ private async detectUsbConnectionType(): Promise<boolean> {
3386
+ if (!this.chipFamily) {
3387
+ throw new Error("Cannot detect USB connection type: chipFamily not set");
3388
+ }
3389
+
3390
+ if (
3391
+ this.chipFamily === CHIP_FAMILY_ESP32S2 ||
3392
+ this.chipFamily === CHIP_FAMILY_ESP32S3
3393
+ ) {
3394
+ const isUsingUsbOtg = await this.usingUsbOtg();
3395
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
3396
+ return isUsingUsbOtg || isUsingUsbJtagSerial;
3397
+ } else if (
3398
+ this.chipFamily === CHIP_FAMILY_ESP32C3 ||
3399
+ this.chipFamily === CHIP_FAMILY_ESP32C5 ||
3400
+ this.chipFamily === CHIP_FAMILY_ESP32C6
3401
+ ) {
3402
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
3403
+ return isUsingUsbJtagSerial;
3404
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
3405
+ const isUsingUsbOtg = await this.usingUsbOtg();
3406
+ const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
3407
+ return isUsingUsbOtg || isUsingUsbJtagSerial;
3408
+ } else {
3409
+ // Other chips don't have USB-JTAG/OTG
3410
+ return false;
3411
+ }
3412
+ }
3413
+
3414
+ /**
3415
+ * @name enterConsoleMode
3416
+ * Prepare device for console mode by resetting to firmware
3417
+ * Handles both USB-JTAG/OTG devices (closes port) and external serial chips (keeps port open)
3418
+ * @returns true if port was closed (USB-JTAG), false if port stays open (serial chip)
3419
+ */
3420
+ public async enterConsoleMode(): Promise<boolean> {
3421
+ // Set console mode flag
3422
+ this._consoleMode = true;
3423
+
3424
+ // Re-detect USB connection type to ensure we have a definitive value
3425
+ // This handles cases where isUsbJtagOrOtg might be undefined
3426
+ let isUsbJtag: boolean;
3427
+ try {
3428
+ isUsbJtag = await this.detectUsbConnectionType();
3429
+ this.logger.debug(
3430
+ `USB connection type detected: ${isUsbJtag ? "USB-JTAG/OTG" : "External Serial Chip"}`,
3431
+ );
3432
+ } catch (err) {
3433
+ // If detection fails, fall back to cached value or fail-fast
3434
+ if (this.isUsbJtagOrOtg === undefined) {
3435
+ throw new Error(
3436
+ `Cannot enter console mode: USB connection type unknown and detection failed: ${err}`,
3437
+ );
3438
+ }
3439
+ this.logger.debug(
3440
+ `USB detection failed, using cached value: ${this.isUsbJtagOrOtg}`,
3441
+ );
3442
+ isUsbJtag = this.isUsbJtagOrOtg;
3443
+ }
3444
+
3445
+ if (isUsbJtag) {
3446
+ // USB-JTAG/OTG devices: Use watchdog reset which closes port
3447
+ const wasReset = await this._resetToFirmwareIfNeeded();
3448
+ return wasReset; // true = port closed, caller must reopen
3449
+ } else {
3450
+ // External serial chip devices: Release locks and do simple reset
3451
+ try {
3452
+ await this.releaseReaderWriter();
3453
+ await this.sleep(100);
3454
+ } catch (err) {
3455
+ this.logger.debug(`Failed to release locks: ${err}`);
3456
+ }
3457
+
3458
+ // Hardware reset to firmware mode (IO0=HIGH)
3459
+ try {
3460
+ await this.hardReset(false);
3461
+ this.logger.log("Device reset to firmware mode");
3462
+ } catch (err) {
3463
+ this.logger.debug(`Could not reset device: ${err}`);
3464
+ }
3465
+
3466
+ return false; // Port stays open
3467
+ }
3468
+ }
3469
+
3470
+ /**
3471
+ * @name _resetToFirmwareIfNeeded
3472
+ * Reset device from bootloader to firmware when switching to console mode
3473
+ * Detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
3474
+ * @returns true if reconnect was performed, false otherwise
3475
+ */
3476
+ private async _resetToFirmwareIfNeeded(): Promise<boolean> {
3477
+ try {
3478
+ // Check if device is using USB-JTAG/Serial or USB-OTG
3479
+ // Value should already be set during main() connection
3480
+ // Use getter to access parent's value if this is a stub
3481
+ const needsReset = this.isUsbJtagOrOtg === true;
3482
+
3483
+ if (needsReset) {
3484
+ const resetMethod =
3485
+ this.chipFamily === CHIP_FAMILY_ESP32S2 ||
3486
+ this.chipFamily === CHIP_FAMILY_ESP32S3
3487
+ ? "USB-JTAG/Serial or USB-OTG"
3488
+ : "USB-JTAG/Serial";
3489
+
3490
+ this.logger.log(
3491
+ `Resetting ${this.chipFamily} (${resetMethod}) to boot into firmware...`,
3492
+ );
3493
+
3494
+ // Set console mode flag before reset to prevent subsequent hardReset calls
3495
+ this._consoleMode = true;
3496
+
3497
+ // For S2/S3: Clear force download boot mask before WDT reset
3498
+ if (
3499
+ this.chipFamily === CHIP_FAMILY_ESP32S2 ||
3500
+ this.chipFamily === CHIP_FAMILY_ESP32S3
3501
+ ) {
3502
+ const OPTION1_REG =
3503
+ this.chipFamily === CHIP_FAMILY_ESP32S2
3504
+ ? ESP32S2_RTC_CNTL_OPTION1_REG
3505
+ : ESP32S3_RTC_CNTL_OPTION1_REG;
3506
+ const FORCE_DOWNLOAD_BOOT_MASK =
3507
+ this.chipFamily === CHIP_FAMILY_ESP32S2
3508
+ ? ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK
3509
+ : ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK;
3510
+
3511
+ try {
3512
+ // Clear force download boot mode to avoid chip being stuck in download mode
3513
+ await this.writeRegister(
3514
+ OPTION1_REG,
3515
+ 0,
3516
+ FORCE_DOWNLOAD_BOOT_MASK,
3517
+ 0,
3518
+ );
3519
+ this.logger.debug("Cleared force download boot mask");
3520
+ } catch (err) {
3521
+ this.logger.debug(
3522
+ `Expected error clearing force download boot mask: ${err}`,
3523
+ );
3524
+ }
3525
+ }
3526
+
3527
+ // Perform watchdog reset to reboot into firmware
3528
+ try {
3529
+ await this.rtcWdtResetChipSpecific();
3530
+ this.logger.debug("Watchdog reset triggered successfully");
3531
+ } catch (err) {
3532
+ // Error is expected - device resets before responding
3533
+ this.logger.debug(
3534
+ `Watchdog reset initiated (connection lost as expected: ${err})`,
3535
+ );
3536
+ }
3537
+
3538
+ // Wait for device to fully boot into firmware
3539
+ this.logger.log("Waiting for device to boot into firmware...");
3540
+ await this.sleep(1000);
3541
+
3542
+ // After WDT reset, streams are dead/locked - don't try to manipulate them
3543
+ // Just mark everything as disconnected and let browser clean up
3544
+ this.connected = false;
3545
+ this._writer = undefined;
3546
+ this._reader = undefined;
3547
+
3548
+ this.logger.debug("Device reset to firmware mode (port closed)");
3549
+ return true;
3550
+ }
3551
+ } catch (err) {
3552
+ this.logger.debug(`Could not reset device to firmware mode: ${err}`);
3553
+ // Continue anyway - console mode might still work
3554
+ }
3555
+ return false;
3556
+ }
3557
+
2862
3558
  /**
2863
3559
  * @name reconnectAndResume
2864
3560
  * Reconnect the serial port to flush browser buffers and reload stub
@@ -2871,6 +3567,7 @@ export class ESPLoader extends EventTarget {
2871
3567
 
2872
3568
  try {
2873
3569
  this.logger.log("Reconnecting serial port...");
3570
+ const savedBaudRate = this._currentBaudRate;
2874
3571
 
2875
3572
  this.connected = false;
2876
3573
  this.__inputBuffer = [];
@@ -2909,7 +3606,7 @@ export class ESPLoader extends EventTarget {
2909
3606
  // Close port
2910
3607
  try {
2911
3608
  await this.port.close();
2912
- this.logger.log("Port closed");
3609
+ this.logger.debug("Port closed");
2913
3610
  } catch (err) {
2914
3611
  this.logger.debug(`Port close error: ${err}`);
2915
3612
  }
@@ -2919,6 +3616,7 @@ export class ESPLoader extends EventTarget {
2919
3616
  try {
2920
3617
  await this.port.open({ baudRate: ESP_ROM_BAUD });
2921
3618
  this.connected = true;
3619
+ this._currentBaudRate = ESP_ROM_BAUD;
2922
3620
  } catch (err) {
2923
3621
  throw new Error(`Failed to open port: ${err}`);
2924
3622
  }
@@ -2972,8 +3670,8 @@ export class ESPLoader extends EventTarget {
2972
3670
  this.logger.debug("Stub loaded");
2973
3671
 
2974
3672
  // Restore baudrate if it was changed
2975
- if (this._currentBaudRate !== ESP_ROM_BAUD) {
2976
- await stubLoader.setBaudrate(this._currentBaudRate);
3673
+ if (savedBaudRate !== ESP_ROM_BAUD) {
3674
+ await stubLoader.setBaudrate(savedBaudRate);
2977
3675
 
2978
3676
  // Verify port is still ready after baudrate change
2979
3677
  if (!this.port.writable || !this.port.readable) {
@@ -3010,6 +3708,9 @@ export class ESPLoader extends EventTarget {
3010
3708
  try {
3011
3709
  this.logger.log("Reconnecting to bootloader mode...");
3012
3710
 
3711
+ // Clear console mode flag when reconnecting to bootloader
3712
+ this._consoleMode = false;
3713
+
3013
3714
  this.connected = false;
3014
3715
  this.__inputBuffer = [];
3015
3716
  this.__inputBufferReadIndex = 0;
@@ -3047,7 +3748,7 @@ export class ESPLoader extends EventTarget {
3047
3748
  // Close port
3048
3749
  try {
3049
3750
  await this.port.close();
3050
- this.logger.log("Port closed");
3751
+ this.logger.debug("Port closed");
3051
3752
  } catch (err) {
3052
3753
  this.logger.debug(`Port close error: ${err}`);
3053
3754
  }
@@ -3057,6 +3758,7 @@ export class ESPLoader extends EventTarget {
3057
3758
  try {
3058
3759
  await this.port.open({ baudRate: ESP_ROM_BAUD });
3059
3760
  this.connected = true;
3761
+ this._currentBaudRate = ESP_ROM_BAUD;
3060
3762
  } catch (err) {
3061
3763
  throw new Error(`Failed to open port: ${err}`);
3062
3764
  }
@@ -3074,6 +3776,8 @@ export class ESPLoader extends EventTarget {
3074
3776
  // Reset chip info and stub state
3075
3777
  this.__chipFamily = undefined;
3076
3778
  this.chipName = "Unknown Chip";
3779
+ this.chipRevision = null;
3780
+ this.chipVariant = null;
3077
3781
  this.IS_STUB = false;
3078
3782
 
3079
3783
  // Start read loop
@@ -3179,6 +3883,10 @@ export class ESPLoader extends EventTarget {
3179
3883
  * @param addr - Address to read from
3180
3884
  * @param size - Number of bytes to read
3181
3885
  * @param onPacketReceived - Optional callback function called when packet is received
3886
+ * @param options - Optional parameters for advanced control
3887
+ * - chunkSize: Amount of data to request from ESP in one command (bytes)
3888
+ * - blockSize: Size of each data block sent by ESP (bytes)
3889
+ * - maxInFlight: Maximum unacknowledged bytes (bytes)
3182
3890
  * @returns Uint8Array containing the flash data
3183
3891
  */
3184
3892
  async readFlash(
@@ -3189,6 +3897,11 @@ export class ESPLoader extends EventTarget {
3189
3897
  progress: number,
3190
3898
  totalSize: number,
3191
3899
  ) => void,
3900
+ options?: {
3901
+ chunkSize?: number;
3902
+ blockSize?: number;
3903
+ maxInFlight?: number;
3904
+ },
3192
3905
  ): Promise<Uint8Array> {
3193
3906
  if (!this.IS_STUB) {
3194
3907
  throw new Error(
@@ -3228,7 +3941,13 @@ export class ESPLoader extends EventTarget {
3228
3941
  // For WebUSB (Android), use smaller chunks to avoid timeouts and buffer issues
3229
3942
  // For Web Serial (Desktop), use larger chunks for better performance
3230
3943
  let CHUNK_SIZE: number;
3231
- if (this.isWebUSB()) {
3944
+ if (options?.chunkSize !== undefined) {
3945
+ // Use user-provided chunkSize if in advanced mode
3946
+ CHUNK_SIZE = options.chunkSize;
3947
+ this.logger.log(
3948
+ `Using custom chunk size: 0x${CHUNK_SIZE.toString(16)} bytes`,
3949
+ );
3950
+ } else if (this.isWebUSB()) {
3232
3951
  // WebUSB: Use smaller chunks to avoid SLIP timeout issues
3233
3952
  CHUNK_SIZE = 0x4 * 0x1000; // 4KB = 16384 bytes
3234
3953
  } else {
@@ -3263,7 +3982,19 @@ export class ESPLoader extends EventTarget {
3263
3982
  let blockSize: number;
3264
3983
  let maxInFlight: number;
3265
3984
 
3266
- if (this.isWebUSB()) {
3985
+ if (
3986
+ options?.blockSize !== undefined &&
3987
+ options?.maxInFlight !== undefined
3988
+ ) {
3989
+ // Use user-provided values if in advanced mode
3990
+ blockSize = options.blockSize;
3991
+ maxInFlight = options.maxInFlight;
3992
+ if (retryCount === 0) {
3993
+ this.logger.debug(
3994
+ `Using custom parameters: blockSize=${blockSize}, maxInFlight=${maxInFlight}`,
3995
+ );
3996
+ }
3997
+ } else if (this.isWebUSB()) {
3267
3998
  // WebUSB (Android): All devices use adaptive speed
3268
3999
  // All have maxTransferSize=64, baseBlockSize=31
3269
4000
  const maxTransferSize =
@@ -3455,7 +4186,7 @@ export class ESPLoader extends EventTarget {
3455
4186
  // Check if it's a timeout error or SLIP error
3456
4187
  if (err instanceof SlipReadError) {
3457
4188
  if (retryCount <= MAX_RETRIES) {
3458
- this.logger.log(
4189
+ this.logger.debug(
3459
4190
  `${err.message} at 0x${currentAddr.toString(16)}. Draining buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`,
3460
4191
  );
3461
4192