tasmota-webserial-esptool 7.3.0 → 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.
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
  /**
@@ -819,6 +844,25 @@ export class ESPLoader extends EventTarget {
819
844
 
820
845
  async reconfigurePort(baud: number) {
821
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
+
822
866
  // SerialPort does not allow to be reconfigured while open so we close and re-open
823
867
  // reader.cancel() causes the Promise returned by the read() operation running on
824
868
  // the readLoop to return immediately with { value: undefined, done: true } and thus
@@ -837,6 +881,8 @@ export class ESPLoader extends EventTarget {
837
881
  } catch (e) {
838
882
  this.logger.error(`Reconfigure port error: ${e}`);
839
883
  throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
884
+ } finally {
885
+ this._isReconfiguring = false;
840
886
  }
841
887
  }
842
888
 
@@ -1583,18 +1629,98 @@ export class ESPLoader extends EventTarget {
1583
1629
  return espStubLoader;
1584
1630
  }
1585
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
+
1586
1661
  async writeToStream(data: number[]) {
1587
1662
  if (!this.port.writable) {
1588
1663
  this.logger.debug("Port writable stream not available, skipping write");
1589
1664
  return;
1590
1665
  }
1591
- const writer = this.port.writable.getWriter();
1592
- await writer.write(new Uint8Array(data));
1593
- try {
1594
- writer.releaseLock();
1595
- } catch (err) {
1596
- this.logger.error(`Ignoring release lock error: ${err}`);
1666
+
1667
+ if (this._isReconfiguring) {
1668
+ throw new Error("Cannot write during port reconfiguration");
1597
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;
1598
1724
  }
1599
1725
 
1600
1726
  async disconnect() {
@@ -1606,15 +1732,49 @@ export class ESPLoader extends EventTarget {
1606
1732
  this.logger.debug("Port already closed, skipping disconnect");
1607
1733
  return;
1608
1734
  }
1609
- await this.port.writable.getWriter().close();
1610
- await new Promise((resolve) => {
1611
- if (!this._reader) {
1612
- 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}`);
1613
1744
  }
1614
- this.addEventListener("disconnect", resolve, { once: true });
1615
- this._reader!.cancel();
1616
- });
1617
- 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
+ }
1618
1778
  }
1619
1779
 
1620
1780
  /**
@@ -1627,99 +1787,122 @@ export class ESPLoader extends EventTarget {
1627
1787
  return;
1628
1788
  }
1629
1789
 
1630
- this.logger.log("Reconnecting serial port...");
1790
+ try {
1791
+ this._isReconfiguring = true;
1631
1792
 
1632
- this.connected = false;
1633
- this.__inputBuffer = [];
1793
+ this.logger.log("Reconnecting serial port...");
1634
1794
 
1635
- // Cancel reader
1636
- if (this._reader) {
1795
+ this.connected = false;
1796
+ this.__inputBuffer = [];
1797
+
1798
+ // Wait for pending writes to complete
1637
1799
  try {
1638
- await this._reader.cancel();
1800
+ await this._writeChain;
1639
1801
  } catch (err) {
1640
- this.logger.debug(`Reader cancel error: ${err}`);
1802
+ this.logger.debug(`Pending write error during reconnect: ${err}`);
1641
1803
  }
1642
- this._reader = undefined;
1643
- }
1644
1804
 
1645
- // Close port
1646
- try {
1647
- await this.port.close();
1648
- this.logger.log("Port closed");
1649
- } catch (err) {
1650
- this.logger.debug(`Port close error: ${err}`);
1651
- }
1652
-
1653
- // Open the port
1654
- this.logger.debug("Opening port...");
1655
- try {
1656
- await this.port.open({ baudRate: ESP_ROM_BAUD });
1657
- this.connected = true;
1658
- } catch (err) {
1659
- throw new Error(`Failed to open port: ${err}`);
1660
- }
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
+ }
1661
1814
 
1662
- // Verify port streams are available
1663
- if (!this.port.readable || !this.port.writable) {
1664
- throw new Error(
1665
- `Port streams not available after open (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`,
1666
- );
1667
- }
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
+ }
1668
1824
 
1669
- // Save chip info and flash size (no need to detect again)
1670
- const savedChipFamily = this.chipFamily;
1671
- const savedChipName = this.chipName;
1672
- const savedChipRevision = this.chipRevision;
1673
- const savedChipVariant = this.chipVariant;
1674
- 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
+ }
1675
1832
 
1676
- // Reinitialize
1677
- 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
+ }
1678
1841
 
1679
- if (!this._parent) {
1680
- this.__inputBuffer = [];
1681
- this.__totalBytesRead = 0;
1682
- this.readLoop();
1683
- }
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
+ }
1684
1848
 
1685
- await this.flushSerialBuffers();
1686
- 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;
1687
1855
 
1688
- // Restore chip info
1689
- this.chipFamily = savedChipFamily;
1690
- this.chipName = savedChipName;
1691
- this.chipRevision = savedChipRevision;
1692
- this.chipVariant = savedChipVariant;
1693
- this.flashSize = savedFlashSize;
1856
+ // Reinitialize
1857
+ await this.hardReset(true);
1694
1858
 
1695
- this.logger.debug(`Reconnect complete (chip: ${this.chipName})`);
1859
+ if (!this._parent) {
1860
+ this.__inputBuffer = [];
1861
+ this.__totalBytesRead = 0;
1862
+ this.readLoop();
1863
+ }
1696
1864
 
1697
- // Verify port is ready
1698
- if (!this.port.writable || !this.port.readable) {
1699
- throw new Error("Port not ready after reconnect");
1700
- }
1865
+ await this.flushSerialBuffers();
1866
+ await this.sync();
1701
1867
 
1702
- // Load stub
1703
- const stubLoader = await this.runStub(true);
1704
- 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;
1705
1874
 
1706
- // Restore baudrate if it was changed
1707
- if (this._currentBaudRate !== ESP_ROM_BAUD) {
1708
- await stubLoader.setBaudrate(this._currentBaudRate);
1875
+ this.logger.debug(`Reconnect complete (chip: ${this.chipName})`);
1709
1876
 
1710
- // Verify port is still ready after baudrate change
1877
+ // Verify port is ready
1711
1878
  if (!this.port.writable || !this.port.readable) {
1712
- throw new Error(
1713
- `Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`,
1714
- );
1879
+ throw new Error("Port not ready after reconnect");
1715
1880
  }
1716
- }
1717
1881
 
1718
- // Copy stub state to this instance if we're a stub loader
1719
- if (this.IS_STUB) {
1720
- 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;
1721
1905
  }
1722
- this.logger.debug("Reconnection successful");
1723
1906
  }
1724
1907
 
1725
1908
  /**