esp32tool 1.3.2 → 1.3.4

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,25 +64,14 @@ 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
67
  ESP32S2_RTC_CNTL_OPTION1_REG,
70
68
  ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
71
69
  ESP32S3_RTC_CNTL_WDTWPROTECT_REG,
72
70
  ESP32S3_RTC_CNTL_WDTCONFIG0_REG,
73
71
  ESP32S3_RTC_CNTL_WDTCONFIG1_REG,
74
72
  ESP32S3_RTC_CNTL_WDT_WKEY,
75
- ESP32S3_GPIO_STRAP_REG,
76
- ESP32S3_GPIO_STRAP_SPI_BOOT_MASK,
77
73
  ESP32S3_RTC_CNTL_OPTION1_REG,
78
74
  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
75
  ESP32C3_EFUSE_RD_MAC_SPI_SYS_3_REG,
87
76
  ESP32C3_EFUSE_RD_MAC_SPI_SYS_5_REG,
88
77
  ESP32C3_RTC_CNTL_WDTWPROTECT_REG,
@@ -93,22 +82,12 @@ import {
93
82
  ESP32C5_C6_RTC_CNTL_WDTCONFIG0_REG,
94
83
  ESP32C5_C6_RTC_CNTL_WDTCONFIG1_REG,
95
84
  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
85
  ESP32P4_RTC_CNTL_WDTWPROTECT_REG,
101
86
  ESP32P4_RTC_CNTL_WDTCONFIG0_REG,
102
87
  ESP32P4_RTC_CNTL_WDTCONFIG1_REG,
103
88
  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
89
  ESP32P4_RTC_CNTL_OPTION1_REG,
109
90
  ESP32P4_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
110
- ESP32H2_UARTDEV_BUF_NO,
111
- ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
112
91
  } from "./const";
113
92
  import { getStubCode } from "./stubs";
114
93
  import { hexFormatter, sleep, slipEncode, toHex } from "./util";
