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.
- package/dist/esp_loader.d.ts +13 -0
- package/dist/esp_loader.js +342 -176
- package/dist/stubs/esp32.json +3 -3
- package/dist/stubs/esp32c2.json +3 -3
- package/dist/stubs/esp32c3.json +4 -4
- package/dist/stubs/esp32c5.json +4 -4
- package/dist/stubs/esp32c6.json +4 -4
- package/dist/stubs/esp32c61.json +4 -4
- package/dist/stubs/esp32h2.json +4 -4
- package/dist/stubs/esp32p4.json +4 -4
- package/dist/stubs/esp32p4r3.json +4 -4
- package/dist/stubs/esp32s2.json +3 -3
- package/dist/stubs/esp32s3.json +4 -4
- package/dist/web/esp32-BRKoi17y.js +1 -0
- package/dist/web/esp32c2-Btgr_lwh.js +1 -0
- package/dist/web/esp32c3-BGQu6Tl5.js +1 -0
- package/dist/web/esp32c5-0b050IXn.js +1 -0
- package/dist/web/esp32c6-D9SxtU9b.js +1 -0
- package/dist/web/esp32c61-B2dSOrao.js +1 -0
- package/dist/web/esp32h2-BBdaXb2C.js +1 -0
- package/dist/web/esp32p4-BLGlFHot.js +1 -0
- package/dist/web/esp32p4r3-CEI3EOJv.js +1 -0
- package/dist/web/esp32s2-iX3WoDbg.js +1 -0
- package/dist/web/esp32s3-BUw3lf0r.js +1 -0
- package/dist/web/index.js +1 -1
- package/js/modules/esp32-BRKoi17y.js +1 -0
- package/js/modules/esp32c2-Btgr_lwh.js +1 -0
- package/js/modules/esp32c3-BGQu6Tl5.js +1 -0
- package/js/modules/esp32c5-0b050IXn.js +1 -0
- package/js/modules/esp32c6-D9SxtU9b.js +1 -0
- package/js/modules/esp32c61-B2dSOrao.js +1 -0
- package/js/modules/esp32h2-BBdaXb2C.js +1 -0
- package/js/modules/esp32p4-BLGlFHot.js +1 -0
- package/js/modules/esp32p4r3-CEI3EOJv.js +1 -0
- package/js/modules/esp32s2-iX3WoDbg.js +1 -0
- package/js/modules/esp32s3-BUw3lf0r.js +1 -0
- package/js/modules/esptool.js +1 -1
- package/package.json +1 -1
- package/src/esp_loader.ts +370 -187
- package/src/stubs/esp32.json +3 -3
- package/src/stubs/esp32c2.json +3 -3
- package/src/stubs/esp32c3.json +4 -4
- package/src/stubs/esp32c5.json +4 -4
- package/src/stubs/esp32c6.json +4 -4
- package/src/stubs/esp32c61.json +4 -4
- package/src/stubs/esp32h2.json +4 -4
- package/src/stubs/esp32p4.json +4 -4
- package/src/stubs/esp32p4r3.json +4 -4
- package/src/stubs/esp32s2.json +3 -3
- package/src/stubs/esp32s3.json +4 -4
- package/dist/web/esp32-CijhsJH1.js +0 -1
- package/dist/web/esp32c2-C17SM4gO.js +0 -1
- package/dist/web/esp32c3-DxRGijbg.js +0 -1
- package/dist/web/esp32c5-3mDOIGa4.js +0 -1
- package/dist/web/esp32c6-h6U0SQTm.js +0 -1
- package/dist/web/esp32c61-BKtexhPZ.js +0 -1
- package/dist/web/esp32h2-RtuWSEmP.js +0 -1
- package/dist/web/esp32p4-5nkIjxqJ.js +0 -1
- package/dist/web/esp32p4r3-CpHBYEwI.js +0 -1
- package/dist/web/esp32s2-IiDBtXxo.js +0 -1
- package/dist/web/esp32s3-6yv5yxum.js +0 -1
- package/js/modules/esp32-CijhsJH1.js +0 -1
- package/js/modules/esp32c2-C17SM4gO.js +0 -1
- package/js/modules/esp32c3-DxRGijbg.js +0 -1
- package/js/modules/esp32c5-3mDOIGa4.js +0 -1
- package/js/modules/esp32c6-h6U0SQTm.js +0 -1
- package/js/modules/esp32c61-BKtexhPZ.js +0 -1
- package/js/modules/esp32h2-RtuWSEmP.js +0 -1
- package/js/modules/esp32p4-5nkIjxqJ.js +0 -1
- package/js/modules/esp32p4r3-CpHBYEwI.js +0 -1
- package/js/modules/esp32s2-IiDBtXxo.js +0 -1
- 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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
571
|
-
statusLen =
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
|
596
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
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
|
-
|
|
677
|
+
|
|
678
|
+
const startTime = Date.now();
|
|
679
|
+
|
|
653
680
|
while (true) {
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
);
|
|
673
|
-
|
|
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
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
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
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
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
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
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
|
-
|
|
1790
|
+
try {
|
|
1791
|
+
this._isReconfiguring = true;
|
|
1792
|
+
|
|
1793
|
+
this.logger.log("Reconnecting serial port...");
|
|
1637
1794
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1795
|
+
this.connected = false;
|
|
1796
|
+
this.__inputBuffer = [];
|
|
1640
1797
|
|
|
1641
|
-
|
|
1642
|
-
if (this._reader) {
|
|
1798
|
+
// Wait for pending writes to complete
|
|
1643
1799
|
try {
|
|
1644
|
-
await this.
|
|
1800
|
+
await this._writeChain;
|
|
1645
1801
|
} catch (err) {
|
|
1646
|
-
this.logger.debug(`
|
|
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
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
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
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
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
|
-
|
|
1683
|
-
|
|
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
|
-
|
|
1686
|
-
this.
|
|
1687
|
-
|
|
1688
|
-
|
|
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
|
-
|
|
1692
|
-
|
|
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
|
-
|
|
1695
|
-
|
|
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
|
-
|
|
1859
|
+
if (!this._parent) {
|
|
1860
|
+
this.__inputBuffer = [];
|
|
1861
|
+
this.__totalBytesRead = 0;
|
|
1862
|
+
this.readLoop();
|
|
1863
|
+
}
|
|
1702
1864
|
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
throw new Error("Port not ready after reconnect");
|
|
1706
|
-
}
|
|
1865
|
+
await this.flushSerialBuffers();
|
|
1866
|
+
await this.sync();
|
|
1707
1867
|
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
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 =
|
|
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
|
-
|
|
1848
|
-
|
|
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
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
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
|
|
1882
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
}
|