tasmota-webserial-esptool 7.2.6 → 7.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/esp_loader.d.ts +13 -0
  2. package/dist/esp_loader.js +342 -176
  3. package/dist/stubs/esp32.json +3 -3
  4. package/dist/stubs/esp32c2.json +3 -3
  5. package/dist/stubs/esp32c3.json +4 -4
  6. package/dist/stubs/esp32c5.json +4 -4
  7. package/dist/stubs/esp32c6.json +4 -4
  8. package/dist/stubs/esp32c61.json +4 -4
  9. package/dist/stubs/esp32h2.json +4 -4
  10. package/dist/stubs/esp32p4.json +4 -4
  11. package/dist/stubs/esp32p4r3.json +4 -4
  12. package/dist/stubs/esp32s2.json +3 -3
  13. package/dist/stubs/esp32s3.json +4 -4
  14. package/dist/web/esp32-BRKoi17y.js +1 -0
  15. package/dist/web/esp32c2-Btgr_lwh.js +1 -0
  16. package/dist/web/esp32c3-BGQu6Tl5.js +1 -0
  17. package/dist/web/esp32c5-0b050IXn.js +1 -0
  18. package/dist/web/esp32c6-D9SxtU9b.js +1 -0
  19. package/dist/web/esp32c61-B2dSOrao.js +1 -0
  20. package/dist/web/esp32h2-BBdaXb2C.js +1 -0
  21. package/dist/web/esp32p4-BLGlFHot.js +1 -0
  22. package/dist/web/esp32p4r3-CEI3EOJv.js +1 -0
  23. package/dist/web/esp32s2-iX3WoDbg.js +1 -0
  24. package/dist/web/esp32s3-BUw3lf0r.js +1 -0
  25. package/dist/web/index.js +1 -1
  26. package/js/modules/esp32-BRKoi17y.js +1 -0
  27. package/js/modules/esp32c2-Btgr_lwh.js +1 -0
  28. package/js/modules/esp32c3-BGQu6Tl5.js +1 -0
  29. package/js/modules/esp32c5-0b050IXn.js +1 -0
  30. package/js/modules/esp32c6-D9SxtU9b.js +1 -0
  31. package/js/modules/esp32c61-B2dSOrao.js +1 -0
  32. package/js/modules/esp32h2-BBdaXb2C.js +1 -0
  33. package/js/modules/esp32p4-BLGlFHot.js +1 -0
  34. package/js/modules/esp32p4r3-CEI3EOJv.js +1 -0
  35. package/js/modules/esp32s2-iX3WoDbg.js +1 -0
  36. package/js/modules/esp32s3-BUw3lf0r.js +1 -0
  37. package/js/modules/esptool.js +1 -1
  38. package/package.json +1 -1
  39. package/src/esp_loader.ts +370 -187
  40. package/src/stubs/esp32.json +3 -3
  41. package/src/stubs/esp32c2.json +3 -3
  42. package/src/stubs/esp32c3.json +4 -4
  43. package/src/stubs/esp32c5.json +4 -4
  44. package/src/stubs/esp32c6.json +4 -4
  45. package/src/stubs/esp32c61.json +4 -4
  46. package/src/stubs/esp32h2.json +4 -4
  47. package/src/stubs/esp32p4.json +4 -4
  48. package/src/stubs/esp32p4r3.json +4 -4
  49. package/src/stubs/esp32s2.json +3 -3
  50. package/src/stubs/esp32s3.json +4 -4
  51. package/dist/web/esp32-CijhsJH1.js +0 -1
  52. package/dist/web/esp32c2-C17SM4gO.js +0 -1
  53. package/dist/web/esp32c3-DxRGijbg.js +0 -1
  54. package/dist/web/esp32c5-3mDOIGa4.js +0 -1
  55. package/dist/web/esp32c6-h6U0SQTm.js +0 -1
  56. package/dist/web/esp32c61-BKtexhPZ.js +0 -1
  57. package/dist/web/esp32h2-RtuWSEmP.js +0 -1
  58. package/dist/web/esp32p4-5nkIjxqJ.js +0 -1
  59. package/dist/web/esp32p4r3-CpHBYEwI.js +0 -1
  60. package/dist/web/esp32s2-IiDBtXxo.js +0 -1
  61. package/dist/web/esp32s3-6yv5yxum.js +0 -1
  62. package/js/modules/esp32-CijhsJH1.js +0 -1
  63. package/js/modules/esp32c2-C17SM4gO.js +0 -1
  64. package/js/modules/esp32c3-DxRGijbg.js +0 -1
  65. package/js/modules/esp32c5-3mDOIGa4.js +0 -1
  66. package/js/modules/esp32c6-h6U0SQTm.js +0 -1
  67. package/js/modules/esp32c61-BKtexhPZ.js +0 -1
  68. package/js/modules/esp32h2-RtuWSEmP.js +0 -1
  69. package/js/modules/esp32p4-5nkIjxqJ.js +0 -1
  70. package/js/modules/esp32p4r3-CpHBYEwI.js +0 -1
  71. package/js/modules/esp32s2-IiDBtXxo.js +0 -1
  72. package/js/modules/esp32s3-6yv5yxum.js +0 -1