@@ -144,6 +123,10 @@ export class ESPLoader extends EventTarget {
144
123
  public currentBaudRate: number = ESP_ROM_BAUD;
145
124
  private _maxUSBSerialBaudrate?: number;
146
125
  private _reader?: ReadableStreamDefaultReader<Uint8Array>;
126
+ private SLIP_END = 0xc0;
127
+ private SLIP_ESC = 0xdb;
128
+ private SLIP_ESC_END = 0xdc;
129
+ private SLIP_ESC_ESC = 0xdd;
147
130
  private _isESP32S2NativeUSB: boolean = false;
148
131
  private _initializationSucceeded: boolean = false;
149
132
  private __commandLock: Promise<[number, number[]]> = Promise.resolve([0, []]);
@@ -457,10 +440,8 @@ export class ESPLoader extends EventTarget {
457
440
  0x303a: {
458
441
  // Espressif (native USB)
459
442
  0x2: { name: "ESP32-S2 Native USB", maxBaudrate: 2000000 },
443
+ 0x12: { name: "ESP32-P4 Native USB", maxBaudrate: 2000000 },
460
444
  0x1001: { name: "ESP32 Native USB", maxBaudrate: 2000000 },
461
- 0x1002: { name: "ESP32 Native USB", maxBaudrate: 2000000 },
462
- 0x4002: { name: "ESP32 Native USB", maxBaudrate: 2000000 },
463
- 0x1000: { name: "ESP32 Native USB", maxBaudrate: 2000000 },
464
445
  },
465
446
  };
466
447
 
@@ -757,7 +738,7 @@ export class ESPLoader extends EventTarget {
757
738
 
758
739
  // Always read from browser's serial buffer immediately
759
740
  // to prevent browser buffer overflow. Don't apply back-pressure here.
760
- const chunk = Array.from(value);
741
+ const chunk = Array.from(value as Uint8Array);
761
742
  Array.prototype.push.apply(this._inputBuffer, chunk);
762
743
 
763
744
  // Track total bytes read from serial port
@@ -1500,101 +1481,6 @@ export class ESPLoader extends EventTarget {
1500
1481
  await this.rtcWdtResetChipSpecific();
1501
1482
  }
1502
1483
 
1503
- /**
1504
- * Check if current chip is using USB-OTG
1505
- * Supports ESP32-S2 and ESP32-S3
1506
- */
1507
- public async usingUsbOtg(): Promise<boolean> {
1508
- let uartDevBufNo: number;
1509
- let usbOtgValue: number;
1510
-
1511
- if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
1512
- uartDevBufNo = ESP32S2_UARTDEV_BUF_NO;
1513
- usbOtgValue = ESP32S2_UARTDEV_BUF_NO_USB_OTG;
1514
- } else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1515
- uartDevBufNo = ESP32S3_UARTDEV_BUF_NO;
1516
- usbOtgValue = ESP32S3_UARTDEV_BUF_NO_USB_OTG;
1517
- } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1518
- // P4: UARTDEV_BUF_NO depends on chip revision
1519
- if (this.chipRevision === null) {
1520
- this.chipRevision = await this.getChipRevision();
1521
- }
1522
-
1523
- if (this.chipRevision < 300) {
1524
- uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV0;
1525
- } else {
1526
- uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV300;
1527
- }
1528
- usbOtgValue = ESP32P4_UARTDEV_BUF_NO_USB_OTG;
1529
- } else {
1530
- return false;
1531
- }
1532
-
1533
- const uartNo = (await this.readRegister(uartDevBufNo)) & 0xff;
1534
- return uartNo === usbOtgValue;
1535
- }
1536
-
1537
- /**
1538
- * Check if current chip is using USB-JTAG/Serial
1539
- * Supports ESP32-S3 and ESP32-C3
1540
- */
1541
- public async usingUsbJtagSerial(): Promise<boolean> {
1542
- let uartDevBufNo: number;
1543
- let usbJtagSerialValue: number;
1544
-
1545
- if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1546
- uartDevBufNo = ESP32S3_UARTDEV_BUF_NO;
1547
- usbJtagSerialValue = ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1548
- } else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
1549
- // ESP32-C3: BSS_UART_DEV_ADDR depends on chip revision
1550
- // Revision < 101: 0x3FCDF064
1551
- // Revision >= 101: 0x3FCDF060
1552
- let bssUartDevAddr: number;
1553
-
1554
- // Get chip revision if not already set
1555
- if (this.chipRevision === null) {
1556
- this.chipRevision = await this.getChipRevisionC3();
1557
- }
1558
-
1559
- if (this.chipRevision < 101) {
1560
- bssUartDevAddr = 0x3fcdf064;
1561
- } else {
1562
- bssUartDevAddr = 0x3fcdf060;
1563
- }
1564
-
1565
- uartDevBufNo = bssUartDevAddr + ESP32C3_BUF_UART_NO_OFFSET;
1566
- usbJtagSerialValue = ESP32C3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1567
- } else if (this.chipFamily === CHIP_FAMILY_ESP32C5) {
1568
- uartDevBufNo = ESP32C5_UARTDEV_BUF_NO;
1569
- usbJtagSerialValue = ESP32C5_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1570
- } else if (this.chipFamily === CHIP_FAMILY_ESP32C6) {
1571
- uartDevBufNo = ESP32C6_UARTDEV_BUF_NO;
1572
- usbJtagSerialValue = ESP32C6_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1573
- } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1574
- // P4: UARTDEV_BUF_NO depends on chip revision
1575
- // Revision < 300: 0x4FF3FEC8
1576
- // Revision >= 300: 0x4FFBFEC8
1577
- if (this.chipRevision === null) {
1578
- this.chipRevision = await this.getChipRevision();
1579
- }
1580
-
1581
- if (this.chipRevision < 300) {
1582
- uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV0;
1583
- } else {
1584
- uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV300;
1585
- }
1586
- usbJtagSerialValue = ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1587
- } else if (this.chipFamily === CHIP_FAMILY_ESP32H2) {
1588
- uartDevBufNo = ESP32H2_UARTDEV_BUF_NO;
1589
- usbJtagSerialValue = ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
1590
- } else {
1591
- return false;
1592
- }
1593
-
1594
- const uartNo = (await this.readRegister(uartDevBufNo)) & 0xff;
1595
- return uartNo === usbJtagSerialValue;
1596
- }
1597
-
1598
1484
  /**
1599
1485
  * Get chip revision for ESP32-C3
1600
1486
  * Reads from EFUSE registers and calculates revision
@@ -1670,52 +1556,6 @@ export class ESPLoader extends EventTarget {
1670
1556
  // Unlock watchdog registers
1671
1557
  await this.writeRegister(WDTWPROTECT_REG, WDT_WKEY, undefined, 0);
1672
1558
 
1673
- // Clear force download boot register (if applicable) BEFORE triggering WDT reset
1674
- // This ensures the chip boots into firmware mode after reset
1675
- if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
1676
- try {
1677
- await this.writeRegister(
1678
- ESP32S2_RTC_CNTL_OPTION1_REG,
1679
- 0,
1680
- ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
1681
- 0,
1682
- );
1683
- this.logger.debug("Cleared force download boot mask");
1684
- } catch (err) {
1685
- this.logger.debug(
1686
- `Expected error clearing force download boot mask: ${err}`,
1687
- );
1688
- }
1689
- } else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1690
- try {
1691
- await this.writeRegister(
1692
- ESP32S3_RTC_CNTL_OPTION1_REG,
1693
- 0,
1694
- ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
1695
- 0,
1696
- );
1697
- this.logger.debug("Cleared force download boot mask");
1698
- } catch (err) {
1699
- this.logger.debug(
1700
- `Expected error clearing force download boot mask: ${err}`,
1701
- );
1702
- }
1703
- } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1704
- try {
1705
- await this.writeRegister(
1706
- ESP32P4_RTC_CNTL_OPTION1_REG,
1707
- 0,
1708
- ESP32P4_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
1709
- 0,
1710
- );
1711
- this.logger.debug("Cleared force download boot mask");
1712
- } catch (err) {
1713
- this.logger.debug(
1714
- `Expected error clearing force download boot mask: ${err}`,
1715
- );
1716
- }
1717
- }
1718
-
1719
1559
  // Set WDT timeout to 2000ms (matches Python esptool)
1720
1560
  await this.writeRegister(WDTCONFIG1_REG, 2000, undefined, 0);
1721
1561
 
@@ -1731,84 +1571,30 @@ export class ESPLoader extends EventTarget {
1731
1571
  }
1732
1572
 
1733
1573
  /**
1734
- * Helper: Check if USB-based WDT reset should be used for S2/S3
1574
+ * Helper: USB-based WDT reset
1735
1575
  * Returns true if WDT reset was performed, false otherwise
1736
1576
  */
