tasmota-webserial-esptool 9.2.19 → 9.2.21

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
@@ -58,7 +58,17 @@ import {
58
58
  CHIP_DETECT_MAGIC_REG_ADDR,
59
59
  CHIP_DETECT_MAGIC_VALUES,
60
60
  CHIP_ID_TO_INFO,
61
+ ESP32_BASEFUSEADDR,
62
+ ESP32_APB_CTL_DATE_ADDR,
63
+ ESP32S2_EFUSE_BLOCK1_ADDR,
64
+ ESP32S3_EFUSE_BLOCK1_ADDR,
65
+ ESP32C2_EFUSE_BLOCK2_ADDR,
66
+ ESP32C5_EFUSE_BLOCK1_ADDR,
67
+ ESP32C6_EFUSE_BLOCK1_ADDR,
68
+ ESP32C61_EFUSE_BLOCK1_ADDR,
69
+ ESP32H2_EFUSE_BLOCK1_ADDR,
61
70
  ESP32P4_EFUSE_BLOCK1_ADDR,
71
+ ESP32S31_EFUSE_BLOCK1_ADDR,
62
72
  SlipReadError,
63
73
  ESP32S2_RTC_CNTL_WDTWPROTECT_REG,
64
74
  ESP32S2_RTC_CNTL_WDTCONFIG0_REG,
@@ -74,14 +84,10 @@ import {
74
84
  ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
75
85
  ESP32C3_EFUSE_RD_MAC_SPI_SYS_3_REG,
76
86
  ESP32C3_EFUSE_RD_MAC_SPI_SYS_5_REG,
77
- ESP32C3_RTC_CNTL_WDTWPROTECT_REG,
78
- ESP32C3_RTC_CNTL_WDTCONFIG0_REG,
79
- ESP32C3_RTC_CNTL_WDTCONFIG1_REG,
80
- ESP32C3_RTC_CNTL_WDT_WKEY,
81
- ESP32C5_C6_RTC_CNTL_WDTWPROTECT_REG,
82
- ESP32C5_C6_RTC_CNTL_WDTCONFIG0_REG,
83
- ESP32C5_C6_RTC_CNTL_WDTCONFIG1_REG,
84
- ESP32C5_C6_RTC_CNTL_WDT_WKEY,
87
+ ESP32C5_UART_CLKDIV_REG,
88
+ ESP32C5_PCR_SYSCLK_CONF_REG,
89
+ ESP32C5_PCR_SYSCLK_XTAL_FREQ_V,
90
+ ESP32C5_PCR_SYSCLK_XTAL_FREQ_S,
85
91
  ESP32P4_RTC_CNTL_WDTWPROTECT_REG,
86
92
  ESP32P4_RTC_CNTL_WDTCONFIG0_REG,
87
93
  ESP32P4_RTC_CNTL_WDTCONFIG1_REG,
@@ -95,17 +101,13 @@ import {
95
101
  ESP32P4_PMU_0P1A_TARGET0_0,
96
102
  ESP32P4_PMU_0P1A_FORCE_TIEH_SEL_0,
97
103
  ESP32P4_PMU_DATE_REG,
98
- ESP32C5_PCR_SYSCLK_CONF_REG,
99
- ESP32C5_PCR_SYSCLK_XTAL_FREQ_V,
100
- ESP32C5_PCR_SYSCLK_XTAL_FREQ_S,
101
- ESP32C5_UART_CLKDIV_REG,
102
104
  ESP32S2_UARTDEV_BUF_NO,
103
105
  ESP32S2_UARTDEV_BUF_NO_USB_OTG,
104
106
  ESP32S3_UARTDEV_BUF_NO,
105
- ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
106
107
  ESP32S3_UARTDEV_BUF_NO_USB_OTG,
107
- ESP32C3_BUF_UART_NO_OFFSET,
108
+ ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
108
109
  ESP32C3_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
110
+ ESP32C3_BUF_UART_NO_OFFSET,
109
111
  ESP32C5_UARTDEV_BUF_NO,
110
112
  ESP32C5_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
111
113
  ESP32C6_UARTDEV_BUF_NO,
@@ -120,11 +122,11 @@ import {
120
122
  ESP32H4_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
121
123
  ESP32P4_UARTDEV_BUF_NO_REV0,
122
124
  ESP32P4_UARTDEV_BUF_NO_REV300,
123
- ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
124
125
  ESP32P4_UARTDEV_BUF_NO_USB_OTG,
126
+ ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
125
127
  } from "./const";
126
128
  import { getStubCode } from "./stubs";
127
- import { hexFormatter, sleep, slipEncode, toHex } from "./util";
129
+ import { hexFormatter, padTo, sleep, slipEncode, toHex } from "./util";
128
130
  import { deflate } from "pako";
129
131
  import { pack, unpack } from "./struct";
130
132
 
@@ -548,7 +550,15 @@ export class ESPLoader extends EventTarget {
548
550
  );
549
551
  } catch (err) {
550
552
  this.logger.debug(`Could not detect USB connection type: ${err}`);
551
- // Leave as undefined if detection fails
553
+ }
554
+
555
+ try {
556
+ const usbMode = await this.getUsbMode();
557
+ this.logger.debug(
558
+ `USB mode (register): ${usbMode.mode} (uartNo=${usbMode.uartNo})`,
559
+ );
560
+ } catch (err) {
561
+ this.logger.debug(`Could not detect USB mode: ${err}`);
552
562
  }
553
563
 
554
564
  // Read the OTP data for this chip and store into this.efuses array
@@ -557,7 +567,11 @@ export class ESPLoader extends EventTarget {
557
567
  for (let i = 0; i < 4; i++) {
558
568
  this._efuses[i] = await this.readRegister(AddrMAC + 4 * i);
559
569
  }
560
- this.logger.log(`Chip type ${this.chipName}`);
570
+ const revisionInfo =
571
+ this.chipRevision !== null && this.chipRevision !== undefined
572
+ ? ` (revision ${this.chipRevision})`
573
+ : "";
574
+ this.logger.log(`Connected to ${this.chipName}${revisionInfo}`);
561
575
  this.logger.debug(
562
576
  `Bootloader flash offset: 0x${FlAddr.flashOffs.toString(16)}`,
563
577
  );
@@ -580,21 +594,16 @@ export class ESPLoader extends EventTarget {
580
594
  this.chipName = chipInfo.name;
581
595
  this.chipFamily = chipInfo.family;
582
596
 
583
- // Get chip revision for ESP32-P4 and ESP32-C3
584
- if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
585
- this.chipRevision = await this.getChipRevision();
586
- this.logger.debug(`ESP32-P4 revision: ${this.chipRevision}`);
597
+ this.chipRevision = await this.getChipRevision();
598
+ this.logger.debug(`${this.chipName} revision: ${this.chipRevision}`);
587
599
 
588
- // Set chip variant based on revision
589
- if (this.chipRevision >= 300) {
590
- this.chipVariant = "rev300";
591
- } else {
592
- this.chipVariant = "rev0";
593
- }
594
- this.logger.debug(`ESP32-P4 variant: ${this.chipVariant}`);
595
- } else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
596
- this.chipRevision = await this.getChipRevision();
597
- this.logger.debug(`ESP32-C3 revision: ${this.chipRevision}`);
600
+ if (
601
+ this.chipFamily === CHIP_FAMILY_ESP32P4 &&
602
+ this.chipRevision >= 300
603
+ ) {
604
+ this.chipVariant = "rev300";
605
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
606
+ this.chipVariant = "rev0";
598
607
  }
599
608
 
600
609
  this.logger.debug(
@@ -644,17 +653,12 @@ export class ESPLoader extends EventTarget {
644
653
  this.chipName = chip.name;
645
654
  this.chipFamily = chip.family;
646
655
 
647
- if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
648
- this.chipRevision = await this.getChipRevision();
656
+ this.chipRevision = await this.getChipRevision();
657
+ this.logger.debug(`${this.chipName} revision: ${this.chipRevision}`);
649
658
 
650
- if (this.chipRevision >= 300) {
651
- this.chipVariant = "rev300";
652
- } else {
653
- this.chipVariant = "rev0";
654
- }
659
+ if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
660
+ this.chipVariant = this.chipRevision >= 300 ? "rev300" : "rev0";
655
661
  this.logger.debug(`ESP32-P4 variant: ${this.chipVariant}`);
656
- } else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
657
- this.chipRevision = await this.getChipRevision();
658
662
  }
659
663
 
660
664
  this.logger.debug(
@@ -662,28 +666,102 @@ export class ESPLoader extends EventTarget {
662
666
  );
663
667
  }
664
668
 
665
- /**
666
- * Get chip revision for ESP32-P4
667
- */
668
669
  async getChipRevision(): Promise<number> {
669
- if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
670
- // Read from EFUSE_BLOCK1 to get chip revision
671
- // Word 2 contains revision info for ESP32-P4
672
- const word2 = await this.readRegister(ESP32P4_EFUSE_BLOCK1_ADDR + 8);
673
-
674
- // Minor revision: bits [3:0]
675
- const minorRev = word2 & 0x0f;
676
-
677
- // Major revision: bits [23] << 2 | bits [5:4]
678
- const majorRev = (((word2 >> 23) & 1) << 2) | ((word2 >> 4) & 0x03);
679
-
680
- // Revision is major * 100 + minor
681
- return majorRev * 100 + minorRev;
682
- } else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
683
- return await this.getChipRevisionC3();
670
+ let minor = 0;
671
+ let major = 0;
672
+
673
+ switch (this.chipFamily) {
674
+ case CHIP_FAMILY_ESP32: {
675
+ const efuse3 = await this.readRegister(ESP32_BASEFUSEADDR + 4 * 3);
676
+ const efuse5 = await this.readRegister(ESP32_BASEFUSEADDR + 4 * 5);
677
+ minor = (efuse5 >> 24) & 0x3;
678
+ const revBit0 = (efuse3 >> 15) & 0x1;
679
+ const revBit1 = (efuse5 >> 20) & 0x1;
680
+ const apb = await this.readRegister(ESP32_APB_CTL_DATE_ADDR);
681
+ const revBit2 = (apb >> 31) & 0x1;
682
+ const combined = (revBit2 << 2) | (revBit1 << 1) | revBit0;
683
+ major =
684
+ ({ 0: 0, 1: 1, 3: 2, 7: 3 } as Record<number, number>)[combined] ?? 0;
685
+ break;
686
+ }
687
+ case CHIP_FAMILY_ESP32S2: {
688
+ const w3 = await this.readRegister(ESP32S2_EFUSE_BLOCK1_ADDR + 4 * 3);
689
+ const w4 = await this.readRegister(ESP32S2_EFUSE_BLOCK1_ADDR + 4 * 4);
690
+ const hi = (w3 >> 20) & 0x01;
691
+ const lo = (w4 >> 4) & 0x07;
692
+ minor = (hi << 3) + lo;
693
+ major = (w3 >> 18) & 0x03;
694
+ break;
695
+ }
696
+ case CHIP_FAMILY_ESP32S3: {
697
+ const w3 = await this.readRegister(ESP32S3_EFUSE_BLOCK1_ADDR + 4 * 3);
698
+ const w5 = await this.readRegister(ESP32S3_EFUSE_BLOCK1_ADDR + 4 * 5);
699
+ const hi = (w5 >> 23) & 0x01;
700
+ const lo = (w3 >> 18) & 0x07;
701
+ minor = (hi << 3) + lo;
702
+ major = (w5 >> 24) & 0x03;
703
+ break;
704
+ }
705
+ case CHIP_FAMILY_ESP32C2: {
706
+ const w1 = await this.readRegister(ESP32C2_EFUSE_BLOCK2_ADDR + 4 * 1);
707
+ minor = (w1 >> 16) & 0x0f;
708
+ major = (w1 >> 20) & 0x03;
709
+ break;
710
+ }
711
+ case CHIP_FAMILY_ESP32C3: {
712
+ const w3 = await this.readRegister(ESP32C3_EFUSE_RD_MAC_SPI_SYS_3_REG);
713
+ const w5 = await this.readRegister(ESP32C3_EFUSE_RD_MAC_SPI_SYS_5_REG);
714
+ const hi = (w5 >> 23) & 0x01;
715
+ const lo = (w3 >> 18) & 0x07;
716
+ minor = (hi << 3) + lo;
717
+ major = (w5 >> 24) & 0x03;
718
+ break;
719
+ }
720
+ case CHIP_FAMILY_ESP32C5: {
721
+ const w2 = await this.readRegister(ESP32C5_EFUSE_BLOCK1_ADDR + 4 * 2);
722
+ minor = w2 & 0x0f;
723
+ major = (w2 >> 4) & 0x03;
724
+ break;
725
+ }
726
+ case CHIP_FAMILY_ESP32C6: {
727
+ const w3 = await this.readRegister(ESP32C6_EFUSE_BLOCK1_ADDR + 4 * 3);
728
+ minor = (w3 >> 18) & 0x0f;
729
+ major = (w3 >> 22) & 0x03;
730
+ break;
731
+ }
732
+ case CHIP_FAMILY_ESP32C61: {
733
+ const w2 = await this.readRegister(ESP32C61_EFUSE_BLOCK1_ADDR + 4 * 2);
734
+ minor = w2 & 0x0f;
735
+ major = (w2 >> 4) & 0x03;
736
+ break;
737
+ }
738
+ case CHIP_FAMILY_ESP32H2: {
739
+ const w3 = await this.readRegister(ESP32H2_EFUSE_BLOCK1_ADDR + 4 * 3);
740
+ minor = (w3 >> 18) & 0x07;
741
+ major = (w3 >> 21) & 0x03;
742
+ break;
743
+ }
744
+ case CHIP_FAMILY_ESP32H4: {
745
+ break;
746
+ }
747
+ case CHIP_FAMILY_ESP32H21: {
748
+ break;
749
+ }
750
+ case CHIP_FAMILY_ESP32P4: {
751
+ const w2 = await this.readRegister(ESP32P4_EFUSE_BLOCK1_ADDR + 4 * 2);
752
+ minor = w2 & 0x0f;
753
+ major = (((w2 >> 23) & 1) << 2) | ((w2 >> 4) & 0x03);
754
+ break;
755
+ }
756
+ case CHIP_FAMILY_ESP32S31: {
757
+ const w2 = await this.readRegister(ESP32S31_EFUSE_BLOCK1_ADDR + 4 * 2);
758
+ minor = w2 & 0x0f;
759
+ major = (w2 >> 4) & 0x03;
760
+ break;
761
+ }
684
762
  }
685
763
 
686
- return 0;
764
+ return major * 100 + minor;
687
765
  }
688
766
 
689
767
  /**
@@ -896,10 +974,6 @@ export class ESPLoader extends EventTarget {
896
974
  this.logger.debug("Finished read loop");
897
975
  }
898
976
 
899
- sleep(ms = 100) {
900
- return new Promise((resolve) => setTimeout(resolve, ms));
901
- }
902
-
903
977
  state_DTR = false;
904
978
  state_RTS = false;
905
979
 
@@ -930,14 +1004,11 @@ export class ESPLoader extends EventTarget {
930
1004
  });
931
1005
  }
932
1006
 
933
- /**
934
- * Helper function to run a sequence of signal changes
935
- * Automatically detects WebUSB vs Web Serial and calls appropriate methods
936
- */
937
1007
  private async runSignalSequence(
938
1008
  steps: Array<{ dtr?: boolean; rts?: boolean; delayMs?: number }>,
939
1009
  ): Promise<void> {
940
- const webusb = (this.port as any).isWebUSB === true;
1010
+ const webusb =
1011
+ (this.port as unknown as { isWebUSB?: boolean }).isWebUSB === true;
941
1012
  for (const step of steps) {
942
1013
  if (step.dtr !== undefined && step.rts !== undefined) {
943
1014
  if (webusb) {
@@ -947,14 +1018,18 @@ export class ESPLoader extends EventTarget {
947
1018
  }
948
1019
  } else {
949
1020
  if (step.dtr !== undefined) {
950
- webusb
951
- ? await this.setDTRWebUSB(step.dtr)
952
- : await this.setDTR(step.dtr);
1021
+ if (webusb) {
1022
+ await this.setDTRWebUSB(step.dtr);
1023
+ } else {
1024
+ await this.setDTR(step.dtr);
1025
+ }
953
1026
  }
954
1027
  if (step.rts !== undefined) {
955
- webusb
956
- ? await this.setRTSWebUSB(step.rts)
957
- : await this.setRTS(step.rts);
1028
+ if (webusb) {
1029
+ await this.setRTSWebUSB(step.rts);
1030
+ } else {
1031
+ await this.setRTS(step.rts);
1032
+ }
958
1033
  }
959
1034
  }
960
1035
  if (step.delayMs) await sleep(step.delayMs);
@@ -1332,8 +1407,14 @@ export class ESPLoader extends EventTarget {
1332
1407
  }
1333
1408
  }
1334
1409
 
1335
- // Add general fallback strategies only for non-CP2102 and non-ESP32-S2 Native USB chips
1336
- if (!isCP2102 && !isESP32S2NativeUSB) {
1410
+ // Add general fallback strategies only for Native USB chips (not USB-Serial)
1411
+ // and only for chips not already handled by specific blocks above
1412
+ if (
1413
+ !isUSBSerialChip &&
1414
+ !isCP2102 &&
1415
+ !isESP32S2NativeUSB &&
1416
+ !isUSBJTAGSerial
1417
+ ) {
1337
1418
  // Classic reset (for chips not handled above)
1338
1419
  if (portInfo.usbVendorId !== 0x1a86) {
1339
1420
  resetStrategies.push({
@@ -1369,7 +1450,7 @@ export class ESPLoader extends EventTarget {
1369
1450
  });
1370
1451
 
1371
1452
  // WebUSB Strategy: USB-JTAG/Serial fallback
1372
- if (!isUSBJTAGSerial && !isEspressifUSB) {
1453
+ if (!isEspressifUSB) {
1373
1454
  resetStrategies.push({
1374
1455
  name: "USB-JTAG/Serial fallback (WebUSB)",
1375
1456
  fn: async function () {
@@ -1457,11 +1538,11 @@ export class ESPLoader extends EventTarget {
1457
1538
  try {
1458
1539
  await Promise.race([syncPromise, timeoutPromise]);
1459
1540
  // Sync succeeded
1460
- this.logger.log(
1541
+ this.logger.debug(
1461
1542
  `Connected CDC/JTAG successfully with ${strategy.name} reset.`,
1462
1543
  );
1463
1544
  return;
1464
- } catch (error) {
1545
+ } catch {
1465
1546
  throw new Error("Sync timeout or abandoned");
1466
1547
  }
1467
1548
  }
@@ -1500,41 +1581,20 @@ export class ESPLoader extends EventTarget {
1500
1581
 
1501
1582
  /**
1502
1583
  * @name watchdogReset
1503
- * Watchdog reset for ESP32-S2/S3/C3 with USB-OTG or USB-JTAG/Serial
1584
+ * Watchdog reset for ESP32-S2/S3/P4 with USB-OTG or USB-JTAG/Serial
1504
1585
  * Uses RTC watchdog timer to reset the chip - works when DTR/RTS signals are not available
1505
1586
  * This is an alias for rtcWdtResetChipSpecific() for backwards compatibility
1587
+ * Note: ESP32-C3, ESP32-C5, ESP32-C6 do NOT boot correctly after WDT reset
1506
1588
  */
1507
1589
  async watchdogReset() {
1508
1590
  await this.rtcWdtResetChipSpecific();
1509
1591
  }
1510
1592
 
1511
1593
  /**
1512
- * Get chip revision for ESP32-C3
1513
- * Reads from EFUSE registers and calculates revision
1514
- */
1515
- async getChipRevisionC3(): Promise<number> {
1516
- if (this.chipFamily !== CHIP_FAMILY_ESP32C3) {
1517
- return 0;
1518
- }
1519
-
1520
- // Read EFUSE_RD_MAC_SPI_SYS_3_REG (bits [20:18] = lower 3 bits of revision)
1521
- const word3 = await this.readRegister(ESP32C3_EFUSE_RD_MAC_SPI_SYS_3_REG);
1522
- const low = (word3 >> 18) & 0x07;
1523
-
1524
- // Read EFUSE_RD_MAC_SPI_SYS_5_REG (bits [25:23] = upper 3 bits of revision)
1525
- const word5 = await this.readRegister(ESP32C3_EFUSE_RD_MAC_SPI_SYS_5_REG);
1526
- const hi = (word5 >> 23) & 0x07;
1527
-
1528
- // Combine: upper 3 bits from word5, lower 3 bits from word3
1529
- const revision = (hi << 3) | low;
1530
-
1531
- return revision;
1532
- }
1533
-
1534
- /**
1535
- * RTC watchdog timer reset for ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C5, ESP32-C6, and ESP32-P4
1594
+ * RTC watchdog timer reset for ESP32-S2, ESP32-S3, and ESP32-P4
1536
1595
  * Uses specific registers for each chip family
1537
- * Note: ESP32-H2 does NOT support WDT reset
1596
+ * Note: ESP32-C3 does NOT boot correctly after WDT reset
1597
+ * Note: ESP32-C5, ESP32-C6, ESP32-C61, ESP32-H2 do NOT support WDT reset (no usable RTC WDT path)
1538
1598
  */
1539
1599
  public async rtcWdtResetChipSpecific(): Promise<void> {
1540
1600
  this.logger.debug("Hard resetting with watchdog timer...");
@@ -1554,20 +1614,6 @@ export class ESPLoader extends EventTarget {
1554
1614
  WDTCONFIG0_REG = ESP32S3_RTC_CNTL_WDTCONFIG0_REG;
1555
1615
  WDTCONFIG1_REG = ESP32S3_RTC_CNTL_WDTCONFIG1_REG;
1556
1616
  WDT_WKEY = ESP32S3_RTC_CNTL_WDT_WKEY;
1557
- } else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
1558
- WDTWPROTECT_REG = ESP32C3_RTC_CNTL_WDTWPROTECT_REG;
1559
- WDTCONFIG0_REG = ESP32C3_RTC_CNTL_WDTCONFIG0_REG;
1560
- WDTCONFIG1_REG = ESP32C3_RTC_CNTL_WDTCONFIG1_REG;
1561
- WDT_WKEY = ESP32C3_RTC_CNTL_WDT_WKEY;
1562
- } else if (
1563
- this.chipFamily === CHIP_FAMILY_ESP32C5 ||
1564
- this.chipFamily === CHIP_FAMILY_ESP32C6
1565
- ) {
1566
- // C5 and C6 use LP_WDT (Low Power Watchdog Timer)
1567
- WDTWPROTECT_REG = ESP32C5_C6_RTC_CNTL_WDTWPROTECT_REG;
1568
- WDTCONFIG0_REG = ESP32C5_C6_RTC_CNTL_WDTCONFIG0_REG;
1569
- WDTCONFIG1_REG = ESP32C5_C6_RTC_CNTL_WDTCONFIG1_REG;
1570
- WDT_WKEY = ESP32C5_C6_RTC_CNTL_WDT_WKEY;
1571
1617
  } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1572
1618
  // P4 uses LP_WDT (Low Power Watchdog Timer)
1573
1619
  WDTWPROTECT_REG = ESP32P4_RTC_CNTL_WDTWPROTECT_REG;
@@ -1594,29 +1640,151 @@ export class ESPLoader extends EventTarget {
1594
1640
  await this.writeRegister(WDTWPROTECT_REG, 0, undefined, 0);
1595
1641
 
1596
1642
  // Wait for reset to take effect
1597
- await this.sleep(500);
1643
+ await sleep(500);
1598
1644
  }
1599
1645
 
1600
1646
  /**
1601
- * Helper: USB-based WDT reset
1602
- * Returns true if WDT reset was performed, false otherwise
1647
+ * Reset device from bootloader mode to firmware mode
1648
+ * Automatically selects the correct reset strategy based on USB connection type
1649
+ * @param clearForceDownloadFlag - If true, clears the force download boot flag (USB-OTG only)
1650
+ * @returns true if port will change (USB-OTG), false otherwise
1603
1651
  */
1604
- private async tryUsbWdtReset(chipName: string): Promise<boolean> {
1605
- const isUsingUsbOtg = await this.detectUsbConnectionType();
1652
+ public async resetToFirmwareMode(
1653
+ clearForceDownloadFlag = true,
1654
+ ): Promise<boolean> {
1655
+ this.logger.debug("Resetting from bootloader to firmware mode...");
1606
1656
 
1607
- if (isUsingUsbOtg) {
1608
- // Use WDT reset for USB-OTG devices
1609
- await this.rtcWdtResetChipSpecific();
1610
- this.logger.debug(
1611
- `${chipName}: RTC WDT reset (USB-JTAG/Serial or USB-OTG detected)`,
1612
- );
1613
- return true;
1614
- } else {
1615
- // Use classic reset for non-USB devices
1616
- await this.hardResetClassic();
1617
- this.logger.debug("Classic reset.");
1657
+ try {
1658
+ // Detect USB connection type
1659
+ const isUsbJtagOrOtg = await this.detectUsbConnectionType();
1660
+
1661
+ if (isUsbJtagOrOtg) {
1662
+ // USB-JTAG/OTG devices need special handling
1663
+ this.logger.debug("USB-JTAG/OTG detected - checking WDT reset support");
1664
+
1665
+ // Get detailed USB mode information
1666
+ let usbMode: {
1667
+ mode: "uart" | "usb-jtag-serial" | "usb-otg";
1668
+ uartNo: number;
1669
+ };
1670
+ try {
1671
+ usbMode = await this.getUsbMode();
1672
+ this.logger.debug(
1673
+ `USB mode: ${usbMode.mode} (uartNo=${usbMode.uartNo})`,
1674
+ );
1675
+ } catch (err) {
1676
+ this.logger.debug(`Could not get USB mode: ${err}`);
1677
+ // Fall back to generic USB-JTAG/OTG handling
1678
+ usbMode = { mode: "usb-jtag-serial", uartNo: 0 };
1679
+ }
1680
+
1681
+ // WDT reset is not needed for ESP32-C3
1682
+ // WDT reset is supported by: ESP32-S2, ESP32-S3, ESP32-P4
1683
+ // WDT reset is NOT supported by: ESP32-C5, ESP32-C6, ESP32-C61, ESP32-H2
1684
+ const supportsWdtReset =
1685
+ this.chipFamily === CHIP_FAMILY_ESP32S2 ||
1686
+ this.chipFamily === CHIP_FAMILY_ESP32S3 ||
1687
+ this.chipFamily === CHIP_FAMILY_ESP32P4;
1688
+
1689
+ if (!supportsWdtReset) {
1690
+ this.logger.debug(
1691
+ `${this.chipName} does not support WDT reset - using classic reset instead`,
1692
+ );
1693
+
1694
+ // Use classic reset for chips without WDT support
1695
+ await this.hardResetToFirmware();
1696
+ this.logger.debug("Classic reset to firmware complete");
1697
+ return false; // Port stays open
1698
+ }
1699
+
1700
+ // WDT reset is supported - proceed with WDT reset logic
1701
+ this.logger.debug(
1702
+ `${this.chipName} supports WDT reset - using WDT reset strategy`,
1703
+ );
1704
+
1705
+ // CRITICAL: WDT register writes require ROM (not stub) and baudrate 115200
1706
+
1707
+ // If on stub, need to return to ROM first
1708
+ if (this.IS_STUB) {
1709
+ this.logger.debug("On stub - returning to ROM before WDT reset");
1710
+
1711
+ // Change baudrate back to ROM baudrate if needed
1712
+ if (this.currentBaudRate !== ESP_ROM_BAUD) {
1713
+ this.logger.debug(
1714
+ `Changing baudrate from ${this.currentBaudRate} to ${ESP_ROM_BAUD}`,
1715
+ );
1716
+ await this.reconfigurePort(ESP_ROM_BAUD);
1717
+ this.currentBaudRate = ESP_ROM_BAUD;
1718
+ this.logger.debug("Baudrate changed to 115200");
1719
+ }
1720
+
1721
+ // CRITICAL: Temporarily clear console mode flag so hardReset(true) works
1722
+ const wasInConsoleMode = this._consoleMode;
1723
+ this._consoleMode = false;
1724
+
1725
+ // Reset to bootloader (ROM)
1726
+ await this.hardReset(true);
1727
+ await sleep(200);
1728
+
1729
+ // Restore console mode flag
1730
+ this._consoleMode = wasInConsoleMode;
1731
+
1732
+ // Sync with ROM
1733
+ await this.sync();
1734
+ this.IS_STUB = false;
1735
+ this.logger.debug("Now on ROM");
1736
+ } else {
1737
+ // Even if not on stub, ensure baudrate is 115200 for WDT register writes
1738
+ if (this.currentBaudRate !== ESP_ROM_BAUD) {
1739
+ this.logger.debug(
1740
+ `Not on stub, but baudrate is ${this.currentBaudRate} - changing to ${ESP_ROM_BAUD} for WDT reset`,
1741
+ );
1742
+ await this.reconfigurePort(ESP_ROM_BAUD);
1743
+ this.currentBaudRate = ESP_ROM_BAUD;
1744
+ this.logger.debug("Baudrate changed to 115200");
1745
+ }
1746
+ }
1747
+
1748
+ // Clear force download boot flag if requested (USB-OTG only)
1749
+ if (clearForceDownloadFlag && usbMode.mode === "usb-otg") {
1750
+ const flagCleared = await this._clearForceDownloadBootIfNeeded();
1751
+ if (flagCleared) {
1752
+ this.logger.debug("Force download boot flag cleared");
1753
+ }
1754
+ }
1755
+
1756
+ // Perform WDT reset to boot into firmware
1757
+ await this.rtcWdtResetChipSpecific();
1758
+ this.logger.debug("WDT reset performed - device will boot to firmware");
1759
+
1760
+ // Check if port will change after WDT reset
1761
+ // USB-OTG (ESP32-S2/P4): Port always changes
1762
+ // USB-JTAG/Serial (ESP32-S3/C3/C5/C6/C61/H2/P4): Port may change depending on platform
1763
+ const portWillChange =
1764
+ usbMode.mode === "usb-otg" || usbMode.mode === "usb-jtag-serial";
1765
+
1766
+ if (portWillChange) {
1767
+ this.logger.debug(
1768
+ `Port will change after WDT reset (${usbMode.mode}) - port reselection needed`,
1769
+ );
1770
+ return true;
1771
+ }
1772
+
1773
+ return false;
1774
+ } else {
1775
+ // External serial chip - use classic reset to firmware
1776
+ this.logger.debug(
1777
+ "External serial chip detected - using classic reset",
1778
+ );
1779
+
1780
+ await this.hardResetToFirmware();
1781
+ this.logger.debug("Classic reset to firmware complete");
1782
+ return false;
1783
+ }
1784
+ } catch (err) {
1785
+ this.logger.error(`Failed to reset to firmware mode: ${err}`);
1786
+ throw err;
1618
1787
  }
1619
- return false;
1620
1788
  }
1621
1789
 
1622
1790
  async hardReset(bootloader = false) {
@@ -1630,58 +1798,98 @@ export class ESPLoader extends EventTarget {
1630
1798
  }
1631
1799
  // Simple hardware reset to restart firmware (IO0=HIGH)
1632
1800
  this.logger.debug("Performing hardware reset (console mode)...");
1633
- await this.hardResetToFirmware();
1801
+ await this.resetInConsoleMode();
1634
1802
  this.logger.debug("Hardware reset complete");
1635
1803
  return;
1636
1804
  }
1637
1805
 
1638
1806
  if (bootloader) {
1639
- // enter flash mode
1807
+ // Enter bootloader/flash mode
1640
1808
  if (this.port.getInfo().usbProductId === USB_JTAG_SERIAL_PID) {
1641
1809
  await this.hardResetUSBJTAGSerial();
1642
- this.logger.debug("USB-JTAG/Serial reset.");
1810
+ this.logger.debug("USB-JTAG/Serial reset to bootloader.");
1643
1811
  } else {
1644
1812
  await this.hardResetClassic();
1645
- this.logger.debug("Classic reset.");
1813
+ this.logger.debug("Classic reset to bootloader.");
1646
1814
  }
1647
1815
  } else {
1648
- // just reset (no bootloader mode)
1649
- // For ESP32-S2/S3/P4 with USB-OTG or USB-JTAG/Serial, check if watchdog reset is needed
1650
- this.logger.debug("*** Performing WDT reset strategy ***");
1651
- if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
1652
- const wdtResetUsed = await this.tryUsbWdtReset("ESP32-S2");
1653
- if (wdtResetUsed) return;
1654
- // } else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1655
- // const wdtResetUsed = await this.tryUsbWdtReset("ESP32-S3");
1656
- // if (wdtResetUsed) return;
1657
- } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1658
- const wdtResetUsed = await this.tryUsbWdtReset("ESP32-P4");
1659
- if (wdtResetUsed) return;
1660
- // } else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
1661
- // const wdtResetUsed = await this.tryUsbWdtReset("ESP32-C3");
1662
- // if (wdtResetUsed) return;
1663
- } else if (this.chipFamily === CHIP_FAMILY_ESP32C5) {
1664
- const wdtResetUsed = await this.tryUsbWdtReset("ESP32-C5");
1665
- if (wdtResetUsed) return;
1666
- } else if (this.chipFamily === CHIP_FAMILY_ESP32C6) {
1667
- const wdtResetUsed = await this.tryUsbWdtReset("ESP32-C6");
1668
- if (wdtResetUsed) return;
1816
+ // Reset to firmware mode (exit bootloader)
1817
+ // Use intelligent reset strategy based on USB connection type
1818
+ this.logger.debug("Resetting to firmware mode...");
1819
+
1820
+ // Detect USB connection type to choose correct reset method
1821
+ const isUsbJtagOrOtg = await this.detectUsbConnectionType();
1822
+
1823
+ if (isUsbJtagOrOtg) {
1824
+ // USB-JTAG/OTG devices: Check if chip supports WDT reset
1825
+ // Only S2, S3, P4 support WDT reset correctly
1826
+ // C3, C5, C6, C61, H2 do NOT boot correctly after WDT reset
1827
+ const supportsWdtReset =
1828
+ this.chipFamily === CHIP_FAMILY_ESP32S2 ||
1829
+ this.chipFamily === CHIP_FAMILY_ESP32S3 ||
1830
+ this.chipFamily === CHIP_FAMILY_ESP32P4;
1831
+
1832
+ if (supportsWdtReset) {
1833
+ this.logger.debug("USB-JTAG/OTG detected - using WDT reset");
1834
+
1835
+ // Get USB mode details
1836
+ let usbMode: {
1837
+ mode: "uart" | "usb-jtag-serial" | "usb-otg";
1838
+ uartNo: number;
1839
+ };
1840
+ try {
1841
+ usbMode = await this.getUsbMode();
1842
+ this.logger.debug(
1843
+ `USB mode: ${usbMode.mode} (uartNo=${usbMode.uartNo})`,
1844
+ );
1845
+ } catch (err) {
1846
+ this.logger.debug(`Could not get USB mode: ${err}`);
1847
+ usbMode = { mode: "usb-jtag-serial", uartNo: 0 };
1848
+ }
1849
+
1850
+ // Clear force download flag for USB-OTG devices
1851
+ if (usbMode.mode === "usb-otg") {
1852
+ try {
1853
+ const flagCleared = await this._clearForceDownloadBootIfNeeded();
1854
+ if (flagCleared) {
1855
+ this.logger.debug("Force download boot flag cleared");
1856
+ }
1857
+ } catch (err) {
1858
+ this.logger.debug(`Could not clear force download flag: ${err}`);
1859
+ }
1860
+ }
1861
+
1862
+ // Perform WDT reset
1863
+ await this.rtcWdtResetChipSpecific();
1864
+ this.logger.debug(`${this.chipName}: WDT reset to firmware complete`);
1865
+ return;
1866
+ } else {
1867
+ // C3, C5, C6, etc. - use classic reset (like external serial chips)
1868
+ this.logger.debug(
1869
+ `${this.chipName} does not support WDT reset - using classic reset instead`,
1870
+ );
1871
+ }
1872
+ } else {
1873
+ // External serial chip: Use classic reset
1874
+ this.logger.debug(
1875
+ "External serial chip detected - using classic reset",
1876
+ );
1669
1877
  }
1670
1878
 
1671
- // Standard reset for all other cases
1879
+ // Classic reset: used for external serial chips and USB-JTAG chips that do not support WDT reset
1672
1880
  if (this.isWebUSB()) {
1673
1881
  // WebUSB: Use longer delays for better compatibility
1674
1882
  await this.setRTSWebUSB(true); // EN->LOW
1675
- await this.sleep(200);
1883
+ await sleep(200);
1676
1884
  await this.setRTSWebUSB(false);
1677
- await this.sleep(200);
1678
- this.logger.debug("Hard reset (WebUSB).");
1885
+ await sleep(200);
1886
+ this.logger.debug("Hard reset to firmware (WebUSB).");
1679
1887
  } else {
1680
1888
  // Web Serial: Standard reset
1681
1889
  await this.setRTS(true); // EN->LOW
1682
- await this.sleep(100);
1890
+ await sleep(100);
1683
1891
  await this.setRTS(false);
1684
- this.logger.debug("Hard reset.");
1892
+ this.logger.debug("Hard reset to firmware.");
1685
1893
  }
1686
1894
  }
1687
1895
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -2128,33 +2336,6 @@ export class ESPLoader extends EventTarget {
2128
2336
  return 26;
2129
2337
  }
2130
2338
 
2131
- private async setBaudrateC5Rom(baud: number) {
2132
- const crystalFreqRomExpect = await this.getC5CrystalFreqRomExpect();
2133
- const crystalFreqDetect = await this.getC5CrystalFreqDetected();
2134
- this.logger.log(
2135
- `ROM expects crystal freq: ${crystalFreqRomExpect} MHz, detected ${crystalFreqDetect} MHz.`,
2136
- );
2137
-
2138
- let baudRate = baud;
2139
- if (crystalFreqDetect === 48 && crystalFreqRomExpect === 40) {
2140
- baudRate = Math.trunc((baud * 40) / 48);
2141
- } else if (crystalFreqDetect === 40 && crystalFreqRomExpect === 48) {
2142
- baudRate = Math.trunc((baud * 48) / 40);
2143
- }
2144
-
2145
- this.logger.log(`Changing baud rate to ${baudRate}...`);
2146
- try {
2147
- const buffer = pack("<II", baudRate, 0);
2148
- await this.checkCommand(ESP_CHANGE_BAUDRATE, buffer);
2149
- } catch (e) {
2150
- this.logger.error(`Baudrate change error: ${e}`);
2151
- throw new Error(
2152
- `Unable to change the baud rate to ${baudRate}: No response from set baud rate command.`,
2153
- );
2154
- }
2155
- this.logger.log("Changed.");
2156
- }
2157
-
2158
2339
  async setBaudrate(baud: number) {
2159
2340
  const chipFamily = this._parent ? this._parent.chipFamily : this.chipFamily;
2160
2341
 
@@ -2162,7 +2343,6 @@ export class ESPLoader extends EventTarget {
2162
2343
  await this.setBaudrateC5Rom(baud);
2163
2344
  } else {
2164
2345
  try {
2165
- // Send ESP_ROM_BAUD(115200) as the old one if running STUB otherwise 0
2166
2346
  const buffer = pack("<II", baud, this.IS_STUB ? ESP_ROM_BAUD : 0);
2167
2347
  await this.checkCommand(ESP_CHANGE_BAUDRATE, buffer);
2168
2348
  } catch (e) {
@@ -2202,7 +2382,34 @@ export class ESPLoader extends EventTarget {
2202
2382
  );
2203
2383
  }
2204
2384
 
2205
- this.logger.log(`Changed baud rate to ${baud}`);
2385
+ this.logger.debug(`Changed baud rate to ${baud}`);
2386
+ }
2387
+
2388
+ private async setBaudrateC5Rom(baud: number) {
2389
+ const crystalFreqRomExpect = await this.getC5CrystalFreqRomExpect();
2390
+ const crystalFreqDetect = await this.getC5CrystalFreqDetected();
2391
+ this.logger.log(
2392
+ `ROM expects crystal freq: ${crystalFreqRomExpect} MHz, detected ${crystalFreqDetect} MHz.`,
2393
+ );
2394
+
2395
+ let baudRate = baud;
2396
+ if (crystalFreqDetect === 48 && crystalFreqRomExpect === 40) {
2397
+ baudRate = Math.trunc((baud * 40) / 48);
2398
+ } else if (crystalFreqDetect === 40 && crystalFreqRomExpect === 48) {
2399
+ baudRate = Math.trunc((baud * 48) / 40);
2400
+ }
2401
+
2402
+ this.logger.log(`Changing baud rate to ${baudRate}...`);
2403
+ try {
2404
+ const buffer = pack("<II", baudRate, 0);
2405
+ await this.checkCommand(ESP_CHANGE_BAUDRATE, buffer);
2406
+ } catch (e) {
2407
+ this.logger.error(`Baudrate change error: ${e}`);
2408
+ throw new Error(
2409
+ `Unable to change the baud rate to ${baudRate}: No response from set baud rate command.`,
2410
+ );
2411
+ }
2412
+ this.logger.log("Changed.");
2206
2413
  }
2207
2414
 
2208
2415
  async reconfigurePort(baud: number) {
@@ -2271,9 +2478,9 @@ export class ESPLoader extends EventTarget {
2271
2478
 
2272
2479
  // Restart Readloop
2273
2480
  this.readLoop();
2274
- } catch (e) {
2275
- // this.logger.error(`Reconfigure port error: ${e}`);
2276
- // throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
2481
+ } catch {
2482
+ // this.logger.error(`Reconfigure port error`);
2483
+ // throw new Error(`Unable to change the baud rate to ${baud}`);
2277
2484
  } finally {
2278
2485
  // Always reset flag, even on error or early return
2279
2486
  this._isReconfiguring = false;
@@ -2401,6 +2608,9 @@ export class ESPLoader extends EventTarget {
2401
2608
  );
2402
2609
  }
2403
2610
 
2611
+ const paddedData = padTo(new Uint8Array(binaryData), 4);
2612
+ binaryData = paddedData.buffer as ArrayBuffer;
2613
+
2404
2614
  const uncompressedFilesize = binaryData.byteLength;
2405
2615
  let compressedFilesize = 0;
2406
2616
 
@@ -2804,20 +3014,11 @@ export class ESPLoader extends EventTarget {
2804
3014
  return status;
2805
3015
  }
2806
3016
  async detectFlashSize() {
2807
- this.logger.log("Detecting Flash Size");
3017
+ this.logger.debug("Detecting Flash Size");
2808
3018
 
2809
3019
  const flashId = await this.flashId();
2810
- const manufacturer = flashId & 0xff;
2811
3020
  const flashIdLowbyte = (flashId >> 16) & 0xff;
2812
3021
 
2813
- this.logger.log(`FlashId: ${toHex(flashId)}`);
2814
- this.logger.log(`Flash Manufacturer: ${manufacturer.toString(16)}`);
2815
- this.logger.log(
2816
- `Flash Device: ${((flashId >> 8) & 0xff).toString(
2817
- 16,
2818
- )}${flashIdLowbyte.toString(16)}`,
2819
- );
2820
-
2821
3022
  this.flashSize = DETECTED_FLASH_SIZES[flashIdLowbyte];
2822
3023
  this.logger.log(`Auto-detected Flash size: ${this.flashSize}`);
2823
3024
  }
@@ -2906,7 +3107,7 @@ export class ESPLoader extends EventTarget {
2906
3107
  const ramBlock = USB_RAM_BLOCK;
2907
3108
 
2908
3109
  // Upload
2909
- this.logger.log("Uploading stub...");
3110
+ this.logger.debug("Uploading stub...");
2910
3111
  for (const field of ["text", "data"] as const) {
2911
3112
  const fieldData = stub[field];
2912
3113
  const offset = stub[`${field}_start` as "text_start" | "data_start"];
@@ -2930,7 +3131,7 @@ export class ESPLoader extends EventTarget {
2930
3131
  if (pChar != "OHAI") {
2931
3132
  throw new Error("Failed to start stub. Unexpected response: " + pChar);
2932
3133
  }
2933
- this.logger.log("Stub is now running...");
3134
+ this.logger.debug("Stub is now running...");
2934
3135
  const espStubLoader = new EspStubLoader(this.port, this.logger, this);
2935
3136
 
2936
3137
  // Try to autodetect the flash size.
@@ -3057,114 +3258,30 @@ export class ESPLoader extends EventTarget {
3057
3258
  await this._writeChain;
3058
3259
  }
3059
3260
 
3060
- public async getUsbMode(): Promise<{
3061
- mode: "uart" | "usb-jtag-serial" | "usb-otg";
3062
- uartNo: number;
3063
- }> {
3064
- const family = this._parent ? this._parent.chipFamily : this.chipFamily;
3065
- const revision = this._parent
3066
- ? (this._parent.chipRevision ?? 0)
3067
- : (this.chipRevision ?? 0);
3261
+ async disconnect() {
3262
+ if (this._parent) {
3263
+ await this._parent.disconnect();
3264
+ return;
3265
+ }
3266
+ if (!this.port.writable) {
3267
+ // this.logger.debug("Port already closed, skipping disconnect");
3268
+ return;
3269
+ }
3068
3270
 
3069
- let bufNoAddr: number | null = null;
3070
- let jtagSerialVal: number | null = null;
3071
- let otgVal: number | null = null;
3072
-
3073
- switch (family) {
3074
- case CHIP_FAMILY_ESP32S2:
3075
- bufNoAddr = ESP32S2_UARTDEV_BUF_NO;
3076
- otgVal = ESP32S2_UARTDEV_BUF_NO_USB_OTG;
3077
- break;
3078
- case CHIP_FAMILY_ESP32S3:
3079
- bufNoAddr = ESP32S3_UARTDEV_BUF_NO;
3080
- jtagSerialVal = ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3081
- otgVal = ESP32S3_UARTDEV_BUF_NO_USB_OTG;
3082
- break;
3083
- case CHIP_FAMILY_ESP32C3: {
3084
- const bssAddr = revision < 101 ? 0x3fcdf064 : 0x3fcdf060;
3085
- bufNoAddr = bssAddr + ESP32C3_BUF_UART_NO_OFFSET;
3086
- jtagSerialVal = ESP32C3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3087
- break;
3088
- }
3089
- case CHIP_FAMILY_ESP32C5:
3090
- bufNoAddr = ESP32C5_UARTDEV_BUF_NO;
3091
- jtagSerialVal = ESP32C5_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3092
- break;
3093
- case CHIP_FAMILY_ESP32C6:
3094
- bufNoAddr = ESP32C6_UARTDEV_BUF_NO;
3095
- jtagSerialVal = ESP32C6_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3096
- break;
3097
- case CHIP_FAMILY_ESP32C61:
3098
- bufNoAddr =
3099
- revision <= 200
3100
- ? ESP32C61_UARTDEV_BUF_NO_REV_LE2
3101
- : ESP32C61_UARTDEV_BUF_NO_REV_GT2;
3102
- jtagSerialVal =
3103
- revision <= 200
3104
- ? ESP32C61_UARTDEV_BUF_NO_USB_JTAG_SERIAL_REV_LE2
3105
- : ESP32C61_UARTDEV_BUF_NO_USB_JTAG_SERIAL_REV_GT2;
3106
- break;
3107
- case CHIP_FAMILY_ESP32H2:
3108
- bufNoAddr = ESP32H2_UARTDEV_BUF_NO;
3109
- jtagSerialVal = ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3110
- break;
3111
- case CHIP_FAMILY_ESP32H4:
3112
- bufNoAddr = ESP32H4_UARTDEV_BUF_NO;
3113
- jtagSerialVal = ESP32H4_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3114
- break;
3115
- case CHIP_FAMILY_ESP32P4:
3116
- bufNoAddr =
3117
- revision < 300
3118
- ? ESP32P4_UARTDEV_BUF_NO_REV0
3119
- : ESP32P4_UARTDEV_BUF_NO_REV300;
3120
- jtagSerialVal = ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3121
- otgVal = ESP32P4_UARTDEV_BUF_NO_USB_OTG;
3122
- break;
3123
- }
3124
-
3125
- if (bufNoAddr === null) {
3126
- return { mode: "uart", uartNo: 0 };
3127
- }
3128
-
3129
- const uartNo = (await this.readRegister(bufNoAddr)) & 0xff;
3130
-
3131
- if (otgVal !== null && uartNo === otgVal) {
3132
- this.logger.debug(`USB mode: USB-OTG (uartNo=${uartNo})`);
3133
- return { mode: "usb-otg", uartNo };
3134
- }
3135
- if (jtagSerialVal !== null && uartNo === jtagSerialVal) {
3136
- this.logger.debug(`USB mode: USB-JTAG/Serial (uartNo=${uartNo})`);
3137
- return { mode: "usb-jtag-serial", uartNo };
3138
- }
3139
-
3140
- this.logger.debug(`USB mode: UART (uartNo=${uartNo})`);
3141
- return { mode: "uart", uartNo };
3142
- }
3143
-
3144
- async disconnect() {
3145
- if (this._parent) {
3146
- await this._parent.disconnect();
3147
- return;
3148
- }
3149
- if (!this.port.writable) {
3150
- // this.logger.debug("Port already closed, skipping disconnect");
3151
- return;
3152
- }
3153
-
3154
- // Wait for pending writes to complete
3155
- try {
3156
- await this._writeChain;
3157
- } catch (err) {
3158
- // this.logger.debug(`Pending write error during disconnect: ${err}`);
3159
- }
3271
+ // Wait for pending writes to complete
3272
+ try {
3273
+ await this._writeChain;
3274
+ } catch {
3275
+ // this.logger.debug("Pending write error during disconnect");
3276
+ }
3160
3277
 
3161
3278
  // Release persistent writer before closing
3162
3279
  if (this._writer) {
3163
3280
  try {
3164
3281
  await this._writer.close();
3165
3282
  this._writer.releaseLock();
3166
- } catch (err) {
3167
- // this.logger.debug(`Writer close/release error: ${err}`);
3283
+ } catch {
3284
+ // this.logger.debug("Writer close/release error");
3168
3285
  }
3169
3286
  this._writer = undefined;
3170
3287
  } else {
@@ -3174,8 +3291,8 @@ export class ESPLoader extends EventTarget {
3174
3291
  const writer = this.port.writable.getWriter();
3175
3292
  await writer.close();
3176
3293
  writer.releaseLock();
3177
- } catch (err) {
3178
- // this.logger.debug(`Direct writer close error: ${err}`);
3294
+ } catch {
3295
+ // this.logger.debug("Direct writer close error");
3179
3296
  }
3180
3297
  }
3181
3298
 
@@ -3203,7 +3320,7 @@ export class ESPLoader extends EventTarget {
3203
3320
  // Only cancel if reader is still active
3204
3321
  try {
3205
3322
  this._reader.cancel();
3206
- } catch (err) {
3323
+ } catch {
3207
3324
  // Reader already released, resolve immediately
3208
3325
  clearTimeout(timeout);
3209
3326
  resolve(undefined);
@@ -3234,8 +3351,8 @@ export class ESPLoader extends EventTarget {
3234
3351
  // Wait for pending writes to complete
3235
3352
  try {
3236
3353
  await this._writeChain;
3237
- } catch (err) {
3238
- // this.logger.debug(`Pending write error during release: ${err}`);
3354
+ } catch {
3355
+ // this.logger.debug("Pending write error during release");
3239
3356
  }
3240
3357
 
3241
3358
  // Release writer
@@ -3249,26 +3366,27 @@ export class ESPLoader extends EventTarget {
3249
3366
  this._writer = undefined;
3250
3367
  }
3251
3368
 
3252
- // Cancel and release reader
3369
+ // Cancel reader - let readLoop's finally block handle releaseLock()
3253
3370
  if (this._reader) {
3254
- const reader = this._reader;
3255
3371
  try {
3256
3372
  // Suppress disconnect event during console mode switching
3257
3373
  this._suppressDisconnect = true;
3258
- await reader.cancel();
3259
- this.logger.debug("Reader cancelled");
3374
+
3375
+ // Cancel will cause readLoop to exit and call releaseLock() in its finally block
3376
+ await this._reader.cancel();
3377
+ this.logger.debug("Reader cancelled - waiting for readLoop to finish");
3378
+
3379
+ // CRITICAL: Wait a bit for readLoop's finally block to complete
3380
+ // The finally block needs time to call releaseLock() and set _reader = undefined
3381
+ // This is much faster than waiting for browser to unlock (just waiting for JS execution)
3382
+ await sleep(50);
3383
+
3384
+ this.logger.debug("ReadLoop cleanup should be complete");
3260
3385
  } catch (err) {
3261
3386
  this.logger.debug(`Reader cancel error: ${err}`);
3262
- } finally {
3263
- try {
3264
- reader.releaseLock();
3265
- } catch (err) {
3266
- this.logger.debug(`Reader release error: ${err}`);
3267
- }
3268
- }
3269
- if (this._reader === reader) {
3270
- this._reader = undefined;
3271
3387
  }
3388
+ // Don't call releaseLock() or set _reader to undefined here
3389
+ // Let readLoop's finally block handle it to avoid race conditions
3272
3390
  }
3273
3391
  }
3274
3392
 
@@ -3317,6 +3435,157 @@ export class ESPLoader extends EventTarget {
3317
3435
  return isUsbJtag;
3318
3436
  }
3319
3437
 
3438
+ public async getUsbMode(): Promise<{
3439
+ mode: "uart" | "usb-jtag-serial" | "usb-otg";
3440
+ uartNo: number;
3441
+ }> {
3442
+ const family = this._parent ? this._parent.chipFamily : this.chipFamily;
3443
+ const revision = this._parent
3444
+ ? (this._parent.chipRevision ?? 0)
3445
+ : (this.chipRevision ?? 0);
3446
+
3447
+ let bufNoAddr: number | null = null;
3448
+ let jtagSerialVal: number | null = null;
3449
+ let otgVal: number | null = null;
3450
+
3451
+ switch (family) {
3452
+ case CHIP_FAMILY_ESP32S2:
3453
+ bufNoAddr = ESP32S2_UARTDEV_BUF_NO;
3454
+ otgVal = ESP32S2_UARTDEV_BUF_NO_USB_OTG;
3455
+ break;
3456
+ case CHIP_FAMILY_ESP32S3:
3457
+ bufNoAddr = ESP32S3_UARTDEV_BUF_NO;
3458
+ jtagSerialVal = ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3459
+ otgVal = ESP32S3_UARTDEV_BUF_NO_USB_OTG;
3460
+ break;
3461
+ case CHIP_FAMILY_ESP32C3: {
3462
+ const bssAddr = revision < 101 ? 0x3fcdf064 : 0x3fcdf060;
3463
+ bufNoAddr = bssAddr + ESP32C3_BUF_UART_NO_OFFSET;
3464
+ jtagSerialVal = ESP32C3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3465
+ break;
3466
+ }
3467
+ case CHIP_FAMILY_ESP32C5:
3468
+ bufNoAddr = ESP32C5_UARTDEV_BUF_NO;
3469
+ jtagSerialVal = ESP32C5_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3470
+ break;
3471
+ case CHIP_FAMILY_ESP32C6:
3472
+ bufNoAddr = ESP32C6_UARTDEV_BUF_NO;
3473
+ jtagSerialVal = ESP32C6_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3474
+ break;
3475
+ case CHIP_FAMILY_ESP32C61:
3476
+ bufNoAddr =
3477
+ revision <= 200
3478
+ ? ESP32C61_UARTDEV_BUF_NO_REV_LE2
3479
+ : ESP32C61_UARTDEV_BUF_NO_REV_GT2;
3480
+ jtagSerialVal =
3481
+ revision <= 200
3482
+ ? ESP32C61_UARTDEV_BUF_NO_USB_JTAG_SERIAL_REV_LE2
3483
+ : ESP32C61_UARTDEV_BUF_NO_USB_JTAG_SERIAL_REV_GT2;
3484
+ break;
3485
+ case CHIP_FAMILY_ESP32H2:
3486
+ bufNoAddr = ESP32H2_UARTDEV_BUF_NO;
3487
+ jtagSerialVal = ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3488
+ break;
3489
+ case CHIP_FAMILY_ESP32H4:
3490
+ bufNoAddr = ESP32H4_UARTDEV_BUF_NO;
3491
+ jtagSerialVal = ESP32H4_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3492
+ break;
3493
+ case CHIP_FAMILY_ESP32P4:
3494
+ bufNoAddr =
3495
+ revision < 300
3496
+ ? ESP32P4_UARTDEV_BUF_NO_REV0
3497
+ : ESP32P4_UARTDEV_BUF_NO_REV300;
3498
+ jtagSerialVal = ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3499
+ otgVal = ESP32P4_UARTDEV_BUF_NO_USB_OTG;
3500
+ break;
3501
+ }
3502
+
3503
+ if (bufNoAddr === null) {
3504
+ return { mode: "uart", uartNo: 0 };
3505
+ }
3506
+
3507
+ const uartNo = (await this.readRegister(bufNoAddr)) & 0xff;
3508
+
3509
+ if (otgVal !== null && uartNo === otgVal) {
3510
+ this.logger.debug(`USB mode: USB-OTG (uartNo=${uartNo})`);
3511
+ return { mode: "usb-otg", uartNo };
3512
+ }
3513
+ if (jtagSerialVal !== null && uartNo === jtagSerialVal) {
3514
+ this.logger.debug(`USB mode: USB-JTAG/Serial (uartNo=${uartNo})`);
3515
+ return { mode: "usb-jtag-serial", uartNo };
3516
+ }
3517
+
3518
+ this.logger.debug(`USB mode: UART (uartNo=${uartNo})`);
3519
+ return { mode: "uart", uartNo };
3520
+ }
3521
+
3522
+ /**
3523
+ * Check if the current chip supports USB-JTAG or USB-OTG
3524
+ * @returns true if chip has native USB support (JTAG or OTG)
3525
+ */
3526
+ public supportsNativeUsb(): boolean {
3527
+ const family = this._parent ? this._parent.chipFamily : this.chipFamily;
3528
+
3529
+ // Chips with USB-JTAG/Serial or USB-OTG support
3530
+ const usbChips = [
3531
+ CHIP_FAMILY_ESP32S2, // USB-OTG
3532
+ CHIP_FAMILY_ESP32S3, // USB-OTG + USB-JTAG/Serial
3533
+ CHIP_FAMILY_ESP32C3, // USB-JTAG/Serial
3534
+ CHIP_FAMILY_ESP32C5, // USB-JTAG/Serial
3535
+ CHIP_FAMILY_ESP32C6, // USB-JTAG/Serial
3536
+ CHIP_FAMILY_ESP32C61, // USB-JTAG/Serial
3537
+ CHIP_FAMILY_ESP32H2, // USB-JTAG/Serial
3538
+ CHIP_FAMILY_ESP32H4, // USB-JTAG/Serial
3539
+ CHIP_FAMILY_ESP32P4, // USB-OTG + USB-JTAG/Serial
3540
+ ];
3541
+
3542
+ return usbChips.includes(family);
3543
+ }
3544
+
3545
+ /**
3546
+ * @name _ensureStreamsReady
3547
+ * After a hardware reset, ensure port streams are available.
3548
+ * On WebUSB, recreates streams since they break after reset.
3549
+ * On Web Serial, waits for streams to become available.
3550
+ */
3551
+ private async _ensureStreamsReady(): Promise<void> {
3552
+ if (this.isWebUSB()) {
3553
+ try {
3554
+ await (
3555
+ this.port as unknown as { recreateStreams(): Promise<void> }
3556
+ ).recreateStreams();
3557
+ this.logger.debug("WebUSB streams recreated");
3558
+
3559
+ let retries = 30;
3560
+ while (retries > 0 && !this.port.readable) {
3561
+ await sleep(100);
3562
+ retries--;
3563
+ }
3564
+ if (!this.port.readable) {
3565
+ throw new Error(
3566
+ "Readable stream not available after recreating streams",
3567
+ );
3568
+ }
3569
+ this.logger.debug("WebUSB streams are ready");
3570
+ } catch (err) {
3571
+ this.logger.error(`Failed to recreate WebUSB streams: ${err}`);
3572
+ this._consoleMode = false;
3573
+ throw err;
3574
+ }
3575
+ } else {
3576
+ let retries = 20;
3577
+ while (retries > 0 && !this.port.readable) {
3578
+ await sleep(100);
3579
+ retries--;
3580
+ }
3581
+ if (!this.port.readable) {
3582
+ this._consoleMode = false;
3583
+ throw new Error("Readable stream not available after reset");
3584
+ }
3585
+ this.logger.debug("Port streams are ready");
3586
+ }
3587
+ }
3588
+
3320
3589
  /**
3321
3590
  * @name enterConsoleMode
3322
3591
  * Prepare device for console mode by resetting to firmware
@@ -3349,8 +3618,6 @@ export class ESPLoader extends EventTarget {
3349
3618
  `Cannot enter console mode: USB connection type unknown and detection failed: ${err}`,
3350
3619
  );
3351
3620
  }
3352
- // Set console mode flag
3353
- this._consoleMode = false;
3354
3621
 
3355
3622
  this.logger.debug(
3356
3623
  `USB detection failed, using cached value: ${this.isUsbJtagOrOtg}`,
@@ -3358,56 +3625,40 @@ export class ESPLoader extends EventTarget {
3358
3625
  isUsbJtag = this.isUsbJtagOrOtg;
3359
3626
  }
3360
3627
 
3361
- // Release reader/writer so console can create new ones
3362
- // This is needed for Desktop (Web Serial) to unlock streams
3628
+ // Set console mode flag BEFORE any operations
3629
+ this._consoleMode = true;
3630
+
3363
3631
  if (isUsbJtag) {
3364
- // USB-JTAG/OTG devices: Use watchdog reset which closes port
3632
+ // USB-JTAG/OTG devices: Use reset which may close port
3365
3633
  const wasReset = await this._resetToFirmwareIfNeeded();
3366
- return wasReset; // true = port closed, caller must reopen
3634
+ if (wasReset) {
3635
+ return true; // port closed, caller must reopen
3636
+ }
3637
+
3638
+ // Port stayed open (e.g. C3/C5/C6/H2 classic reset)
3639
+ await this._ensureStreamsReady();
3640
+ return false;
3367
3641
  } else {
3368
3642
  // External serial chip devices: Release locks and do simple reset
3369
3643
  try {
3370
3644
  await this.releaseReaderWriter();
3371
- await this.sleep(100);
3645
+ await sleep(100);
3372
3646
  } catch (err) {
3373
3647
  this.logger.debug(`Failed to release locks: ${err}`);
3374
3648
  }
3375
3649
 
3376
- // Hardware reset to firmware mode (IO0=HIGH)
3377
3650
  try {
3378
- await this.hardReset(false);
3379
- this.logger.log("Device reset to firmware mode");
3651
+ await this.hardResetToFirmware();
3652
+ this.logger.debug("Device reset to firmware mode");
3380
3653
  } catch (err) {
3381
3654
  this.logger.debug(`Could not reset device: ${err}`);
3382
3655
  }
3383
3656
 
3384
- // For WebUSB (Android), recreate streams after hardware reset
3385
- if (this.isWebUSB()) {
3386
- try {
3387
- // Use the public recreateStreams() method to safely recreate streams
3388
- // without closing the port (important after hardware reset)
3389
- await (this.port as any).recreateStreams();
3390
- this.logger.debug("WebUSB streams recreated for console mode");
3391
- } catch (err) {
3392
- // Set console mode flag
3393
- this._consoleMode = false;
3394
- this.logger.debug(`Failed to recreate WebUSB streams: ${err}`);
3395
- }
3396
- }
3397
-
3398
- // Set console mode flag
3399
- this._consoleMode = true;
3400
-
3401
- return false; // Port stays open
3657
+ await this._ensureStreamsReady();
3658
+ return false;
3402
3659
  }
3403
3660
  }
3404
3661
 
3405
- /**
3406
- * @name _resetToFirmwareIfNeeded
3407
- * Reset device from bootloader to firmware when switching to console mode
3408
- * Detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
3409
- * @returns true if reconnect was performed, false otherwise
3410
- */
3411
3662
  /**
3412
3663
  * @name _clearForceDownloadBootIfNeeded
3413
3664
  * Read and clear the force download boot flag if it is set
@@ -3468,7 +3719,15 @@ export class ESPLoader extends EventTarget {
3468
3719
  }
3469
3720
  }
3470
3721
 
3722
+ /**
3723
+ * @name _resetToFirmwareIfNeeded
3724
+ * Reset device from bootloader to firmware when switching to console mode
3725
+ * Detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
3726
+ * @returns true if reconnect was performed, false otherwise
3727
+ */
3471
3728
  private async _resetToFirmwareIfNeeded(): Promise<boolean> {
3729
+ // Detect if we need WDT reset (USB-JTAG/OTG) or classic reset
3730
+ const isUsbJtagOrOtg = await this.detectUsbConnectionType();
3472
3731
  try {
3473
3732
  // Check if port is open - if not, assume device is already in firmware mode
3474
3733
  if (!this.port.writable || !this.port.readable) {
@@ -3478,115 +3737,65 @@ export class ESPLoader extends EventTarget {
3478
3737
  return false;
3479
3738
  }
3480
3739
 
3481
- const isUsingUsbOtg = await this.detectUsbConnectionType();
3482
-
3483
- if (isUsingUsbOtg) {
3484
- // For USB-OTG devices, we need to check if force download flag is set
3485
- // Only if it's set, we need WDT reset (which causes port change)
3486
- // If it's clear, we can use normal reset (no port change)
3487
-
3488
- if (this.IS_STUB) {
3489
- this.logger.debug("On stub - need to get back to ROM to check flag");
3490
-
3491
- // If we're running at higher baudrate, we need to change back to ROM baudrate
3492
- if (this.currentBaudRate !== ESP_ROM_BAUD) {
3493
- this.logger.debug(
3494
- `Changing baudrate from ${this.currentBaudRate} to ${ESP_ROM_BAUD} for ROM`,
3495
- );
3496
- try {
3497
- await this.reconfigurePort(ESP_ROM_BAUD);
3498
- this.currentBaudRate = ESP_ROM_BAUD;
3499
- } catch (err) {
3500
- this.logger.debug(`Baudrate change failed: ${err}`);
3501
- // Continue anyway
3502
- }
3503
- }
3504
-
3505
- this.logger.debug("Resetting to bootloader (ROM)...");
3506
-
3507
- // Reset to bootloader - this will clear the stub from RAM
3508
- try {
3509
- await this.hardReset(true);
3510
-
3511
- // Wait for reset to complete
3512
- await sleep(200);
3513
-
3514
- // Sync with ROM
3515
- await this.sync();
3516
-
3517
- this.logger.debug("Now on ROM after reset");
3518
-
3519
- // Mark that we're no longer on stub
3520
- this.IS_STUB = false;
3521
- } catch (resetErr) {
3522
- this.logger.debug(`Reset to ROM failed: ${resetErr}`);
3523
- // If reset fails, we might already be in firmware mode
3524
- // In this case, we don't need to do anything - just use normal reset
3525
- this.logger.debug("Assuming device is already in firmware mode");
3526
-
3527
- // Release reader/writer before returning
3528
- await this.releaseReaderWriter();
3529
- return false; // No port change needed
3530
- }
3531
- } else {
3532
- this.logger.debug("Already on ROM - checking force download flag");
3533
- }
3534
-
3535
- // Now check if force download flag is set and clear it if needed
3536
- const flagWasCleared = await this._clearForceDownloadBootIfNeeded();
3537
-
3538
- if (flagWasCleared) {
3539
- this.logger.debug(
3540
- "Force download flag was cleared - device will boot to firmware after reset",
3541
- );
3542
- } else {
3543
- this.logger.debug(
3544
- "Force download flag already clear - device will boot to firmware after reset",
3545
- );
3546
- }
3547
-
3548
- // Perform WDT reset BEFORE releasing reader/writer (needs communication)
3549
- // After WDT reset, the device will reboot into firmware mode
3550
- await this.hardReset(false);
3740
+ if (isUsbJtagOrOtg) {
3741
+ // USB-JTAG/OTG: DON'T release reader/writer before WDT reset
3742
+ // The WDT reset needs active communication to send register write commands
3743
+ // The port will close automatically after the WDT reset anyway
3744
+ this.logger.debug(
3745
+ "USB-JTAG/OTG: Keeping reader/writer active for WDT reset",
3746
+ );
3747
+ } else {
3748
+ // External serial chip: Release reader/writer before classic reset
3749
+ await this.releaseReaderWriter();
3750
+ this.logger.debug(
3751
+ "External serial: Reader/writer released before reset",
3752
+ );
3753
+ }
3551
3754
 
3552
- // For USB-OTG devices (ESP32-S2, ESP32-P4), the port will change after WDT reset
3553
- const portWillChange =
3554
- (this.chipFamily === CHIP_FAMILY_ESP32S2 && isUsingUsbOtg) ||
3555
- (this.chipFamily === CHIP_FAMILY_ESP32P4 && isUsingUsbOtg);
3755
+ // Use the new resetToFirmwareMode method which handles all the logic
3756
+ const portWillChange = await this.resetToFirmwareMode(true);
3556
3757
 
3557
- if (portWillChange) {
3558
- // Port will change - release reader/writer and let the port become invalid
3559
- await this.releaseReaderWriter();
3758
+ if (portWillChange) {
3759
+ this.logger.debug(
3760
+ `${this.chipName}: Port will change after WDT reset - user must reselect port`,
3761
+ );
3560
3762
 
3561
- this.logger.log(
3562
- `${this.chipName} USB-OTG: Port will change after WDT reset`,
3563
- );
3564
- this.logger.log("Please select the new port for console mode");
3565
-
3566
- // Dispatch event to signal port change
3567
- this.dispatchEvent(
3568
- new CustomEvent("usb-otg-port-change", {
3569
- detail: {
3570
- chipName: this.chipName,
3571
- message: `${this.chipName} USB port changed after reset. Please select the new port.`,
3572
- reason: "wdt-reset-to-firmware",
3573
- },
3574
- }),
3575
- );
3763
+ // Dispatch event to signal port change
3764
+ this.dispatchEvent(
3765
+ new CustomEvent("usb-otg-port-change", {
3766
+ detail: {
3767
+ chipName: this.chipName,
3768
+ message: `${this.chipName} USB port changed after reset. Please select the new port.`,
3769
+ reason: "wdt-reset-to-firmware",
3770
+ },
3771
+ }),
3772
+ );
3576
3773
 
3577
- // Return true to indicate port selection is needed
3578
- return true;
3579
- } else {
3580
- // Port stays the same - release reader/writer so console can use the stream
3774
+ return true;
3775
+ } else {
3776
+ // Port stays the same - release reader/writer now if not already done
3777
+ if (isUsbJtagOrOtg) {
3581
3778
  await this.releaseReaderWriter();
3582
- return false;
3779
+ this.logger.debug("Reader/writer released after reset");
3583
3780
  }
3781
+ return false;
3584
3782
  }
3585
3783
  } catch (err) {
3586
- this.logger.debug(`Could not reset device to firmware mode: ${err}`);
3587
- // Continue anyway - console mode might still work
3784
+ this.logger.error(`Reset to firmware mode failed: ${err}`);
3785
+
3786
+ // For USB-JTAG/OTG, the port is likely dead after a failed reset
3787
+ // For external serial, the port is usually still fine
3788
+ if (isUsbJtagOrOtg) {
3789
+ this.logger.debug(
3790
+ "Forcing port reselection due to USB-JTAG/OTG reset failure",
3791
+ );
3792
+ return true;
3793
+ }
3794
+ this.logger.debug(
3795
+ "External serial reset failed, but port should still be usable",
3796
+ );
3797
+ return false;
3588
3798
  }
3589
- return false;
3590
3799
  }
3591
3800
 
3592
3801
  /**
@@ -3839,7 +4048,7 @@ export class ESPLoader extends EventTarget {
3839
4048
  // Detect chip type
3840
4049
  await this.detectChip();
3841
4050
 
3842
- this.logger.log(`Reconnected to bootloader: ${this.chipName}`);
4051
+ this.logger.debug(`Reconnected to bootloader: ${this.chipName}`);
3843
4052
  } catch (err) {
3844
4053
  // Ensure flag is reset on error
3845
4054
  this._isReconfiguring = false;
@@ -3882,7 +4091,7 @@ export class ESPLoader extends EventTarget {
3882
4091
 
3883
4092
  if (isUsbOtgChip && isUsbJtagOrOtg) {
3884
4093
  // USB-OTG devices: Need to reset to bootloader, which will cause port change
3885
- this.logger.log(`${this.chipName} USB: Resetting to bootloader mode`);
4094
+ this.logger.debug(`${this.chipName} USB: Resetting to bootloader mode`);
3886
4095
 
3887
4096
  // Perform hardware reset to bootloader (GPIO0=LOW)
3888
4097
  // This will cause the port to change from CDC (firmware) to JTAG (bootloader)
@@ -3896,7 +4105,7 @@ export class ESPLoader extends EventTarget {
3896
4105
  // Wait for reset to complete and port to change
3897
4106
  await sleep(500);
3898
4107
 
3899
- this.logger.log(
4108
+ this.logger.debug(
3900
4109
  `${this.chipName}: Port changed. Please select the bootloader port.`,
3901
4110
  );
3902
4111
 
@@ -3954,17 +4163,19 @@ export class ESPLoader extends EventTarget {
3954
4163
 
3955
4164
  if (!this.isConsoleResetSupported()) {
3956
4165
  this.logger.debug(
3957
- "Console reset not supported for ESP32-S2 USB-JTAG/CDC",
4166
+ "Simple Console reset not supported for ESP32-S2 USB-JTAG/CDC - using exitConsoleMode to enter bootloader",
4167
+ );
4168
+ await this.exitConsoleMode();
4169
+ this.logger.debug(
4170
+ "S2 now in bootloader mode - caller must do syncAndWdtReset on new port, then reconnect console",
3958
4171
  );
3959
- return; // Do nothing
4172
+ return;
3960
4173
  }
3961
4174
 
3962
4175
  // For other devices: Use standard firmware reset
3963
4176
  try {
3964
4177
  this.logger.debug("Resetting device in console mode");
3965
-
3966
4178
  await this.hardResetToFirmware();
3967
-
3968
4179
  this.logger.debug("Device reset complete");
3969
4180
  } catch (err) {
3970
4181
  this.logger.error(`Reset failed: ${err}`);
@@ -3972,6 +4183,47 @@ export class ESPLoader extends EventTarget {
3972
4183
  }
3973
4184
  }
3974
4185
 
4186
+ /**
4187
+ * @name syncAndWdtReset
4188
+ * Open a new bootloader port, sync with ROM (no stub, no reset strategies), and fire WDT reset.
4189
+ * This is used for ESP32-S2 USB-OTG devices which require WDT reset to switch modes.
4190
+ * After WDT reset the port will re-enumerate again.
4191
+ * The user must select the new port after this method is called.
4192
+ * @param newPort - The bootloader port selected by the user
4193
+ */
4194
+ async syncAndWdtReset(newPort: SerialPort): Promise<void> {
4195
+ if (this._parent) {
4196
+ await this._parent.syncAndWdtReset(newPort);
4197
+ return;
4198
+ }
4199
+
4200
+ this.port = newPort;
4201
+ this.connected = false;
4202
+ this.IS_STUB = false;
4203
+ this.__inputBuffer = [];
4204
+ this.__inputBufferReadIndex = 0;
4205
+ this.__totalBytesRead = 0;
4206
+
4207
+ this.logger.debug("Opening bootloader port at 115200...");
4208
+ await this.port.open({ baudRate: ESP_ROM_BAUD });
4209
+ this.connected = true;
4210
+ this.currentBaudRate = ESP_ROM_BAUD;
4211
+
4212
+ // Start read loop
4213
+ this.readLoop();
4214
+ await sleep(100);
4215
+
4216
+ // Sync with ROM only - no reset strategies, device is already in bootloader
4217
+ this.logger.debug("Syncing with bootloader ROM...");
4218
+ await this.sync();
4219
+ this.logger.debug("Bootloader sync OK, no stub");
4220
+
4221
+ // Fire WDT reset → device boots into firmware
4222
+ this.logger.debug("Firing WDT reset...");
4223
+ await this.rtcWdtResetChipSpecific();
4224
+ this.logger.debug("WDT reset fired - device will boot to firmware");
4225
+ }
4226
+
3975
4227
  /**
3976
4228
  * @name drainInputBuffer
3977
4229
  * Actively drain the input buffer by reading data for a specified time.
@@ -4200,7 +4452,7 @@ export class ESPLoader extends EventTarget {
4200
4452
  } catch (err) {
4201
4453
  if (err instanceof SlipReadError) {
4202
4454
  this.logger.debug(
4203
- `SLIP read error at ${resp.length} bytes: ${err.message}`,
4455
+ `${err.message} at byte 0x${resp.length.toString(16)}`,
4204
4456
  );
4205
4457
 
4206
4458
  // Send empty SLIP frame to abort the stub's read operation
@@ -4354,22 +4606,9 @@ export class ESPLoader extends EventTarget {
4354
4606
  if (err instanceof SlipReadError) {
4355
4607
  if (retryCount <= MAX_RETRIES) {
4356
4608
  this.logger.debug(
4357
- `${err.message} at 0x${currentAddr.toString(16)}. Draining buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`,
4609
+ `Cleared buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`,
4358
4610
  );
4359
-
4360
- try {
4361
- await this.drainInputBuffer(200);
4362
-
4363
- // Clear application buffer
4364
- await this.flushSerialBuffers();
4365
-
4366
- // Wait before retry to let hardware settle
4367
- await sleep(SYNC_TIMEOUT);
4368
-
4369
- // Continue to retry the same chunk (will send NEW read command)
4370
- } catch (drainErr) {
4371
- this.logger.debug(`Buffer drain error: ${drainErr}`);
4372
- }
4611
+ // Continue to retry the same chunk (will send NEW read command)
4373
4612
  } else {
4374
4613
  // All retries exhausted - attempt recovery by reloading stub
4375
4614
  // IMPORTANT: Do NOT close port to keep ESP32 in bootloader mode