package/src/esp_loader.ts CHANGED
@@ -85,6 +85,8 @@ export class ESPLoader extends EventTarget {
85
85
  private _reader?: ReadableStreamDefaultReader<Uint8Array>;
86
86
  private _isESP32S2NativeUSB: boolean = false;
87
87
  private _initializationSucceeded: boolean = false;
88
+ private __commandLock: Promise<[number, number[]]> = Promise.resolve([0, []]);
89
+ private _isReconfiguring: boolean = false;
88
90
 
89
91
  constructor(
90
92
  public port: SerialPort,
@@ -112,6 +114,18 @@ export class ESPLoader extends EventTarget {
112
114
  }
113
115
  }
114
116
 
117
+ private get _commandLock(): Promise<[number, number[]]> {
118
+ return this._parent ? this._parent._commandLock : this.__commandLock;
119
+ }
120
+
121
+ private set _commandLock(value: Promise<[number, number[]]>) {
122
+ if (this._parent) {
123
+ this._parent._commandLock = value;
124
+ } else {
125
+ this.__commandLock = value;
126
+ }
127
+ }
128
+
115
129
  private detectUSBSerialChip(
116
130
  vendorId: number,
117
131
  productId: number,
@@ -549,6 +563,9 @@ export class ESPLoader extends EventTarget {
549
563
  * Send a command packet, check that the command succeeded and
550
564
  * return a tuple with the value and data.
551
565
  * See the ESP Serial Protocol for more details on what value/data are
566
+ *
567
+ * Commands are serialized to prevent concurrent execution which can cause
568
+ * WritableStream lock contention on CP210x adapters under Windows
552
569
  */
553
570
  async checkCommand(
554
571
  opcode: number,
@@ -556,69 +573,77 @@ export class ESPLoader extends EventTarget {
556
573
  checksum = 0,
557
574
  timeout = DEFAULT_TIMEOUT,
558
575
  ): Promise<[number, number[]]> {
559
- timeout = Math.min(timeout, MAX_TIMEOUT);
560
- await this.sendCommand(opcode, buffer, checksum);
561
- const [value, responseData] = await this.getResponse(opcode, timeout);
562
-
563
- if (responseData === null) {
564
- throw new Error("Didn't get enough status bytes");
565
- }
566
-
567
- let data = responseData;
568
- let statusLen = 0;
576
+ // Serialize command execution to prevent lock contention
577
+ const executeCommand = async (): Promise<[number, number[]]> => {
578
+ timeout = Math.min(timeout, MAX_TIMEOUT);
579
+ await this.sendCommand(opcode, buffer, checksum);
580
+ const [value, responseData] = await this.getResponse(opcode, timeout);
581
+
582
+ if (responseData === null) {
583
+ throw new Error("Didn't get enough status bytes");
584
+ }
569
585
 
570
- if (this.IS_STUB || this.chipFamily == CHIP_FAMILY_ESP8266) {
571
- statusLen = 2;
572
- } else if (
573
- [
574
- CHIP_FAMILY_ESP32,
575
- CHIP_FAMILY_ESP32S2,
576
- CHIP_FAMILY_ESP32S3,
577
- CHIP_FAMILY_ESP32C2,
578
- CHIP_FAMILY_ESP32C3,
579
- CHIP_FAMILY_ESP32C5,
580
- CHIP_FAMILY_ESP32C6,
581
- CHIP_FAMILY_ESP32C61,
582
- CHIP_FAMILY_ESP32H2,
583
- CHIP_FAMILY_ESP32H4,
584
- CHIP_FAMILY_ESP32H21,
585
- CHIP_FAMILY_ESP32P4,
586
- CHIP_FAMILY_ESP32S31,
587
- ].includes(this.chipFamily)
588
- ) {
589
- statusLen = 4;
590
- } else {
591
- // When chipFamily is not yet set (e.g., during GET_SECURITY_INFO in detectChip),
592
- // assume modern chips use 4-byte status
593
- if (opcode === ESP_GET_SECURITY_INFO) {
586
+ let data = responseData;
587
+ let statusLen = 0;
588
+
589
+ if (this.IS_STUB || this.chipFamily == CHIP_FAMILY_ESP8266) {
590
+ statusLen = 2;
591
+ } else if (
592
+ [
593
+ CHIP_FAMILY_ESP32,
594
+ CHIP_FAMILY_ESP32S2,
595
+ CHIP_FAMILY_ESP32S3,
596
+ CHIP_FAMILY_ESP32C2,
597
+ CHIP_FAMILY_ESP32C3,
598
+ CHIP_FAMILY_ESP32C5,
599
+ CHIP_FAMILY_ESP32C6,
600
+ CHIP_FAMILY_ESP32C61,
601
+ CHIP_FAMILY_ESP32H2,
602
+ CHIP_FAMILY_ESP32H4,
603
+ CHIP_FAMILY_ESP32H21,
604
+ CHIP_FAMILY_ESP32P4,
605
+ CHIP_FAMILY_ESP32S31,
606
+ ].includes(this.chipFamily)
607
+ ) {
594
608
  statusLen = 4;
595
- } else if ([2, 4].includes(data.length)) {
596
- statusLen = data.length;
609
+ } else {
610
+ // When chipFamily is not yet set (e.g., during GET_SECURITY_INFO in detectChip),
611
+ // assume modern chips use 4-byte status
612
+ if (opcode === ESP_GET_SECURITY_INFO) {
613
+ statusLen = 4;
614
+ } else if ([2, 4].includes(data.length)) {
615
+ statusLen = data.length;
616
+ }
597
617
  }
598
- }
599
618
 
600
- if (data.length < statusLen) {
601
- throw new Error("Didn't get enough status bytes");
602
- }
603
- const status = data.slice(-statusLen, data.length);
604
- data = data.slice(0, -statusLen);
605
- if (this.debug) {
606
- this.logger.debug("status", status);
607
- this.logger.debug("value", value);
608
- this.logger.debug("data", data);
609
- }
610
- if (status[0] == 1) {
611
- if (status[1] == ROM_INVALID_RECV_MSG) {
612
- // Unsupported command can result in more than one error response
613
- // Use drainInputBuffer for CP210x compatibility on Windows
614
- await this.drainInputBuffer(200);
615
- throw new Error("Invalid (unsupported) command " + toHex(opcode));
616
- } else {
617
- throw new Error("Command failure error code " + toHex(status[1]));
619
+ if (data.length < statusLen) {
620
+ throw new Error("Didn't get enough status bytes");
621
+ }
622
+ const status = data.slice(-statusLen, data.length);
623
+ data = data.slice(0, -statusLen);
624
+ if (this.debug) {
625
+ this.logger.debug("status", status);
626
+ this.logger.debug("value", value);
627
+ this.logger.debug("data", data);
628
+ }
629
+ if (status[0] == 1) {
630
+ if (status[1] == ROM_INVALID_RECV_MSG) {
631
+ // Unsupported command can result in more than one error response
632
+ // Use drainInputBuffer for CP210x compatibility on Windows
633
+ await this.drainInputBuffer(200);
634
+ throw new Error("Invalid (unsupported) command " + toHex(opcode));
635
+ } else {
636
+ throw new Error("Command failure error code " + toHex(status[1]));
637
+ }
618
638
  }
619
- }
620
639
 
621
- return [value, data];
640
+ return [value, data];
641
+ };
642
+
643
+ // Chain command execution through the lock
644
+ // Use both .then() handlers to ensure lock continues even on error
645
+ this._commandLock = this._commandLock.then(executeCommand, executeCommand);
646
+ return this._commandLock;
622
647
  }
623
648
 
624
649
  /**
@@ -649,37 +674,34 @@ export class ESPLoader extends EventTarget {
649
674
  async readPacket(timeout: number): Promise<number[]> {
650
675
  let partialPacket: number[] | null = null;
651
676
  let inEscape = false;
652
- let readBytes: number[] = [];
677
+
678
+ const startTime = Date.now();
679
+
653
680
  while (true) {
654
- const stamp = Date.now();
655
- readBytes = [];
656
- while (Date.now() - stamp < timeout) {
657
- if (this._inputBuffer.length > 0) {
658
- readBytes.push(this._inputBuffer.shift()!);
659
- break;
660
- } else {
661
- // Reduced sleep time for faster response during high-speed transfers
662
- await sleep(1);
663
- }
664
- }
665
- if (readBytes.length == 0) {
681
+ // Check timeout
682
+ if (Date.now() - startTime > timeout) {
666
683
  const waitingFor = partialPacket === null ? "header" : "content";
667
684
  throw new SlipReadError("Timed out waiting for packet " + waitingFor);
668
685
  }
669
- if (this.debug)
670
- this.logger.debug(
671
- "Read " + readBytes.length + " bytes: " + hexFormatter(readBytes),
672
- );
673
- for (const b of readBytes) {
686
+
687
+ // If no data available, wait a bit
688
+ if (this._inputBuffer.length === 0) {
689
+ await sleep(1);
690
+ continue;
691
+ }
692
+
693
+ // Process all available bytes without going back to outer loop
694
+ // This is critical for handling high-speed burst transfers
695
+ while (this._inputBuffer.length > 0) {
696
+ const b = this._inputBuffer.shift()!;
697
+
674
698
  if (partialPacket === null) {
675
699
  // waiting for packet header
676
700
  if (b == 0xc0) {
677
701
  partialPacket = [];
678
702
  } else {
679
703
  if (this.debug) {
680
- this.logger.debug(
681
- "Read invalid data: " + hexFormatter(readBytes),
682
- );
704
+ this.logger.debug("Read invalid data: " + toHex(b));
683
705
  this.logger.debug(
684
706
  "Remaining data in serial buffer: " +
685
707
  hexFormatter(this._inputBuffer),
@@ -698,9 +720,7 @@ export class ESPLoader extends EventTarget {
698
720
  partialPacket.push(0xdb);
699
721
  } else {
700
722
  if (this.debug) {
701
- this.logger.debug(
702
- "Read invalid data: " + hexFormatter(readBytes),
703
- );
723
+ this.logger.debug("Read invalid data: " + toHex(b));
704
724
  this.logger.debug(
705
725
  "Remaining data in serial buffer: " +
706
726
  hexFormatter(this._inputBuffer),
@@ -726,7 +746,6 @@ export class ESPLoader extends EventTarget {
726
746
  }
727
747
  }
728
748
  }
729
- throw new SlipReadError("Invalid state");
730
749
  }
731
750
 
732
751
  /**
@@ -825,6 +844,25 @@ export class ESPLoader extends EventTarget {
825
844
 
826
845
  async reconfigurePort(baud: number) {
827
846
  try {
847
+ this._isReconfiguring = true;
848
+
849
+ // Wait for pending writes to complete
850
+ try {
851
+ await this._writeChain;
852
+ } catch (err) {
853
+ this.logger.debug(`Pending write error during reconfigure: ${err}`);
854
+ }
855
+
856
+ // Release persistent writer before closing
857
+ if (this._writer) {
858
+ try {
859
+ this._writer.releaseLock();
860
+ } catch (err) {
861
+ this.logger.debug(`Writer release error during reconfigure: ${err}`);
862
+ }
863
+ this._writer = undefined;
864
+ }
865
+
828
866
  // SerialPort does not allow to be reconfigured while open so we close and re-open
829
867
  // reader.cancel() causes the Promise returned by the read() operation running on
830
868
  // the readLoop to return immediately with { value: undefined, done: true } and thus
@@ -843,6 +881,8 @@ export class ESPLoader extends EventTarget {
843
881
  } catch (e) {
844
882
  this.logger.error(`Reconfigure port error: ${e}`);
845
883
  throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
884
+ } finally {
885
+ this._isReconfiguring = false;
846
886
  }
847
887
  }
848
888
 
@@ -1589,18 +1629,98 @@ export class ESPLoader extends EventTarget {
1589
1629
  return espStubLoader;
1590
1630
  }
1591
1631
 
1632
+ __writer?: WritableStreamDefaultWriter<Uint8Array>;
1633
+ __writeChain: Promise<void> = Promise.resolve();
1634
+
1635
+ private get _writer(): WritableStreamDefaultWriter<Uint8Array> | undefined {
1636
+ return this._parent ? this._parent._writer : this.__writer;
1637
+ }
1638
+
1639
+ private set _writer(
1640
+ value: WritableStreamDefaultWriter<Uint8Array> | undefined,
1641
+ ) {
1642
+ if (this._parent) {
1643
+ this._parent._writer = value;
1644
+ } else {
1645
+ this.__writer = value;
1646
+ }
1647
+ }
1648
+
1649
+ private get _writeChain(): Promise<void> {
1650
+ return this._parent ? this._parent._writeChain : this.__writeChain;
1651
+ }
1652
+
1653
+ private set _writeChain(value: Promise<void>) {
1654
+ if (this._parent) {
1655
+ this._parent._writeChain = value;
1656
+ } else {
1657
+ this.__writeChain = value;
1658
+ }
1659
+ }
1660
+
1592
1661
  async writeToStream(data: number[]) {
1593
1662
  if (!this.port.writable) {
1594
1663
  this.logger.debug("Port writable stream not available, skipping write");
1595
1664
  return;
1596
1665
  }
1597
- const writer = this.port.writable.getWriter();
1598
- await writer.write(new Uint8Array(data));
1599
- try {
1600
- writer.releaseLock();
1601
- } catch (err) {
1602
- this.logger.error(`Ignoring release lock error: ${err}`);
1666
+
1667
+ if (this._isReconfiguring) {
1668
+ throw new Error("Cannot write during port reconfiguration");
1603
1669
  }
1670
+
1671
+ // Queue writes to prevent lock contention (critical for CP2102 on Windows)
1672
+ this._writeChain = this._writeChain
1673
+ .then(
1674
+ async () => {
1675
+ // Check if port is still writable before attempting write
1676
+ if (!this.port.writable) {
1677
+ throw new Error("Port became unavailable during write");
1678
+ }
1679
+
1680
+ // Get or create persistent writer
1681
+ if (!this._writer) {
1682
+ try {
1683
+ this._writer = this.port.writable.getWriter();
1684
+ } catch (err) {
1685
+ this.logger.error(`Failed to get writer: ${err}`);
1686
+ throw err;
1687
+ }
1688
+ }
1689
+
1690
+ // Perform the write
1691
+ await this._writer.write(new Uint8Array(data));
1692
+ },
1693
+ async () => {
1694
+ // Previous write failed, but still attempt this write
1695
+ if (!this.port.writable) {
1696
+ throw new Error("Port became unavailable during write");
1697
+ }
1698
+
1699
+ // Writer was likely cleaned up by previous error, create new one
1700
+ if (!this._writer) {
1701
+ this._writer = this.port.writable.getWriter();
1702
+ }
1703
+
1704
+ await this._writer.write(new Uint8Array(data));
1705
+ },
1706
+ )
1707
+ .catch((err) => {
1708
+ this.logger.error(`Write error: ${err}`);
1709
+ // Ensure writer is cleaned up on any error
1710
+ if (this._writer) {
1711
+ try {
1712
+ this._writer.releaseLock();
1713
+ } catch (e) {
1714
+ // Ignore release errors
1715
+ }
1716
+ this._writer = undefined;
1717
+ }
1718
+ // Re-throw to propagate error
1719
+ throw err;
1720
+ });
1721
+
1722
+ // Always await the write chain to ensure errors are caught
1723
+ await this._writeChain;
1604
1724
  }
1605
1725
 
1606
1726
  async disconnect() {
@@ -1612,15 +1732,49 @@ export class ESPLoader extends EventTarget {
1612
1732
  this.logger.debug("Port already closed, skipping disconnect");
1613
1733
  return;
1614
1734
  }
1615
- await this.port.writable.getWriter().close();
1616
- await new Promise((resolve) => {
1617
- if (!this._reader) {
1618
- resolve(undefined);
1735
+
1736
+ try {
1737
+ this._isReconfiguring = true;
1738
+
1739
+ // Wait for pending writes to complete
1740
+ try {
1741
+ await this._writeChain;
1742
+ } catch (err) {
1743
+ this.logger.debug(`Pending write error during disconnect: ${err}`);
1619
1744
  }
1620
- this.addEventListener("disconnect", resolve, { once: true });
1621
- this._reader!.cancel();
1622
- });
1623
- this.connected = false;
1745
+
1746
+ // Release persistent writer before closing
1747
+ if (this._writer) {
1748
+ try {
1749
+ await this._writer.close();
1750
+ this._writer.releaseLock();
1751
+ } catch (err) {
1752
+ this.logger.debug(`Writer close/release error: ${err}`);
1753
+ }
1754
+ this._writer = undefined;
1755
+ } else {
1756
+ // No persistent writer exists, close stream directly
1757
+ // This path is taken when no writes have been queued
1758
+ try {
1759
+ const writer = this.port.writable.getWriter();
1760
+ await writer.close();
1761
+ writer.releaseLock();
1762
+ } catch (err) {
1763
+ this.logger.debug(`Direct writer close error: ${err}`);
1764
+ }
1765
+ }
1766
+
1767
+ await new Promise((resolve) => {
1768
+ if (!this._reader) {
1769
+ resolve(undefined);
1770
+ }
1771
+ this.addEventListener("disconnect", resolve, { once: true });
1772
+ this._reader!.cancel();
1773
+ });
1774
+ this.connected = false;
1775
+ } finally {
1776
+ this._isReconfiguring = false;
1777
+ }
1624
1778
  }
1625
1779
 
1626
1780
  /**
@@ -1633,99 +1787,122 @@ export class ESPLoader extends EventTarget {
1633
1787
  return;
1634
1788
  }
1635
1789
 
1636
- this.logger.log("Reconnecting serial port...");
1790
+ try {
1791
+ this._isReconfiguring = true;
1792
+
1793
+ this.logger.log("Reconnecting serial port...");
1637
1794
 
1638
- this.connected = false;
1639
- this.__inputBuffer = [];
1795
+ this.connected = false;
1796
+ this.__inputBuffer = [];
1640
1797
 
1641
- // Cancel reader
1642
- if (this._reader) {
1798
+ // Wait for pending writes to complete
1643
1799
  try {
1644
- await this._reader.cancel();
1800
+ await this._writeChain;
1645
1801
  } catch (err) {
1646
- this.logger.debug(`Reader cancel error: ${err}`);
1802
+ this.logger.debug(`Pending write error during reconnect: ${err}`);
1647
1803
  }
1648
- this._reader = undefined;
1649
- }
1650
-
1651
- // Close port
1652
- try {
1653
- await this.port.close();
1654
- this.logger.log("Port closed");
1655
- } catch (err) {
1656
- this.logger.debug(`Port close error: ${err}`);
1657
- }
1658
1804
 
1659
- // Open the port
1660
- this.logger.debug("Opening port...");
1661
- try {
1662
- await this.port.open({ baudRate: ESP_ROM_BAUD });
1663
- this.connected = true;
1664
- } catch (err) {
1665
- throw new Error(`Failed to open port: ${err}`);
1666
- }
1805
+ // Release persistent writer
1806
+ if (this._writer) {
1807
+ try {
1808
+ this._writer.releaseLock();
1809
+ } catch (err) {
1810
+ this.logger.debug(`Writer release error during reconnect: ${err}`);
1811
+ }
1812
+ this._writer = undefined;
1813
+ }
1667
1814
 
1668
- // Verify port streams are available
1669
- if (!this.port.readable || !this.port.writable) {
1670
- throw new Error(
1671
- `Port streams not available after open (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`,
1672
- );
1673
- }
1815
+ // Cancel reader
1816
+ if (this._reader) {
1817
+ try {
1818
+ await this._reader.cancel();
1819
+ } catch (err) {
1820
+ this.logger.debug(`Reader cancel error: ${err}`);
1821
+ }
1822
+ this._reader = undefined;
1823
+ }
1674
1824
 
1675
- // Save chip info and flash size (no need to detect again)
1676
- const savedChipFamily = this.chipFamily;
1677
- const savedChipName = this.chipName;
1678
- const savedChipRevision = this.chipRevision;
1679
- const savedChipVariant = this.chipVariant;
1680
- const savedFlashSize = this.flashSize;
1825
+ // Close port
1826
+ try {
1827
+ await this.port.close();
1828
+ this.logger.log("Port closed");
1829
+ } catch (err) {
1830
+ this.logger.debug(`Port close error: ${err}`);
1831
+ }
1681
1832
 
1682
- // Reinitialize
1683
- await this.hardReset(true);
1833
+ // Open the port
1834
+ this.logger.debug("Opening port...");
1835
+ try {
1836
+ await this.port.open({ baudRate: ESP_ROM_BAUD });
1837
+ this.connected = true;
1838
+ } catch (err) {
1839
+ throw new Error(`Failed to open port: ${err}`);
1840
+ }
1684
1841
 
1685
- if (!this._parent) {
1686
- this.__inputBuffer = [];
1687
- this.__totalBytesRead = 0;
1688
- this.readLoop();
1689
- }
1842
+ // Verify port streams are available
1843
+ if (!this.port.readable || !this.port.writable) {
1844
+ throw new Error(
1845
+ `Port streams not available after open (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`,
1846
+ );
1847
+ }
1690
1848
 
1691
- await this.flushSerialBuffers();
1692
- await this.sync();
1849
+ // Save chip info and flash size (no need to detect again)
1850
+ const savedChipFamily = this.chipFamily;
1851
+ const savedChipName = this.chipName;
1852
+ const savedChipRevision = this.chipRevision;
1853
+ const savedChipVariant = this.chipVariant;
1854
+ const savedFlashSize = this.flashSize;
1693
1855
 
1694
- // Restore chip info
1695
- this.chipFamily = savedChipFamily;
1696
- this.chipName = savedChipName;
1697
- this.chipRevision = savedChipRevision;
1698
- this.chipVariant = savedChipVariant;
1699
- this.flashSize = savedFlashSize;
1856
+ // Reinitialize
1857
+ await this.hardReset(true);
1700
1858
 
1701
- this.logger.debug(`Reconnect complete (chip: ${this.chipName})`);
1859
+ if (!this._parent) {
1860
+ this.__inputBuffer = [];
1861
+ this.__totalBytesRead = 0;
1862
+ this.readLoop();
1863
+ }
1702
1864
 
1703
- // Verify port is ready
1704
- if (!this.port.writable || !this.port.readable) {
1705
- throw new Error("Port not ready after reconnect");
1706
- }
1865
+ await this.flushSerialBuffers();
1866
+ await this.sync();
1707
1867
 
1708
- // Load stub
1709
- const stubLoader = await this.runStub(true);
1710
- this.logger.debug("Stub loaded");
1868
+ // Restore chip info
1869
+ this.chipFamily = savedChipFamily;
1870
+ this.chipName = savedChipName;
1871
+ this.chipRevision = savedChipRevision;
1872
+ this.chipVariant = savedChipVariant;
1873
+ this.flashSize = savedFlashSize;
1711
1874
 
1712
- // Restore baudrate if it was changed
1713
- if (this._currentBaudRate !== ESP_ROM_BAUD) {
1714
- await stubLoader.setBaudrate(this._currentBaudRate);
1875
+ this.logger.debug(`Reconnect complete (chip: ${this.chipName})`);
1715
1876
 
1716
- // Verify port is still ready after baudrate change
1877
+ // Verify port is ready
1717
1878
  if (!this.port.writable || !this.port.readable) {
1718
- throw new Error(
1719
- `Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`,
1720
- );
1879
+ throw new Error("Port not ready after reconnect");
1721
1880
  }
1722
- }
1723
1881
 
1724
- // Copy stub state to this instance if we're a stub loader
1725
- if (this.IS_STUB) {
1726
- Object.assign(this, stubLoader);
1882
+ // Load stub
1883
+ const stubLoader = await this.runStub(true);
1884
+ this.logger.debug("Stub loaded");
1885
+
1886
+ // Restore baudrate if it was changed
1887
+ if (this._currentBaudRate !== ESP_ROM_BAUD) {
1888
+ await stubLoader.setBaudrate(this._currentBaudRate);
1889
+
1890
+ // Verify port is still ready after baudrate change
1891
+ if (!this.port.writable || !this.port.readable) {
1892
+ throw new Error(
1893
+ `Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`,
1894
+ );
1895
+ }
1896
+ }
1897
+
1898
+ // Copy stub state to this instance if we're a stub loader
1899
+ if (this.IS_STUB) {
1900
+ Object.assign(this, stubLoader);
1901
+ }
1902
+ this.logger.debug("Reconnection successful");
1903
+ } finally {
1904
+ this._isReconfiguring = false;
1727
1905
  }
1728
- this.logger.debug("Reconnection successful");
1729
1906
  }
1730
1907
 
1731
1908
  /**
@@ -1837,18 +2014,22 @@ export class ESPLoader extends EventTarget {
1837
2014
  const chunkSize = Math.min(CHUNK_SIZE, remainingSize);
1838
2015
  let chunkSuccess = false;
1839
2016
  let retryCount = 0;
1840
- const MAX_RETRIES = 5;
2017
+ const MAX_RETRIES = 15;
1841
2018
 
1842
2019
  // Retry loop for this chunk
1843
2020
  while (!chunkSuccess && retryCount <= MAX_RETRIES) {
1844
2021
  let resp = new Uint8Array(0);
1845
2022
 
1846
2023
  try {
1847
- this.logger.debug(
1848
- `Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`,
1849
- );
2024
+ // Only log on first attempt or retries
2025
+ if (retryCount === 0) {
2026
+ this.logger.debug(
2027
+ `Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`,
2028
+ );
2029
+ }
1850
2030
 
1851
2031
  // Send read flash command for this chunk
2032
+ // This must be inside the retry loop so we send a fresh command after errors
1852
2033
  const pkt = pack("<IIII", currentAddr, chunkSize, 0x1000, 1024);
1853
2034
  const [res] = await this.checkCommand(ESP_READ_FLASH, pkt);
1854
2035
 
@@ -1867,20 +2048,22 @@ export class ESPLoader extends EventTarget {
1867
2048
  `SLIP read error at ${resp.length} bytes: ${err.message}`,
1868
2049
  );
1869
2050
 
1870
- // Send final ACK for any data we did receive before the error
1871
- if (resp.length > 0) {
1872
- try {
1873
- const ackData = pack("<I", resp.length);
1874
- const slipEncodedAck = slipEncode(ackData);
1875
- await this.writeToStream(slipEncodedAck);
1876
- } catch (ackErr) {
1877
- this.logger.debug(`ACK send error: ${ackErr}`);
1878
- }
2051
+ // Send empty SLIP frame to abort the stub's read operation
2052
+ // The stub expects 4 bytes (ACK), if we send less it will break out
2053
+ try {
2054
+ // Send SLIP frame with no data (just delimiters)
2055
+ const abortFrame = [0xc0, 0xc0]; // Empty SLIP frame
2056
+ await this.writeToStream(abortFrame);
2057
+ this.logger.debug(`Sent abort frame to stub`);
2058
+
2059
+ // Give stub time to process abort
2060
+ await sleep(50);
2061
+ } catch (abortErr) {
2062
+ this.logger.debug(`Abort frame error: ${abortErr}`);
1879
2063
  }
1880
2064
 
1881
- // Drain input buffer for CP210x compatibility on Windows
1882
- // This clears any stale data that may be causing the error
1883
- await this.drainInputBuffer(300);
2065
+ // Drain input buffer to clear any stale data
2066
+ await this.drainInputBuffer(200);
1884
2067
 
1885
2068
  // If we've read all the data we need, break
1886
2069
  if (resp.length >= chunkSize) {
@@ -1920,11 +2103,11 @@ export class ESPLoader extends EventTarget {
1920
2103
  if (err instanceof SlipReadError) {
1921
2104
  if (retryCount <= MAX_RETRIES) {
1922
2105
  this.logger.log(
1923
- `⚠️ ${err.message} at 0x${currentAddr.toString(16)}. Draining buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`,
2106
+ `${err.message} at 0x${currentAddr.toString(16)}. Draining buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`,
1924
2107
  );
1925
2108
 
1926
2109
  try {
1927
- await this.drainInputBuffer(300);
2110
+ await this.drainInputBuffer(200);
1928
2111
 
1929
2112
  // Clear application buffer
1930
2113
  await this.flushSerialBuffers();
@@ -1932,7 +2115,7 @@ export class ESPLoader extends EventTarget {
1932
2115
  // Wait before retry to let hardware settle
1933
2116
  await sleep(SYNC_TIMEOUT);
1934
2117
 
1935
- // Continue to retry the same chunk (will send new read command)
2118
+ // Continue to retry the same chunk (will send NEW read command)
1936
2119
  } catch (drainErr) {
1937
2120
  this.logger.debug(`Buffer drain error: ${drainErr}`);
1938
2121
  }