1737
- private async tryUsbWdtReset(
1738
- chipName: string,
1739
- GPIO_STRAP_REG: number,
1740
- GPIO_STRAP_SPI_BOOT_MASK: number,
1741
- RTC_CNTL_OPTION1_REG: number,
1742
- RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK: number,
1743
- ): Promise<boolean> {
1744
- const isUsingUsbOtg = await this.usingUsbOtg();
1745
- const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
1746
-
1747
- if (isUsingUsbOtg || isUsingUsbJtagSerial) {
1748
- const strapReg = await this.readRegister(GPIO_STRAP_REG);
1749
- const forceDlReg = await this.readRegister(RTC_CNTL_OPTION1_REG);
1750
-
1751
- // Only use watchdog reset if GPIO0 is low AND force download boot mode is not set
1752
- if (
1753
- (strapReg & GPIO_STRAP_SPI_BOOT_MASK) === 0 &&
1754
- (forceDlReg & RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK) === 0
1755
- ) {
1756
- await this.rtcWdtResetChipSpecific();
1757
- this.logger.debug(
1758
- `${chipName}: RTC WDT reset (USB detected, GPIO0 low)`,
1759
- );
1760
- return true;
1761
- }
1762
- }
1763
- return false;
1764
- }
1577
+ private async tryUsbWdtReset(chipName: string): Promise<boolean> {
1578
+ const isUsingUsbOtg = await this.detectUsbConnectionType();
1765
1579
 
1766
- /**
1767
- * Chip-specific hard reset for ESP32-S2
1768
- * Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
1769
- */
1770
- public async hardResetS2(): Promise<void> {
1771
- const isUsingUsbOtg = await this.usingUsbOtg();
1772
1580
  if (isUsingUsbOtg) {
1581
+ // Use WDT reset for USB-OTG devices
1773
1582
  await this.rtcWdtResetChipSpecific();
1774
- this.logger.debug("ESP32-S2: RTC WDT reset (USB-OTG detected)");
1775
- } else {
1776
- // Use standard hardware reset
1777
- await this.hardResetClassic();
1778
- this.logger.debug("ESP32-S2: Classic reset");
1779
- }
1780
- }
1781
-
1782
- /**
1783
- * Chip-specific hard reset for ESP32-S3
1784
- * Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
1785
- */
1786
- public async hardResetS3(): Promise<void> {
1787
- const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
1788
- if (isUsingUsbJtagSerial) {
1789
- await this.rtcWdtResetChipSpecific();
1790
- this.logger.debug("ESP32-S3: RTC WDT reset (USB-JTAG/Serial detected)");
1791
- } else {
1792
- // Use standard hardware reset
1793
- await this.hardResetClassic();
1794
- this.logger.debug("ESP32-S3: Classic reset");
1795
- }
1796
- }
1797
-
1798
- /**
1799
- * Chip-specific hard reset for ESP32-C3
1800
- * Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
1801
- */
1802
- public async hardResetC3(): Promise<void> {
1803
- const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
1804
- if (isUsingUsbJtagSerial) {
1805
- await this.rtcWdtResetChipSpecific();
1806
- this.logger.debug("ESP32-C3: RTC WDT reset (USB-JTAG/Serial detected)");
1583
+ this.logger.debug(
1584
+ `${chipName}: RTC WDT reset (USB-JTAG/Serial or USB-OTG detected)`,
1585
+ );
1586
+ return true;
1807
1587
  } else {
1808
- // Use standard hardware reset
1809
- await this.hardResetClassic();
1810
- this.logger.debug("ESP32-C3: Classic reset");
1588
+ // Use classic reset for non-USB devices
1589
+ if (this.isWebUSB()) {
1590
+ await this.hardResetClassicWebUSB();
1591
+ this.logger.debug("Classic reset (WebUSB/Android).");
1592
+ } else {
1593
+ await this.hardResetClassic();
1594
+ this.logger.debug("Classic reset.");
1595
+ }
1811
1596
  }
1597
+ return false;
1812
1598
  }
1813
1599
 
1814
1600
  async hardReset(bootloader = false) {
@@ -1848,27 +1634,25 @@ export class ESPLoader extends EventTarget {
1848
1634
  }
1849
1635
  } else {
1850
1636
  // just reset (no bootloader mode)
1851
- // For ESP32-S2/S3 with USB-OTG or USB-JTAG/Serial, check if watchdog reset is needed
1852
- if (this.chipFamily === CHIP_FAMILY_ESP32S2 && !this._consoleMode) {
1853
- const wdtResetUsed = await this.tryUsbWdtReset(
1854
- "ESP32-S2",
1855
- ESP32S2_GPIO_STRAP_REG,
1856
- ESP32S2_GPIO_STRAP_SPI_BOOT_MASK,
1857
- ESP32S2_RTC_CNTL_OPTION1_REG,
1858
- ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
1859
- );
1637
+ // For ESP32-S2/S3/P4 with USB-OTG or USB-JTAG/Serial, check if watchdog reset is needed
1638
+ this.logger.debug("*** Performing WDT reset strategy ***");
1639
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
1640
+ const wdtResetUsed = await this.tryUsbWdtReset("ESP32-S2");
1860
1641
  if (wdtResetUsed) return;
1861
- } else if (
1862
- this.chipFamily === CHIP_FAMILY_ESP32S3 &&
1863
- !this._consoleMode
1864
- ) {
1865
- const wdtResetUsed = await this.tryUsbWdtReset(
1866
- "ESP32-S3",
1867
- ESP32S3_GPIO_STRAP_REG,
1868
- ESP32S3_GPIO_STRAP_SPI_BOOT_MASK,
1869
- ESP32S3_RTC_CNTL_OPTION1_REG,
1870
- ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
1871
- );
1642
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1643
+ const wdtResetUsed = await this.tryUsbWdtReset("ESP32-S3");
1644
+ if (wdtResetUsed) return;
1645
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1646
+ const wdtResetUsed = await this.tryUsbWdtReset("ESP32-P4");
1647
+ if (wdtResetUsed) return;
1648
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
1649
+ const wdtResetUsed = await this.tryUsbWdtReset("ESP32-C3");
1650
+ if (wdtResetUsed) return;
1651
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32C5) {
1652
+ const wdtResetUsed = await this.tryUsbWdtReset("ESP32-C5");
1653
+ if (wdtResetUsed) return;
1654
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32C6) {
1655
+ const wdtResetUsed = await this.tryUsbWdtReset("ESP32-C6");
1872
1656
  if (wdtResetUsed) return;
1873
1657
  }
1874
1658
 
@@ -2126,47 +1910,47 @@ export class ESPLoader extends EventTarget {
2126
1910
  "Timed out waiting for packet " + waitingFor,
2127
1911
  );
2128
1912
  }
2129
- const b = this._readByte()!;
1913
+ const byte = this._readByte()!;
2130
1914
 
2131
1915
  if (partialPacket === null) {
2132
1916
  // waiting for packet header
2133
- if (b == 0xc0) {
1917
+ if (byte == this.SLIP_END) {
2134
1918
  partialPacket = [];
2135
1919
  } else {
2136
1920
  if (this.debug) {
2137
- this.logger.debug("Read invalid data: " + toHex(b));
1921
+ this.logger.debug("Read invalid data: " + toHex(byte));
2138
1922
  this.logger.debug(
2139
1923
  "Remaining data in serial buffer: " +
2140
1924
  hexFormatter(this._inputBuffer),
2141
1925
  );
2142
1926
  }
2143
1927
  throw new SlipReadError(
2144
- "Invalid head of packet (" + toHex(b) + ")",
1928
+ "Invalid head of packet (" + toHex(byte) + ")",
2145
1929
  );
2146
1930
  }
2147
1931
  } else if (inEscape) {
2148
1932
  // part-way through escape sequence
2149
1933
  inEscape = false;
2150
- if (b == 0xdc) {
2151
- partialPacket.push(0xc0);
2152
- } else if (b == 0xdd) {
2153
- partialPacket.push(0xdb);
1934
+ if (byte == this.SLIP_ESC_END) {
1935
+ partialPacket.push(this.SLIP_END);
1936
+ } else if (byte == this.SLIP_ESC_ESC) {
1937
+ partialPacket.push(this.SLIP_ESC);
2154
1938
  } else {
2155
1939
  if (this.debug) {
2156
- this.logger.debug("Read invalid data: " + toHex(b));
1940
+ this.logger.debug("Read invalid data: " + toHex(byte));
2157
1941
  this.logger.debug(
2158
1942
  "Remaining data in serial buffer: " +
2159
1943
  hexFormatter(this._inputBuffer),
2160
1944
  );
2161
1945
  }
2162
1946
  throw new SlipReadError(
2163
- "Invalid SLIP escape (0xdb, " + toHex(b) + ")",
1947
+ "Invalid SLIP escape (0xdb, " + toHex(byte) + ")",
2164
1948
  );
2165
1949
  }
2166
- } else if (b == 0xdb) {
1950
+ } else if (byte == this.SLIP_ESC) {
2167
1951
  // start of escape sequence
2168
1952
  inEscape = true;
2169
- } else if (b == 0xc0) {
1953
+ } else if (byte == this.SLIP_END) {
2170
1954
  // end of packet
2171
1955
  if (this.debug)
2172
1956
  this.logger.debug(
@@ -2177,7 +1961,7 @@ export class ESPLoader extends EventTarget {
2177
1961
  return partialPacket;
2178
1962
  } else {
2179
1963
  // normal byte in packet
2180
- partialPacket.push(b);
1964
+ partialPacket.push(byte);
2181
1965
  }
2182
1966
  }
2183
1967
  }
@@ -2211,46 +1995,46 @@ export class ESPLoader extends EventTarget {
2211
1995
  this.logger.debug(
2212
1996
  "Read " + readBytes.length + " bytes: " + hexFormatter(readBytes),
2213
1997
  );
2214
- for (const b of readBytes) {
1998
+ for (const byte of readBytes) {
2215
1999
  if (partialPacket === null) {
2216
2000
  // waiting for packet header
2217
- if (b == 0xc0) {
2001
+ if (byte == this.SLIP_END) {
2218
2002
  partialPacket = [];
2219
2003
  } else {
2220
2004
  if (this.debug) {
2221
- this.logger.debug("Read invalid data: " + toHex(b));
2005
+ this.logger.debug("Read invalid data: " + toHex(byte));
2222
2006
  this.logger.debug(
2223
2007
  "Remaining data in serial buffer: " +
2224
2008
  hexFormatter(this._inputBuffer),
2225
2009
  );
2226
2010
  }
2227
2011
  throw new SlipReadError(
2228
- "Invalid head of packet (" + toHex(b) + ")",
2012
+ "Invalid head of packet (" + toHex(byte) + ")",
2229
2013
  );
2230
2014
  }
2231
2015
  } else if (inEscape) {
2232
2016
  // part-way through escape sequence
2233
2017
  inEscape = false;
2234
- if (b == 0xdc) {
2235
- partialPacket.push(0xc0);
2236
- } else if (b == 0xdd) {
2237
- partialPacket.push(0xdb);
2018
+ if (byte == this.SLIP_ESC_END) {
2019
+ partialPacket.push(this.SLIP_END);
2020
+ } else if (byte == this.SLIP_ESC_ESC) {
2021
+ partialPacket.push(this.SLIP_ESC);
2238
2022
  } else {
2239
2023
  if (this.debug) {
2240
- this.logger.debug("Read invalid data: " + toHex(b));
2024
+ this.logger.debug("Read invalid data: " + toHex(byte));
2241
2025
  this.logger.debug(
2242
2026
  "Remaining data in serial buffer: " +
2243
2027
  hexFormatter(this._inputBuffer),
2244
2028
  );
2245
2029
  }
2246
2030
  throw new SlipReadError(
2247
- "Invalid SLIP escape (0xdb, " + toHex(b) + ")",
2031
+ "Invalid SLIP escape (0xdb, " + toHex(byte) + ")",
2248
2032
  );
2249
2033
  }
2250
- } else if (b == 0xdb) {
2034
+ } else if (byte == this.SLIP_ESC) {
2251
2035
  // start of escape sequence
2252
2036
  inEscape = true;
2253
- } else if (b == 0xc0) {
2037
+ } else if (byte == this.SLIP_END) {
2254
2038
  // end of packet
2255
2039
  if (this.debug)
2256
2040
  this.logger.debug(
@@ -2261,7 +2045,7 @@ export class ESPLoader extends EventTarget {
2261
2045
  return partialPacket;
2262
2046
  } else {
2263
2047
  // normal byte in packet
2264
- partialPacket.push(b);
2048
+ partialPacket.push(byte);
2265
2049
  }
2266
2050
  }
2267
2051
  }
@@ -3257,7 +3041,6 @@ export class ESPLoader extends EventTarget {
3257
3041
  try {
3258
3042
  this._reader.cancel();
3259
3043
  } catch (err) {
3260
- // this.logger.debug(`Reader cancel error: ${err}`);
3261
3044
  // Reader already released, resolve immediately
3262
3045
  clearTimeout(timeout);
3263
3046
  resolve(undefined);
@@ -3285,14 +3068,6 @@ export class ESPLoader extends EventTarget {
3285
3068
  return;
3286
3069
  }
3287
3070
 
3288
- // Check if device is in JTAG mode and needs reset to boot into firmware
3289
- const didReconnect = await this._resetToFirmwareIfNeeded();
3290
-
3291
- // If we reconnected for console, the reader/writer are already released and restarted
3292
- if (didReconnect) {
3293
- return;
3294
- }
3295
-
3296
3071
  // Wait for pending writes to complete
3297
3072
  try {
3298
3073
  await this._writeChain;
@@ -3347,37 +3122,36 @@ export class ESPLoader extends EventTarget {
3347
3122
  /**
3348
3123
  * @name detectUsbConnectionType
3349
3124
  * Detect if device is using USB-JTAG/Serial or USB-OTG (not external serial chip)
3350
- * This helper extracts the detection logic from initialize() for reuse
3125
+ * Uses USB PID (Product ID) for reliable detection - does NOT require chipFamily
3351
3126
  * @returns true if USB-JTAG or USB-OTG, false if external serial chip
3352
- * @throws Error if detection fails and chipFamily is not set
3353
3127
  */
3354
- private async detectUsbConnectionType(): Promise<boolean> {
3355
- if (!this.chipFamily) {
3356
- throw new Error("Cannot detect USB connection type: chipFamily not set");
3357
- }
3128
+ public async detectUsbConnectionType(): Promise<boolean> {
3129
+ // Use PID-based detection
3130
+ const portInfo = this.port.getInfo();
3131
+ const pid = portInfo.usbProductId;
3132
+ const vid = portInfo.usbVendorId;
3358
3133
 
3359
- if (
3360
- this.chipFamily === CHIP_FAMILY_ESP32S2 ||
3361
- this.chipFamily === CHIP_FAMILY_ESP32S3
3362
- ) {
3363
- const isUsingUsbOtg = await this.usingUsbOtg();
3364
- const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
3365
- return isUsingUsbOtg || isUsingUsbJtagSerial;
3366
- } else if (
3367
- this.chipFamily === CHIP_FAMILY_ESP32C3 ||
3368
- this.chipFamily === CHIP_FAMILY_ESP32C5 ||
3369
- this.chipFamily === CHIP_FAMILY_ESP32C6
3370
- ) {
3371
- const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
3372
- return isUsingUsbJtagSerial;
3373
- } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
3374
- const isUsingUsbOtg = await this.usingUsbOtg();
3375
- const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
3376
- return isUsingUsbOtg || isUsingUsbJtagSerial;
3377
- } else {
3378
- // Other chips don't have USB-JTAG/OTG
3134
+ // Check if this is an Espressif device
3135
+ const isEspressif = vid === 0x303a;
3136
+
3137
+ if (!isEspressif) {
3138
+ this.logger.debug("Not Espressif VID - external serial chip");
3379
3139
  return false;
3380
3140
  }
3141
+
3142
+ // ESP32-S2/S3/C3/C5/C6/C61/H2/P4 USB-JTAG/OTG PIDs
3143
+ // According to official Espressif documentation:
3144
+ // https://docs.espressif.com/projects/esp-iot-solution/en/latest/usb/usb_overview/usb_device_const_COM.html
3145
+ // 0x0002 = ESP32-S2 USB-OTG, 0x0012 = ESP32-P4 USB-Serial-JTAG
3146
+ // 0x1001 = ESP32-S3, C3, C5, C6, C61, H2 USB-Serial-JTAG
3147
+ const usbJtagPids = [0x0002, 0x0012, 0x1001];
3148
+ const isUsbJtag = usbJtagPids.includes(pid || 0);
3149
+
3150
+ this.logger.debug(
3151
+ `USB-JTAG/OTG detection: ${isUsbJtag ? "YES" : "NO"} (PID=0x${pid?.toString(16)})`,
3152
+ );
3153
+
3154
+ return isUsbJtag;
3381
3155
  }
3382
3156
 
3383
3157
  /**
@@ -3387,17 +3161,24 @@ export class ESPLoader extends EventTarget {
3387
3161
  * @returns true if port was closed (USB-JTAG), false if port stays open (serial chip)
3388
3162
  */
3389
3163
  public async enterConsoleMode(): Promise<boolean> {
3390
- // Set console mode flag
3391
- this._consoleMode = true;
3164
+ // Check if port is open - if not, we need a new port selection
3165
+ if (!this.port.writable || !this.port.readable) {
3166
+ this.logger.debug("Port is not open - port selection needed");
3167
+ // Return true to signal that port selection is needed
3168
+ // The caller should handle port selection and try again
3169
+ return true;
3170
+ }
3392
3171
 
3393
3172
  // Re-detect USB connection type to ensure we have a definitive value
3394
- // This handles cases where isUsbJtagOrOtg might be undefined
3395
3173
  let isUsbJtag: boolean;
3396
3174
  try {
3397
3175
  isUsbJtag = await this.detectUsbConnectionType();
3398
3176
  this.logger.debug(
3399
3177
  `USB connection type detected: ${isUsbJtag ? "USB-JTAG/OTG" : "External Serial Chip"}`,
3400
3178
  );
3179
+
3180
+ // CRITICAL: Set the cached value so _resetToFirmwareIfNeeded() can use it
3181
+ this._isUsbJtagOrOtg = isUsbJtag;
3401
3182
  } catch (err) {
3402
3183
  // If detection fails, fall back to cached value or fail-fast
3403
3184
  if (this.isUsbJtagOrOtg === undefined) {
@@ -3405,12 +3186,17 @@ export class ESPLoader extends EventTarget {
3405
3186
  `Cannot enter console mode: USB connection type unknown and detection failed: ${err}`,
3406
3187
  );
3407
3188
  }
3189
+ // Set console mode flag
3190
+ this._consoleMode = false;
3191
+
3408
3192
  this.logger.debug(
3409
3193
  `USB detection failed, using cached value: ${this.isUsbJtagOrOtg}`,
3410
3194
  );
3411
3195
  isUsbJtag = this.isUsbJtagOrOtg;
3412
3196
  }
3413
3197
 
3198
+ // Release reader/writer so console can create new ones
3199
+ // This is needed for Desktop (Web Serial) to unlock streams
3414
3200
  if (isUsbJtag) {
3415
3201
  // USB-JTAG/OTG devices: Use watchdog reset which closes port
3416
3202
  const wasReset = await this._resetToFirmwareIfNeeded();
@@ -3432,6 +3218,23 @@ export class ESPLoader extends EventTarget {
3432
3218
  this.logger.debug(`Could not reset device: ${err}`);
3433
3219
  }
3434
3220
 
3221
+ // For WebUSB (Android), recreate streams after hardware reset
3222
+ if (this.isWebUSB()) {
3223
+ try {
3224
+ // Use the public recreateStreams() method to safely recreate streams
3225
+ // without closing the port (important after hardware reset)
3226
+ await (this.port as any).recreateStreams();
3227
+ this.logger.debug("WebUSB streams recreated for console mode");
3228
+ } catch (err) {
3229
+ // Set console mode flag
3230
+ this._consoleMode = false;
3231
+ this.logger.debug(`Failed to recreate WebUSB streams: ${err}`);
3232
+ }
3233
+ }
3234
+
3235
+ // Set console mode flag
3236
+ this._consoleMode = true;
3237
+
3435
3238
  return false; // Port stays open
3436
3239
  }
3437
3240
  }
@@ -3442,80 +3245,179 @@ export class ESPLoader extends EventTarget {
3442
3245
  * Detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
3443
3246
  * @returns true if reconnect was performed, false otherwise
3444
3247
  */
3445
- private async _resetToFirmwareIfNeeded(): Promise<boolean> {
3248
+ /**
3249
+ * @name _clearForceDownloadBootIfNeeded
3250
+ * Read and clear the force download boot flag if it is set
3251
+ * This should ONLY be called when on ROM (not stub) and before WDT reset
3252
+ * Clearing it on every connect causes issues with flash operations
3253
+ * Returns true if the flag was cleared, false if it was already clear
3254
+ */
3255
+ private async _clearForceDownloadBootIfNeeded(): Promise<boolean> {
3446
3256
  try {
3447
- // Check if device is using USB-JTAG/Serial or USB-OTG
3448
- // Value should already be set during main() connection
3449
- // Use getter to access parent's value if this is a stub
3450
- const needsReset = this.isUsbJtagOrOtg === true;
3451
-
3452
- if (needsReset) {
3453
- const resetMethod =
3454
- this.chipFamily === CHIP_FAMILY_ESP32S2 ||
3455
- this.chipFamily === CHIP_FAMILY_ESP32S3
3456
- ? "USB-JTAG/Serial or USB-OTG"
3457
- : "USB-JTAG/Serial";
3257
+ let regAddr: number;
3258
+ let mask: number;
3259
+ let chipName: string;
3260
+
3261
+ // Get register address and mask for this chip
3262
+ if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
3263
+ regAddr = ESP32S2_RTC_CNTL_OPTION1_REG;
3264
+ mask = ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK;
3265
+ chipName = "ESP32-S2";
3266
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
3267
+ regAddr = ESP32S3_RTC_CNTL_OPTION1_REG;
3268
+ mask = ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK;
3269
+ chipName = "ESP32-S3";
3270
+ } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
3271
+ regAddr = ESP32P4_RTC_CNTL_OPTION1_REG;
3272
+ mask = ESP32P4_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK;
3273
+ chipName = "ESP32-P4";
3274
+ } else {
3275
+ // Not a chip that needs this
3276
+ return false;
3277
+ }
3458
3278
 
3459
- this.logger.log(
3460
- `Resetting ${this.chipName || "device"} (${resetMethod}) to boot into firmware...`,
3279
+ // Read current register value
3280
+ const currentValue = await this.readRegister(regAddr);
3281
+ this.logger.debug(
3282
+ `${chipName} force download boot register: 0x${currentValue.toString(16)} (mask: 0x${mask.toString(16)})`,
3283
+ );
3284
+
3285
+ // Check if the flag is set
3286
+ const isFlagSet = (currentValue & mask) !== 0;
3287
+
3288
+ if (isFlagSet) {
3289
+ this.logger.debug(
3290
+ `${chipName} force download boot flag is SET - clearing it`,
3461
3291
  );
3292
+ // Clear the flag by writing 0 to the masked bits
3293
+ await this.writeRegister(regAddr, 0, mask, 0);
3294
+ this.logger.debug(`${chipName} force download boot flag cleared`);
3295
+ return true;
3296
+ } else {
3297
+ this.logger.debug(
3298
+ `${chipName} force download boot flag is already CLEAR - no action needed`,
3299
+ );
3300
+ return false;
3301
+ }
3302
+ } catch (err) {
3303
+ this.logger.debug(`Error checking/clearing force download flag: ${err}`);
3304
+ return false;
3305
+ }
3306
+ }
3462
3307
 
3463
- // Set console mode flag before reset to prevent subsequent hardReset calls
3464
- this._consoleMode = true;
3308
+ private async _resetToFirmwareIfNeeded(): Promise<boolean> {
3309
+ try {
3310
+ // Check if port is open - if not, assume device is already in firmware mode
3311
+ if (!this.port.writable || !this.port.readable) {
3312
+ this.logger.debug(
3313
+ "Port is not open - assuming device is already in firmware mode",
3314
+ );
3315
+ return false;
3316
+ }
3465
3317
 
3466
- // For S2/S3: Clear force download boot mask before WDT reset
3467
- if (
3468
- this.chipFamily === CHIP_FAMILY_ESP32S2 ||
3469
- this.chipFamily === CHIP_FAMILY_ESP32S3
3470
- ) {
3471
- const OPTION1_REG =
3472
- this.chipFamily === CHIP_FAMILY_ESP32S2
3473
- ? ESP32S2_RTC_CNTL_OPTION1_REG
3474
- : ESP32S3_RTC_CNTL_OPTION1_REG;
3475
- const FORCE_DOWNLOAD_BOOT_MASK =
3476
- this.chipFamily === CHIP_FAMILY_ESP32S2
3477
- ? ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK
3478
- : ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK;
3318
+ const isUsingUsbOtg = await this.detectUsbConnectionType();
3479
3319
 
3480
- try {
3481
- // Clear force download boot mode to avoid chip being stuck in download mode
3482
- await this.writeRegister(
3483
- OPTION1_REG,
3484
- 0,
3485
- FORCE_DOWNLOAD_BOOT_MASK,
3486
- 0,
3487
- );
3488
- this.logger.debug("Cleared force download boot mask");
3489
- } catch (err) {
3320
+ if (isUsingUsbOtg) {
3321
+ // For USB-OTG devices, we need to check if force download flag is set
3322
+ // Only if it's set, we need WDT reset (which causes port change)
3323
+ // If it's clear, we can use normal reset (no port change)
3324
+
3325
+ if (this.IS_STUB) {
3326
+ this.logger.debug("On stub - need to get back to ROM to check flag");
3327
+
3328
+ // If we're running at higher baudrate, we need to change back to ROM baudrate
3329
+ if (this.currentBaudRate !== ESP_ROM_BAUD) {
3490
3330
  this.logger.debug(
3491
- `Expected error clearing force download boot mask: ${err}`,
3331
+ `Changing baudrate from ${this.currentBaudRate} to ${ESP_ROM_BAUD} for ROM`,
3492
3332
  );
3333
+ try {
3334
+ await this.reconfigurePort(ESP_ROM_BAUD);
3335
+ this.currentBaudRate = ESP_ROM_BAUD;
3336
+ } catch (err) {
3337
+ this.logger.debug(`Baudrate change failed: ${err}`);
3338
+ // Continue anyway
3339
+ }
3493
3340
  }
3341
+
3342
+ this.logger.debug("Resetting to bootloader (ROM)...");
3343
+
3344
+ // Reset to bootloader - this will clear the stub from RAM
3345
+ try {
3346
+ await this.hardReset(true);
3347
+
3348
+ // Wait for reset to complete
3349
+ await sleep(200);
3350
+
3351
+ // Sync with ROM
3352
+ await this.sync();
3353
+
3354
+ this.logger.debug("Now on ROM after reset");
3355
+
3356
+ // Mark that we're no longer on stub
3357
+ this.IS_STUB = false;
3358
+ } catch (resetErr) {
3359
+ this.logger.debug(`Reset to ROM failed: ${resetErr}`);
3360
+ // If reset fails, we might already be in firmware mode
3361
+ // In this case, we don't need to do anything - just use normal reset
3362
+ this.logger.debug("Assuming device is already in firmware mode");
3363
+
3364
+ // Release reader/writer before returning
3365
+ await this.releaseReaderWriter();
3366
+ return false; // No port change needed
3367
+ }
3368
+ } else {
3369
+ this.logger.debug("Already on ROM - checking force download flag");
3494
3370
  }
3495
3371
 
3496
- // Perform watchdog reset to reboot into firmware
3497
- try {
3498
- await this.rtcWdtResetChipSpecific();
3499
- this.logger.debug("Watchdog reset triggered successfully");
3500
- } catch (err) {
3501
- // Error is expected - device resets before responding
3372
+ // Now check if force download flag is set and clear it if needed
3373
+ const flagWasCleared = await this._clearForceDownloadBootIfNeeded();
3374
+
3375
+ if (flagWasCleared) {
3376
+ this.logger.debug(
3377
+ "Force download flag was cleared - device will boot to firmware after reset",
3378
+ );
3379
+ } else {
3502
3380
  this.logger.debug(
3503
- `Watchdog reset initiated (connection lost as expected: ${err})`,
3381
+ "Force download flag already clear - device will boot to firmware after reset",
3504
3382
  );
3505
3383
  }
3506
3384
 
3507
- // Wait for device to fully boot into firmware
3508
- this.logger.log("Waiting for device to boot into firmware...");
3509
- await this.sleep(1000);
3385
+ // Perform WDT reset BEFORE releasing reader/writer (needs communication)
3386
+ // After WDT reset, the device will reboot into firmware mode
3387
+ await this.hardReset(false);
3510
3388
 
3511
- // After WDT reset, streams are dead/locked - don't try to manipulate them
3512
- // Just mark everything as disconnected and let browser clean up
3513
- this.connected = false;
3514
- this._writer = undefined;
3515
- this._reader = undefined;
3389
+ // For USB-OTG devices (ESP32-S2, ESP32-P4), the port will change after WDT reset
3390
+ const portWillChange =
3391
+ (this.chipFamily === CHIP_FAMILY_ESP32S2 && isUsingUsbOtg) ||
3392
+ (this.chipFamily === CHIP_FAMILY_ESP32P4 && isUsingUsbOtg);
3516
3393
 
3517
- this.logger.debug("Device reset to firmware mode (port closed)");
3518
- return true;
3394
+ if (portWillChange) {
3395
+ // Port will change - release reader/writer and let the port become invalid
3396
+ await this.releaseReaderWriter();
3397
+
3398
+ this.logger.log(
3399
+ `${this.chipName} USB-OTG: Port will change after WDT reset`,
3400
+ );
3401
+ this.logger.log("Please select the new port for console mode");
3402
+
3403
+ // Dispatch event to signal port change
3404
+ this.dispatchEvent(
3405
+ new CustomEvent("usb-otg-port-change", {
3406
+ detail: {
3407
+ chipName: this.chipName,
3408
+ message: `${this.chipName} USB port changed after reset. Please select the new port.`,
3409
+ reason: "wdt-reset-to-firmware",
3410
+ },
3411
+ }),
3412
+ );
3413
+
3414
+ // Return true to indicate port selection is needed
3415
+ return true;
3416
+ } else {
3417
+ // Port stays the same - release reader/writer so console can use the stream
3418
+ await this.releaseReaderWriter();
3419
+ return false;
3420
+ }
3519
3421
  }
3520
3422
  } catch (err) {
3521
3423
  this.logger.debug(`Could not reset device to firmware mode: ${err}`);
@@ -3788,35 +3690,61 @@ export class ESPLoader extends EventTarget {
3788
3690
  // Clear console mode flag
3789
3691
  this._consoleMode = false;
3790
3692
 
3791
- // Check if this is ESP32-S2 with USB-JTAG/OTG
3792
- const isESP32S2 = this.chipFamily === CHIP_FAMILY_ESP32S2;
3693
+ // Check if this is a USB-OTG device (ESP32-S2 or ESP32-P4)
3694
+ const isUsbOtgChip =
3695
+ this.chipFamily === CHIP_FAMILY_ESP32S2 ||
3696
+ this.chipFamily === CHIP_FAMILY_ESP32P4;
3793
3697
 
3794
- // For ESP32-S2: if _isUsbJtagOrOtg is undefined, try to detect it
3698
+ // For USB-OTG chips: if _isUsbJtagOrOtg is undefined, try to detect it
3795
3699
  // If detection fails or is undefined, assume USB-JTAG/OTG (conservative/safe path)
3796
3700
  let isUsbJtagOrOtg = this._isUsbJtagOrOtg;
3797
- if (isESP32S2 && isUsbJtagOrOtg === undefined) {
3701
+ if (isUsbOtgChip && isUsbJtagOrOtg === undefined) {
3798
3702
  try {
3799
3703
  isUsbJtagOrOtg = await this.detectUsbConnectionType();
3800
3704
  } catch (err) {
3801
3705
  this.logger.debug(
3802
- `USB detection failed, assuming USB-JTAG/OTG for ESP32-S2: ${err}`,
3706
+ `USB detection failed, assuming USB-JTAG/OTG for ${this.chipName}: ${err}`,
3803
3707
  );
3804
- isUsbJtagOrOtg = true; // Conservative fallback for ESP32-S2
3708
+ isUsbJtagOrOtg = true; // Conservative fallback
3805
3709
  }
3806
3710
  }
3807
3711
 
3808
- if (isESP32S2 && isUsbJtagOrOtg) {
3809
- // ESP32-S2 USB: Use reconnectToBootloader which handles the mode switch
3810
- // This will close the port and the device will reboot to bootloader
3811
- this.logger.log("ESP32-S2 USB detected - reconnecting to bootloader");
3712
+ if (isUsbOtgChip && isUsbJtagOrOtg) {
3713
+ // USB-OTG devices: Need to reset to bootloader, which will cause port change
3714
+ this.logger.log(`${this.chipName} USB: Resetting to bootloader mode`);
3812
3715
 
3716
+ // Perform hardware reset to bootloader (GPIO0=LOW)
3717
+ // This will cause the port to change from CDC (firmware) to JTAG (bootloader)
3813
3718
  try {
3814
- await this.reconnectToBootloader();
3719
+ if (this.isWebUSB()) {
3720
+ await this.hardResetClassicWebUSB();
3721
+ } else {
3722
+ await this.hardResetClassic();
3723
+ }
3724
+ this.logger.debug("Reset to bootloader initiated");
3815
3725
  } catch (err) {
3816
- this.logger.debug(`Reconnect error (expected for ESP32-S2): ${err}`);
3726
+ this.logger.debug(`Reset error: ${err}`);
3817
3727
  }
3818
3728
 
3819
- // For ESP32-S2, port will change, so return true to indicate manual reconnection needed
3729
+ // Wait for reset to complete and port to change
3730
+ await sleep(500);
3731
+
3732
+ this.logger.log(
3733
+ `${this.chipName}: Port changed. Please select the bootloader port.`,
3734
+ );
3735
+
3736
+ // Dispatch event to signal port change
3737
+ this.dispatchEvent(
3738
+ new CustomEvent("usb-otg-port-change", {
3739
+ detail: {
3740
+ chipName: this.chipName,
3741
+ message: `${this.chipName}: Port changed. Please select the bootloader port.`,
3742
+ reason: "exit-console-to-bootloader",
3743
+ },
3744
+ }),
3745
+ );
3746
+
3747
+ // Port will change, so return true to indicate manual reconnection needed
3820
3748
  return true;
3821
3749
  }
3822
3750
 
@@ -3898,7 +3826,7 @@ export class ESPLoader extends EventTarget {
3898
3826
  await sleep(bufferingTime);
3899
3827
 
3900
3828
  // Unsupported command response is sent 8 times and has
3901
- // 14 bytes length including delimiter 0xC0 bytes.
3829
+ // 14 bytes length including delimiter SLIP_END (0xC0) bytes.
3902
3830
  // At least part of it is read as a command response,
3903
3831
  // but to be safe, read it all.
3904
3832
  const bytesToDrain = 14 * 8;
@@ -4118,7 +4046,7 @@ export class ESPLoader extends EventTarget {
4118
4046
  // The stub expects 4 bytes (ACK), if we send less it will break out
4119
4047
  try {
4120
4048
  // Send SLIP frame with no data (just delimiters)
4121
- const abortFrame = [0xc0, 0xc0]; // Empty SLIP frame
4049
+ const abortFrame = [this.SLIP_END, this.SLIP_END]; // Empty SLIP frame
4122
4050
  await this.writeToStream(abortFrame);
4123
4051
  this.logger.debug(`Sent abort frame to stub`);
4124
4052