tasmota-webserial-esptool 9.2.8 → 9.2.10
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/const.d.ts +89 -6
- package/dist/const.js +116 -15
- package/dist/esp_loader.d.ts +136 -5
- package/dist/esp_loader.js +780 -148
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/util.d.ts +4 -0
- package/dist/util.js +8 -0
- package/dist/web/index.js +1 -1
- package/js/modules/esptool.js +1 -1
- package/js/script.js +207 -180
- package/js/webusb-serial.js +21 -1
- package/package.json +3 -3
- package/src/const.ts +152 -19
- package/src/esp_loader.ts +930 -172
- package/src/index.ts +3 -0
- package/src/util.ts +9 -0
package/src/esp_loader.ts
CHANGED
|
@@ -76,6 +76,39 @@ import {
|
|
|
76
76
|
ESP32S3_GPIO_STRAP_SPI_BOOT_MASK,
|
|
77
77
|
ESP32S3_RTC_CNTL_OPTION1_REG,
|
|
78
78
|
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
|
+
ESP32C3_EFUSE_RD_MAC_SPI_SYS_3_REG,
|
|
87
|
+
ESP32C3_EFUSE_RD_MAC_SPI_SYS_5_REG,
|
|
88
|
+
ESP32C3_RTC_CNTL_WDTWPROTECT_REG,
|
|
89
|
+
ESP32C3_RTC_CNTL_WDTCONFIG0_REG,
|
|
90
|
+
ESP32C3_RTC_CNTL_WDTCONFIG1_REG,
|
|
91
|
+
ESP32C3_RTC_CNTL_WDT_WKEY,
|
|
92
|
+
ESP32C5_C6_RTC_CNTL_WDTWPROTECT_REG,
|
|
93
|
+
ESP32C5_C6_RTC_CNTL_WDTCONFIG0_REG,
|
|
94
|
+
ESP32C5_C6_RTC_CNTL_WDTCONFIG1_REG,
|
|
95
|
+
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
|
+
ESP32P4_RTC_CNTL_WDTWPROTECT_REG,
|
|
101
|
+
ESP32P4_RTC_CNTL_WDTCONFIG0_REG,
|
|
102
|
+
ESP32P4_RTC_CNTL_WDTCONFIG1_REG,
|
|
103
|
+
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
|
+
ESP32P4_RTC_CNTL_OPTION1_REG,
|
|
109
|
+
ESP32P4_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
|
|
110
|
+
ESP32H2_UARTDEV_BUF_NO,
|
|
111
|
+
ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
|
|
79
112
|
} from "./const";
|
|
80
113
|
import { getStubCode } from "./stubs";
|
|
81
114
|
import { hexFormatter, sleep, slipEncode, toHex } from "./util";
|
|
@@ -108,14 +141,29 @@ export class ESPLoader extends EventTarget {
|
|
|
108
141
|
__inputBuffer?: number[];
|
|
109
142
|
__inputBufferReadIndex?: number;
|
|
110
143
|
__totalBytesRead?: number;
|
|
111
|
-
|
|
144
|
+
public currentBaudRate: number = ESP_ROM_BAUD;
|
|
112
145
|
private _maxUSBSerialBaudrate?: number;
|
|
113
146
|
public __reader?: ReadableStreamDefaultReader<Uint8Array>;
|
|
147
|
+
private SLIP_END = 0xc0;
|
|
148
|
+
private SLIP_ESC = 0xdb;
|
|
149
|
+
private SLIP_ESC_END = 0xdc;
|
|
150
|
+
private SLIP_ESC_ESC = 0xdd;
|
|
114
151
|
private _isESP32S2NativeUSB: boolean = false;
|
|
115
152
|
private _initializationSucceeded: boolean = false;
|
|
116
153
|
private __commandLock: Promise<[number, number[]]> = Promise.resolve([0, []]);
|
|
117
154
|
private __isReconfiguring: boolean = false;
|
|
118
155
|
private __abandonCurrentOperation: boolean = false;
|
|
156
|
+
private _suppressDisconnect: boolean = false;
|
|
157
|
+
private __consoleMode: boolean = false;
|
|
158
|
+
public _isUsbJtagOrOtg: boolean | undefined = undefined;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if device is using USB-JTAG or USB-OTG (not external serial chip)
|
|
162
|
+
* Returns undefined if not yet determined
|
|
163
|
+
*/
|
|
164
|
+
public get isUsbJtagOrOtg(): boolean | undefined {
|
|
165
|
+
return this._parent ? this._parent._isUsbJtagOrOtg : this._isUsbJtagOrOtg;
|
|
166
|
+
}
|
|
119
167
|
|
|
120
168
|
// Adaptive speed adjustment for flash read operations
|
|
121
169
|
private __adaptiveBlockMultiplier: number = 1;
|
|
@@ -182,6 +230,24 @@ export class ESPLoader extends EventTarget {
|
|
|
182
230
|
}
|
|
183
231
|
}
|
|
184
232
|
|
|
233
|
+
// Console mode with parent delegation
|
|
234
|
+
private get _consoleMode(): boolean {
|
|
235
|
+
return this._parent ? this._parent._consoleMode : this.__consoleMode;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private set _consoleMode(value: boolean) {
|
|
239
|
+
if (this._parent) {
|
|
240
|
+
this._parent._consoleMode = value;
|
|
241
|
+
} else {
|
|
242
|
+
this.__consoleMode = value;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Public setter for console mode (used by script.js)
|
|
247
|
+
public setConsoleMode(value: boolean): void {
|
|
248
|
+
this._consoleMode = value;
|
|
249
|
+
}
|
|
250
|
+
|
|
185
251
|
private get _inputBuffer(): number[] {
|
|
186
252
|
if (this._parent) {
|
|
187
253
|
return this._parent._inputBuffer;
|
|
@@ -457,6 +523,18 @@ export class ESPLoader extends EventTarget {
|
|
|
457
523
|
// Detect chip type
|
|
458
524
|
await this.detectChip();
|
|
459
525
|
|
|
526
|
+
// Detect if device is using USB-JTAG/Serial or USB-OTG (not external serial chip)
|
|
527
|
+
// This is needed to determine the correct reset strategy for console mode
|
|
528
|
+
try {
|
|
529
|
+
this._isUsbJtagOrOtg = await this.detectUsbConnectionType();
|
|
530
|
+
this.logger.debug(
|
|
531
|
+
`USB connection type: ${this._isUsbJtagOrOtg ? "USB-JTAG/OTG" : "External Serial Chip"}`,
|
|
532
|
+
);
|
|
533
|
+
} catch (err) {
|
|
534
|
+
this.logger.debug(`Could not detect USB connection type: ${err}`);
|
|
535
|
+
// Leave as undefined if detection fails
|
|
536
|
+
}
|
|
537
|
+
|
|
460
538
|
// Read the OTP data for this chip and store into this.efuses array
|
|
461
539
|
const FlAddr = getSpiFlashAddresses(this.getChipFamily());
|
|
462
540
|
const AddrMAC = FlAddr.macFuse;
|
|
@@ -486,7 +564,7 @@ export class ESPLoader extends EventTarget {
|
|
|
486
564
|
this.chipName = chipInfo.name;
|
|
487
565
|
this.chipFamily = chipInfo.family;
|
|
488
566
|
|
|
489
|
-
// Get chip revision for ESP32-P4
|
|
567
|
+
// Get chip revision for ESP32-P4 and ESP32-C3
|
|
490
568
|
if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
491
569
|
this.chipRevision = await this.getChipRevision();
|
|
492
570
|
this.logger.debug(`ESP32-P4 revision: ${this.chipRevision}`);
|
|
@@ -498,6 +576,9 @@ export class ESPLoader extends EventTarget {
|
|
|
498
576
|
this.chipVariant = "rev0";
|
|
499
577
|
}
|
|
500
578
|
this.logger.debug(`ESP32-P4 variant: ${this.chipVariant}`);
|
|
579
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
|
|
580
|
+
this.chipRevision = await this.getChipRevision();
|
|
581
|
+
this.logger.debug(`ESP32-C3 revision: ${this.chipRevision}`);
|
|
501
582
|
}
|
|
502
583
|
|
|
503
584
|
this.logger.debug(
|
|
@@ -549,7 +630,6 @@ export class ESPLoader extends EventTarget {
|
|
|
549
630
|
|
|
550
631
|
if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
551
632
|
this.chipRevision = await this.getChipRevision();
|
|
552
|
-
this.logger.debug(`ESP32-P4 revision: ${this.chipRevision}`);
|
|
553
633
|
|
|
554
634
|
if (this.chipRevision >= 300) {
|
|
555
635
|
this.chipVariant = "rev300";
|
|
@@ -557,6 +637,8 @@ export class ESPLoader extends EventTarget {
|
|
|
557
637
|
this.chipVariant = "rev0";
|
|
558
638
|
}
|
|
559
639
|
this.logger.debug(`ESP32-P4 variant: ${this.chipVariant}`);
|
|
640
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
|
|
641
|
+
this.chipRevision = await this.getChipRevision();
|
|
560
642
|
}
|
|
561
643
|
|
|
562
644
|
this.logger.debug(
|
|
@@ -568,22 +650,24 @@ export class ESPLoader extends EventTarget {
|
|
|
568
650
|
* Get chip revision for ESP32-P4
|
|
569
651
|
*/
|
|
570
652
|
async getChipRevision(): Promise<number> {
|
|
571
|
-
if (this.chipFamily
|
|
572
|
-
|
|
573
|
-
|
|
653
|
+
if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
654
|
+
// Read from EFUSE_BLOCK1 to get chip revision
|
|
655
|
+
// Word 2 contains revision info for ESP32-P4
|
|
656
|
+
const word2 = await this.readRegister(ESP32P4_EFUSE_BLOCK1_ADDR + 8);
|
|
574
657
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const word2 = await this.readRegister(ESP32P4_EFUSE_BLOCK1_ADDR + 8);
|
|
658
|
+
// Minor revision: bits [3:0]
|
|
659
|
+
const minorRev = word2 & 0x0f;
|
|
578
660
|
|
|
579
|
-
|
|
580
|
-
|
|
661
|
+
// Major revision: bits [23] << 2 | bits [5:4]
|
|
662
|
+
const majorRev = (((word2 >> 23) & 1) << 2) | ((word2 >> 4) & 0x03);
|
|
581
663
|
|
|
582
|
-
|
|
583
|
-
|
|
664
|
+
// Revision is major * 100 + minor
|
|
665
|
+
return majorRev * 100 + minorRev;
|
|
666
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
|
|
667
|
+
return await this.getChipRevisionC3();
|
|
668
|
+
}
|
|
584
669
|
|
|
585
|
-
|
|
586
|
-
return majorRev * 100 + minorRev;
|
|
670
|
+
return 0;
|
|
587
671
|
}
|
|
588
672
|
|
|
589
673
|
/**
|
|
@@ -677,19 +761,30 @@ export class ESPLoader extends EventTarget {
|
|
|
677
761
|
|
|
678
762
|
// Always read from browser's serial buffer immediately
|
|
679
763
|
// to prevent browser buffer overflow. Don't apply back-pressure here.
|
|
680
|
-
const chunk = Array.from(value);
|
|
764
|
+
const chunk = Array.from(value as Uint8Array);
|
|
681
765
|
Array.prototype.push.apply(this._inputBuffer, chunk);
|
|
682
766
|
|
|
683
767
|
// Track total bytes read from serial port
|
|
684
768
|
this._totalBytesRead += value.length;
|
|
685
769
|
}
|
|
686
770
|
} catch {
|
|
687
|
-
this.logger.error("Read loop got disconnected");
|
|
771
|
+
// this.logger.error("Read loop got disconnected");
|
|
688
772
|
} finally {
|
|
689
773
|
// Always reset reconfiguring flag when read loop ends
|
|
690
774
|
// This prevents "Cannot write during port reconfiguration" errors
|
|
691
775
|
// when the read loop dies unexpectedly
|
|
692
776
|
this._isReconfiguring = false;
|
|
777
|
+
|
|
778
|
+
// Release reader if still locked
|
|
779
|
+
if (this._reader) {
|
|
780
|
+
try {
|
|
781
|
+
this._reader.releaseLock();
|
|
782
|
+
this.logger.debug("Reader released in readLoop cleanup");
|
|
783
|
+
} catch (err) {
|
|
784
|
+
this.logger.debug(`Reader release error in readLoop: ${err}`);
|
|
785
|
+
}
|
|
786
|
+
this._reader = undefined;
|
|
787
|
+
}
|
|
693
788
|
}
|
|
694
789
|
|
|
695
790
|
// Disconnected!
|
|
@@ -708,7 +803,11 @@ export class ESPLoader extends EventTarget {
|
|
|
708
803
|
);
|
|
709
804
|
}
|
|
710
805
|
|
|
711
|
-
|
|
806
|
+
// Only dispatch disconnect event if not suppressed
|
|
807
|
+
if (!this._suppressDisconnect) {
|
|
808
|
+
this.dispatchEvent(new Event("disconnect"));
|
|
809
|
+
}
|
|
810
|
+
this._suppressDisconnect = false;
|
|
712
811
|
this.logger.debug("Finished read loop");
|
|
713
812
|
}
|
|
714
813
|
|
|
@@ -786,6 +885,34 @@ export class ESPLoader extends EventTarget {
|
|
|
786
885
|
await this.sleep(200);
|
|
787
886
|
}
|
|
788
887
|
|
|
888
|
+
/**
|
|
889
|
+
* Reset to firmware mode (not bootloader) for Web Serial
|
|
890
|
+
* Keeps IO0=HIGH during reset so chip boots into firmware
|
|
891
|
+
*/
|
|
892
|
+
async hardResetToFirmware() {
|
|
893
|
+
await this.setDTR(false); // IO0=HIGH
|
|
894
|
+
await this.setRTS(true); // EN=LOW, chip in reset
|
|
895
|
+
await this.sleep(100);
|
|
896
|
+
await this.setRTS(false); // EN=HIGH, chip out of reset (IO0 stays HIGH)
|
|
897
|
+
await this.sleep(50);
|
|
898
|
+
|
|
899
|
+
await this.sleep(200);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Reset to firmware mode (not bootloader) for WebUSB
|
|
904
|
+
* Keeps IO0=HIGH during reset so chip boots into firmware
|
|
905
|
+
*/
|
|
906
|
+
async hardResetToFirmwareWebUSB() {
|
|
907
|
+
await this.setDTRWebUSB(false); // IO0=HIGH
|
|
908
|
+
await this.setRTSWebUSB(true); // EN=LOW, chip in reset
|
|
909
|
+
await this.sleep(100);
|
|
910
|
+
await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset (IO0 stays HIGH)
|
|
911
|
+
await this.sleep(50);
|
|
912
|
+
|
|
913
|
+
await this.sleep(200);
|
|
914
|
+
}
|
|
915
|
+
|
|
789
916
|
/**
|
|
790
917
|
* @name hardResetUnixTight
|
|
791
918
|
* Unix Tight reset for Web Serial (Desktop) - sets DTR and RTS simultaneously
|
|
@@ -1284,7 +1411,9 @@ export class ESPLoader extends EventTarget {
|
|
|
1284
1411
|
try {
|
|
1285
1412
|
// Check if port is still open, if not, skip this strategy
|
|
1286
1413
|
if (!this.connected || !this.port.writable) {
|
|
1287
|
-
this.logger.
|
|
1414
|
+
this.logger.debug(
|
|
1415
|
+
`Port disconnected, skipping ${strategy.name} reset`,
|
|
1416
|
+
);
|
|
1288
1417
|
continue;
|
|
1289
1418
|
}
|
|
1290
1419
|
|
|
@@ -1334,9 +1463,9 @@ export class ESPLoader extends EventTarget {
|
|
|
1334
1463
|
}
|
|
1335
1464
|
} catch (error) {
|
|
1336
1465
|
lastError = error as Error;
|
|
1337
|
-
this.logger.
|
|
1338
|
-
`${strategy.name} reset failed: ${(error as Error).message}`,
|
|
1339
|
-
);
|
|
1466
|
+
// this.logger.debug(
|
|
1467
|
+
// `${strategy.name} reset failed: ${(error as Error).message}`,
|
|
1468
|
+
// );
|
|
1340
1469
|
|
|
1341
1470
|
// Set abandon flag to stop any in-flight operations
|
|
1342
1471
|
this._abandonCurrentOperation = true;
|
|
@@ -1367,13 +1496,140 @@ export class ESPLoader extends EventTarget {
|
|
|
1367
1496
|
|
|
1368
1497
|
/**
|
|
1369
1498
|
* @name watchdogReset
|
|
1370
|
-
* Watchdog reset for ESP32-S2/S3 with USB-OTG
|
|
1499
|
+
* Watchdog reset for ESP32-S2/S3/C3 with USB-OTG or USB-JTAG/Serial
|
|
1371
1500
|
* Uses RTC watchdog timer to reset the chip - works when DTR/RTS signals are not available
|
|
1501
|
+
* This is an alias for rtcWdtResetChipSpecific() for backwards compatibility
|
|
1372
1502
|
*/
|
|
1373
1503
|
async watchdogReset() {
|
|
1374
|
-
this.
|
|
1504
|
+
await this.rtcWdtResetChipSpecific();
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* Check if current chip is using USB-OTG
|
|
1509
|
+
* Supports ESP32-S2 and ESP32-S3
|
|
1510
|
+
*/
|
|
1511
|
+
public async usingUsbOtg(): Promise<boolean> {
|
|
1512
|
+
let uartDevBufNo: number;
|
|
1513
|
+
let usbOtgValue: number;
|
|
1514
|
+
|
|
1515
|
+
if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
|
|
1516
|
+
uartDevBufNo = ESP32S2_UARTDEV_BUF_NO;
|
|
1517
|
+
usbOtgValue = ESP32S2_UARTDEV_BUF_NO_USB_OTG;
|
|
1518
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
|
|
1519
|
+
uartDevBufNo = ESP32S3_UARTDEV_BUF_NO;
|
|
1520
|
+
usbOtgValue = ESP32S3_UARTDEV_BUF_NO_USB_OTG;
|
|
1521
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
1522
|
+
// P4: UARTDEV_BUF_NO depends on chip revision
|
|
1523
|
+
if (this.chipRevision === null) {
|
|
1524
|
+
this.chipRevision = await this.getChipRevision();
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
if (this.chipRevision < 300) {
|
|
1528
|
+
uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV0;
|
|
1529
|
+
} else {
|
|
1530
|
+
uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV300;
|
|
1531
|
+
}
|
|
1532
|
+
usbOtgValue = ESP32P4_UARTDEV_BUF_NO_USB_OTG;
|
|
1533
|
+
} else {
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const uartNo = (await this.readRegister(uartDevBufNo)) & 0xff;
|
|
1538
|
+
return uartNo === usbOtgValue;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* Check if current chip is using USB-JTAG/Serial
|
|
1543
|
+
* Supports ESP32-S3 and ESP32-C3
|
|
1544
|
+
*/
|
|
1545
|
+
public async usingUsbJtagSerial(): Promise<boolean> {
|
|
1546
|
+
let uartDevBufNo: number;
|
|
1547
|
+
let usbJtagSerialValue: number;
|
|
1548
|
+
|
|
1549
|
+
if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
|
|
1550
|
+
uartDevBufNo = ESP32S3_UARTDEV_BUF_NO;
|
|
1551
|
+
usbJtagSerialValue = ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
|
|
1552
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
|
|
1553
|
+
// ESP32-C3: BSS_UART_DEV_ADDR depends on chip revision
|
|
1554
|
+
// Revision < 101: 0x3FCDF064
|
|
1555
|
+
// Revision >= 101: 0x3FCDF060
|
|
1556
|
+
let bssUartDevAddr: number;
|
|
1557
|
+
|
|
1558
|
+
// Get chip revision if not already set
|
|
1559
|
+
if (this.chipRevision === null) {
|
|
1560
|
+
this.chipRevision = await this.getChipRevisionC3();
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
if (this.chipRevision < 101) {
|
|
1564
|
+
bssUartDevAddr = 0x3fcdf064;
|
|
1565
|
+
} else {
|
|
1566
|
+
bssUartDevAddr = 0x3fcdf060;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
uartDevBufNo = bssUartDevAddr + ESP32C3_BUF_UART_NO_OFFSET;
|
|
1570
|
+
usbJtagSerialValue = ESP32C3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
|
|
1571
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32C5) {
|
|
1572
|
+
uartDevBufNo = ESP32C5_UARTDEV_BUF_NO;
|
|
1573
|
+
usbJtagSerialValue = ESP32C5_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
|
|
1574
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32C6) {
|
|
1575
|
+
uartDevBufNo = ESP32C6_UARTDEV_BUF_NO;
|
|
1576
|
+
usbJtagSerialValue = ESP32C6_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
|
|
1577
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
1578
|
+
// P4: UARTDEV_BUF_NO depends on chip revision
|
|
1579
|
+
// Revision < 300: 0x4FF3FEC8
|
|
1580
|
+
// Revision >= 300: 0x4FFBFEC8
|
|
1581
|
+
if (this.chipRevision === null) {
|
|
1582
|
+
this.chipRevision = await this.getChipRevision();
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
if (this.chipRevision < 300) {
|
|
1586
|
+
uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV0;
|
|
1587
|
+
} else {
|
|
1588
|
+
uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV300;
|
|
1589
|
+
}
|
|
1590
|
+
usbJtagSerialValue = ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
|
|
1591
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32H2) {
|
|
1592
|
+
uartDevBufNo = ESP32H2_UARTDEV_BUF_NO;
|
|
1593
|
+
usbJtagSerialValue = ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
|
|
1594
|
+
} else {
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const uartNo = (await this.readRegister(uartDevBufNo)) & 0xff;
|
|
1599
|
+
return uartNo === usbJtagSerialValue;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
/**
|
|
1603
|
+
* Get chip revision for ESP32-C3
|
|
1604
|
+
* Reads from EFUSE registers and calculates revision
|
|
1605
|
+
*/
|
|
1606
|
+
async getChipRevisionC3(): Promise<number> {
|
|
1607
|
+
if (this.chipFamily !== CHIP_FAMILY_ESP32C3) {
|
|
1608
|
+
return 0;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// Read EFUSE_RD_MAC_SPI_SYS_3_REG (bits [20:18] = lower 3 bits of revision)
|
|
1612
|
+
const word3 = await this.readRegister(ESP32C3_EFUSE_RD_MAC_SPI_SYS_3_REG);
|
|
1613
|
+
const low = (word3 >> 18) & 0x07;
|
|
1614
|
+
|
|
1615
|
+
// Read EFUSE_RD_MAC_SPI_SYS_5_REG (bits [25:23] = upper 3 bits of revision)
|
|
1616
|
+
const word5 = await this.readRegister(ESP32C3_EFUSE_RD_MAC_SPI_SYS_5_REG);
|
|
1617
|
+
const hi = (word5 >> 23) & 0x07;
|
|
1618
|
+
|
|
1619
|
+
// Combine: upper 3 bits from word5, lower 3 bits from word3
|
|
1620
|
+
const revision = (hi << 3) | low;
|
|
1621
|
+
|
|
1622
|
+
return revision;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* RTC watchdog timer reset for ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C5, ESP32-C6, and ESP32-P4
|
|
1627
|
+
* Uses specific registers for each chip family
|
|
1628
|
+
* Note: ESP32-H2 does NOT support WDT reset
|
|
1629
|
+
*/
|
|
1630
|
+
public async rtcWdtResetChipSpecific(): Promise<void> {
|
|
1631
|
+
this.logger.debug("Hard resetting with watchdog timer...");
|
|
1375
1632
|
|
|
1376
|
-
// Select correct register addresses based on chip family
|
|
1377
1633
|
let WDTWPROTECT_REG: number;
|
|
1378
1634
|
let WDTCONFIG0_REG: number;
|
|
1379
1635
|
let WDTCONFIG1_REG: number;
|
|
@@ -1389,16 +1645,82 @@ export class ESPLoader extends EventTarget {
|
|
|
1389
1645
|
WDTCONFIG0_REG = ESP32S3_RTC_CNTL_WDTCONFIG0_REG;
|
|
1390
1646
|
WDTCONFIG1_REG = ESP32S3_RTC_CNTL_WDTCONFIG1_REG;
|
|
1391
1647
|
WDT_WKEY = ESP32S3_RTC_CNTL_WDT_WKEY;
|
|
1648
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
|
|
1649
|
+
WDTWPROTECT_REG = ESP32C3_RTC_CNTL_WDTWPROTECT_REG;
|
|
1650
|
+
WDTCONFIG0_REG = ESP32C3_RTC_CNTL_WDTCONFIG0_REG;
|
|
1651
|
+
WDTCONFIG1_REG = ESP32C3_RTC_CNTL_WDTCONFIG1_REG;
|
|
1652
|
+
WDT_WKEY = ESP32C3_RTC_CNTL_WDT_WKEY;
|
|
1653
|
+
} else if (
|
|
1654
|
+
this.chipFamily === CHIP_FAMILY_ESP32C5 ||
|
|
1655
|
+
this.chipFamily === CHIP_FAMILY_ESP32C6
|
|
1656
|
+
) {
|
|
1657
|
+
// C5 and C6 use LP_WDT (Low Power Watchdog Timer)
|
|
1658
|
+
WDTWPROTECT_REG = ESP32C5_C6_RTC_CNTL_WDTWPROTECT_REG;
|
|
1659
|
+
WDTCONFIG0_REG = ESP32C5_C6_RTC_CNTL_WDTCONFIG0_REG;
|
|
1660
|
+
WDTCONFIG1_REG = ESP32C5_C6_RTC_CNTL_WDTCONFIG1_REG;
|
|
1661
|
+
WDT_WKEY = ESP32C5_C6_RTC_CNTL_WDT_WKEY;
|
|
1662
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
1663
|
+
// P4 uses LP_WDT (Low Power Watchdog Timer)
|
|
1664
|
+
WDTWPROTECT_REG = ESP32P4_RTC_CNTL_WDTWPROTECT_REG;
|
|
1665
|
+
WDTCONFIG0_REG = ESP32P4_RTC_CNTL_WDTCONFIG0_REG;
|
|
1666
|
+
WDTCONFIG1_REG = ESP32P4_RTC_CNTL_WDTCONFIG1_REG;
|
|
1667
|
+
WDT_WKEY = ESP32P4_RTC_CNTL_WDT_WKEY;
|
|
1392
1668
|
} else {
|
|
1393
1669
|
throw new Error(
|
|
1394
|
-
`
|
|
1670
|
+
`rtcWdtResetChipSpecific() is not supported for ${this.chipFamily}`,
|
|
1395
1671
|
);
|
|
1396
1672
|
}
|
|
1397
1673
|
|
|
1398
1674
|
// Unlock watchdog registers
|
|
1399
1675
|
await this.writeRegister(WDTWPROTECT_REG, WDT_WKEY, undefined, 0);
|
|
1400
1676
|
|
|
1401
|
-
//
|
|
1677
|
+
// Clear force download boot register (if applicable) BEFORE triggering WDT reset
|
|
1678
|
+
// This ensures the chip boots into firmware mode after reset
|
|
1679
|
+
if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
|
|
1680
|
+
try {
|
|
1681
|
+
await this.writeRegister(
|
|
1682
|
+
ESP32S2_RTC_CNTL_OPTION1_REG,
|
|
1683
|
+
0,
|
|
1684
|
+
ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
|
|
1685
|
+
0,
|
|
1686
|
+
);
|
|
1687
|
+
this.logger.debug("Cleared force download boot mask");
|
|
1688
|
+
} catch (err) {
|
|
1689
|
+
this.logger.debug(
|
|
1690
|
+
`Expected error clearing force download boot mask: ${err}`,
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
|
|
1694
|
+
try {
|
|
1695
|
+
await this.writeRegister(
|
|
1696
|
+
ESP32S3_RTC_CNTL_OPTION1_REG,
|
|
1697
|
+
0,
|
|
1698
|
+
ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
|
|
1699
|
+
0,
|
|
1700
|
+
);
|
|
1701
|
+
this.logger.debug("Cleared force download boot mask");
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
this.logger.debug(
|
|
1704
|
+
`Expected error clearing force download boot mask: ${err}`,
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
1708
|
+
try {
|
|
1709
|
+
await this.writeRegister(
|
|
1710
|
+
ESP32P4_RTC_CNTL_OPTION1_REG,
|
|
1711
|
+
0,
|
|
1712
|
+
ESP32P4_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
|
|
1713
|
+
0,
|
|
1714
|
+
);
|
|
1715
|
+
this.logger.debug("Cleared force download boot mask");
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
this.logger.debug(
|
|
1718
|
+
`Expected error clearing force download boot mask: ${err}`,
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// Set WDT timeout to 2000ms (matches Python esptool)
|
|
1402
1724
|
await this.writeRegister(WDTCONFIG1_REG, 2000, undefined, 0);
|
|
1403
1725
|
|
|
1404
1726
|
// Enable WDT: bit 31 = enable, bits 28-30 = stage, bit 8 = sys reset, bits 0-2 = prescaler
|
|
@@ -1412,120 +1734,162 @@ export class ESPLoader extends EventTarget {
|
|
|
1412
1734
|
await this.sleep(500);
|
|
1413
1735
|
}
|
|
1414
1736
|
|
|
1737
|
+
/**
|
|
1738
|
+
* Helper: Check if USB-based WDT reset should be used for S2/S3
|
|
1739
|
+
* Returns true if WDT reset was performed, false otherwise
|
|
1740
|
+
*/
|
|
1741
|
+
private async tryUsbWdtReset(
|
|
1742
|
+
chipName: string,
|
|
1743
|
+
GPIO_STRAP_REG: number,
|
|
1744
|
+
GPIO_STRAP_SPI_BOOT_MASK: number,
|
|
1745
|
+
RTC_CNTL_OPTION1_REG: number,
|
|
1746
|
+
RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK: number,
|
|
1747
|
+
): Promise<boolean> {
|
|
1748
|
+
const isUsingUsbOtg = await this.usingUsbOtg();
|
|
1749
|
+
const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
|
|
1750
|
+
|
|
1751
|
+
if (isUsingUsbOtg || isUsingUsbJtagSerial) {
|
|
1752
|
+
const strapReg = await this.readRegister(GPIO_STRAP_REG);
|
|
1753
|
+
const forceDlReg = await this.readRegister(RTC_CNTL_OPTION1_REG);
|
|
1754
|
+
|
|
1755
|
+
// Only use watchdog reset if GPIO0 is low AND force download boot mode is not set
|
|
1756
|
+
if (
|
|
1757
|
+
(strapReg & GPIO_STRAP_SPI_BOOT_MASK) === 0 &&
|
|
1758
|
+
(forceDlReg & RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK) === 0
|
|
1759
|
+
) {
|
|
1760
|
+
await this.rtcWdtResetChipSpecific();
|
|
1761
|
+
this.logger.debug(
|
|
1762
|
+
`${chipName}: RTC WDT reset (USB detected, GPIO0 low)`,
|
|
1763
|
+
);
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
return false;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
/**
|
|
1771
|
+
* Chip-specific hard reset for ESP32-S2
|
|
1772
|
+
* Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
|
|
1773
|
+
*/
|
|
1774
|
+
public async hardResetS2(): Promise<void> {
|
|
1775
|
+
const isUsingUsbOtg = await this.usingUsbOtg();
|
|
1776
|
+
if (isUsingUsbOtg) {
|
|
1777
|
+
await this.rtcWdtResetChipSpecific();
|
|
1778
|
+
this.logger.debug("ESP32-S2: RTC WDT reset (USB-OTG detected)");
|
|
1779
|
+
} else {
|
|
1780
|
+
// Use standard hardware reset
|
|
1781
|
+
await this.hardResetClassic();
|
|
1782
|
+
this.logger.debug("ESP32-S2: Classic reset");
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* Chip-specific hard reset for ESP32-S3
|
|
1788
|
+
* Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
|
|
1789
|
+
*/
|
|
1790
|
+
public async hardResetS3(): Promise<void> {
|
|
1791
|
+
const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
|
|
1792
|
+
if (isUsingUsbJtagSerial) {
|
|
1793
|
+
await this.rtcWdtResetChipSpecific();
|
|
1794
|
+
this.logger.debug("ESP32-S3: RTC WDT reset (USB-JTAG/Serial detected)");
|
|
1795
|
+
} else {
|
|
1796
|
+
// Use standard hardware reset
|
|
1797
|
+
await this.hardResetClassic();
|
|
1798
|
+
this.logger.debug("ESP32-S3: Classic reset");
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/**
|
|
1803
|
+
* Chip-specific hard reset for ESP32-C3
|
|
1804
|
+
* Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
|
|
1805
|
+
*/
|
|
1806
|
+
public async hardResetC3(): Promise<void> {
|
|
1807
|
+
const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
|
|
1808
|
+
if (isUsingUsbJtagSerial) {
|
|
1809
|
+
await this.rtcWdtResetChipSpecific();
|
|
1810
|
+
this.logger.debug("ESP32-C3: RTC WDT reset (USB-JTAG/Serial detected)");
|
|
1811
|
+
} else {
|
|
1812
|
+
// Use standard hardware reset
|
|
1813
|
+
await this.hardResetClassic();
|
|
1814
|
+
this.logger.debug("ESP32-C3: Classic reset");
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1415
1818
|
async hardReset(bootloader = false) {
|
|
1819
|
+
// In console mode, only allow simple hardware reset (no bootloader entry)
|
|
1820
|
+
if (this._consoleMode) {
|
|
1821
|
+
if (bootloader) {
|
|
1822
|
+
this.logger.debug(
|
|
1823
|
+
"Skipping bootloader reset - device is in console mode",
|
|
1824
|
+
);
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
// Simple hardware reset to restart firmware (IO0=HIGH)
|
|
1828
|
+
this.logger.debug("Performing hardware reset (console mode)...");
|
|
1829
|
+
if (this.isWebUSB()) {
|
|
1830
|
+
await this.hardResetToFirmwareWebUSB();
|
|
1831
|
+
} else {
|
|
1832
|
+
await this.hardResetToFirmware();
|
|
1833
|
+
}
|
|
1834
|
+
this.logger.debug("Hardware reset complete");
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1416
1838
|
if (bootloader) {
|
|
1417
1839
|
// enter flash mode
|
|
1418
1840
|
if (this.port.getInfo().usbProductId === USB_JTAG_SERIAL_PID) {
|
|
1419
1841
|
await this.hardResetUSBJTAGSerial();
|
|
1420
|
-
this.logger.
|
|
1842
|
+
this.logger.debug("USB-JTAG/Serial reset.");
|
|
1421
1843
|
} else {
|
|
1422
1844
|
// Use different reset strategy for WebUSB (Android) vs Web Serial (Desktop)
|
|
1423
1845
|
if (this.isWebUSB()) {
|
|
1424
1846
|
await this.hardResetClassicWebUSB();
|
|
1425
|
-
this.logger.
|
|
1847
|
+
this.logger.debug("Classic reset (WebUSB/Android).");
|
|
1426
1848
|
} else {
|
|
1427
1849
|
await this.hardResetClassic();
|
|
1428
|
-
this.logger.
|
|
1850
|
+
this.logger.debug("Classic reset.");
|
|
1429
1851
|
}
|
|
1430
1852
|
}
|
|
1431
1853
|
} else {
|
|
1432
1854
|
// just reset (no bootloader mode)
|
|
1433
|
-
// For ESP32-S2/S3 with USB-OTG, check if watchdog reset is needed
|
|
1434
|
-
if (
|
|
1435
|
-
this.
|
|
1436
|
-
|
|
1437
|
-
|
|
1855
|
+
// For ESP32-S2/S3 with USB-OTG or USB-JTAG/Serial, check if watchdog reset is needed
|
|
1856
|
+
if (this.chipFamily === CHIP_FAMILY_ESP32S2 && !this._consoleMode) {
|
|
1857
|
+
const wdtResetUsed = await this.tryUsbWdtReset(
|
|
1858
|
+
"ESP32-S2",
|
|
1859
|
+
ESP32S2_GPIO_STRAP_REG,
|
|
1860
|
+
ESP32S2_GPIO_STRAP_SPI_BOOT_MASK,
|
|
1861
|
+
ESP32S2_RTC_CNTL_OPTION1_REG,
|
|
1862
|
+
ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
|
|
1863
|
+
);
|
|
1864
|
+
if (wdtResetUsed) return;
|
|
1865
|
+
} else if (
|
|
1866
|
+
this.chipFamily === CHIP_FAMILY_ESP32S3 &&
|
|
1867
|
+
!this._consoleMode
|
|
1438
1868
|
) {
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
const RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK =
|
|
1449
|
-
this.chipFamily === CHIP_FAMILY_ESP32S2
|
|
1450
|
-
? ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK
|
|
1451
|
-
: ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK;
|
|
1452
|
-
|
|
1453
|
-
await this.writeRegister(
|
|
1454
|
-
RTC_CNTL_OPTION1_REG,
|
|
1455
|
-
0,
|
|
1456
|
-
RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
|
|
1457
|
-
0,
|
|
1458
|
-
);
|
|
1459
|
-
} catch (e) {
|
|
1460
|
-
// Skip invalid response and continue reset (can happen when monitoring during reset)
|
|
1461
|
-
this.logger.log(
|
|
1462
|
-
"Warning: Could not clear force download boot mode:",
|
|
1463
|
-
e,
|
|
1464
|
-
);
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
// Check the strapping register to see if we can perform a watchdog reset
|
|
1468
|
-
// Only use watchdog reset if GPIO0 is low AND force download boot mode is not set
|
|
1469
|
-
let useWatchdogReset = false;
|
|
1470
|
-
try {
|
|
1471
|
-
const GPIO_STRAP_REG =
|
|
1472
|
-
this.chipFamily === CHIP_FAMILY_ESP32S2
|
|
1473
|
-
? ESP32S2_GPIO_STRAP_REG
|
|
1474
|
-
: ESP32S3_GPIO_STRAP_REG;
|
|
1475
|
-
const GPIO_STRAP_SPI_BOOT_MASK =
|
|
1476
|
-
this.chipFamily === CHIP_FAMILY_ESP32S2
|
|
1477
|
-
? ESP32S2_GPIO_STRAP_SPI_BOOT_MASK
|
|
1478
|
-
: ESP32S3_GPIO_STRAP_SPI_BOOT_MASK;
|
|
1479
|
-
const RTC_CNTL_OPTION1_REG =
|
|
1480
|
-
this.chipFamily === CHIP_FAMILY_ESP32S2
|
|
1481
|
-
? ESP32S2_RTC_CNTL_OPTION1_REG
|
|
1482
|
-
: ESP32S3_RTC_CNTL_OPTION1_REG;
|
|
1483
|
-
const RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK =
|
|
1484
|
-
this.chipFamily === CHIP_FAMILY_ESP32S2
|
|
1485
|
-
? ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK
|
|
1486
|
-
: ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK;
|
|
1487
|
-
|
|
1488
|
-
const strapReg = await this.readRegister(GPIO_STRAP_REG);
|
|
1489
|
-
const forceDlReg = await this.readRegister(RTC_CNTL_OPTION1_REG);
|
|
1490
|
-
|
|
1491
|
-
// GPIO0 low (download mode) AND force download boot not set
|
|
1492
|
-
if (
|
|
1493
|
-
(strapReg & GPIO_STRAP_SPI_BOOT_MASK) === 0 &&
|
|
1494
|
-
(forceDlReg & RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK) === 0
|
|
1495
|
-
) {
|
|
1496
|
-
useWatchdogReset = true;
|
|
1497
|
-
}
|
|
1498
|
-
} catch (e) {
|
|
1499
|
-
// If we can't read the registers, use watchdog reset as fallback
|
|
1500
|
-
this.logger.log(
|
|
1501
|
-
"Warning: Could not read strap/option registers, using watchdog reset:",
|
|
1502
|
-
e,
|
|
1503
|
-
);
|
|
1504
|
-
useWatchdogReset = true;
|
|
1505
|
-
}
|
|
1869
|
+
const wdtResetUsed = await this.tryUsbWdtReset(
|
|
1870
|
+
"ESP32-S3",
|
|
1871
|
+
ESP32S3_GPIO_STRAP_REG,
|
|
1872
|
+
ESP32S3_GPIO_STRAP_SPI_BOOT_MASK,
|
|
1873
|
+
ESP32S3_RTC_CNTL_OPTION1_REG,
|
|
1874
|
+
ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK,
|
|
1875
|
+
);
|
|
1876
|
+
if (wdtResetUsed) return;
|
|
1877
|
+
}
|
|
1506
1878
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
this.logger.log("Watchdog reset (USB-OTG).");
|
|
1510
|
-
} else {
|
|
1511
|
-
// Not in download mode, can use DTR/RTS reset
|
|
1512
|
-
// But USB-OTG doesn't have DTR/RTS, so fall back to watchdog
|
|
1513
|
-
await this.watchdogReset();
|
|
1514
|
-
this.logger.log("Watchdog reset (USB-OTG, normal boot).");
|
|
1515
|
-
}
|
|
1516
|
-
} else if (this.isWebUSB()) {
|
|
1879
|
+
// Standard reset for all other cases
|
|
1880
|
+
if (this.isWebUSB()) {
|
|
1517
1881
|
// WebUSB: Use longer delays for better compatibility
|
|
1518
1882
|
await this.setRTSWebUSB(true); // EN->LOW
|
|
1519
1883
|
await this.sleep(200);
|
|
1520
1884
|
await this.setRTSWebUSB(false);
|
|
1521
1885
|
await this.sleep(200);
|
|
1522
|
-
this.logger.
|
|
1886
|
+
this.logger.debug("Hard reset (WebUSB).");
|
|
1523
1887
|
} else {
|
|
1524
1888
|
// Web Serial: Standard reset
|
|
1525
1889
|
await this.setRTS(true); // EN->LOW
|
|
1526
1890
|
await this.sleep(100);
|
|
1527
1891
|
await this.setRTS(false);
|
|
1528
|
-
this.logger.
|
|
1892
|
+
this.logger.debug("Hard reset.");
|
|
1529
1893
|
}
|
|
1530
1894
|
}
|
|
1531
1895
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -1766,47 +2130,47 @@ export class ESPLoader extends EventTarget {
|
|
|
1766
2130
|
"Timed out waiting for packet " + waitingFor,
|
|
1767
2131
|
);
|
|
1768
2132
|
}
|
|
1769
|
-
const
|
|
2133
|
+
const byte = this._readByte()!;
|
|
1770
2134
|
|
|
1771
2135
|
if (partialPacket === null) {
|
|
1772
2136
|
// waiting for packet header
|
|
1773
|
-
if (
|
|
2137
|
+
if (byte == this.SLIP_END) {
|
|
1774
2138
|
partialPacket = [];
|
|
1775
2139
|
} else {
|
|
1776
2140
|
if (this.debug) {
|
|
1777
|
-
this.logger.debug("Read invalid data: " + toHex(
|
|
2141
|
+
this.logger.debug("Read invalid data: " + toHex(byte));
|
|
1778
2142
|
this.logger.debug(
|
|
1779
2143
|
"Remaining data in serial buffer: " +
|
|
1780
2144
|
hexFormatter(this._inputBuffer),
|
|
1781
2145
|
);
|
|
1782
2146
|
}
|
|
1783
2147
|
throw new SlipReadError(
|
|
1784
|
-
"Invalid head of packet (" + toHex(
|
|
2148
|
+
"Invalid head of packet (" + toHex(byte) + ")",
|
|
1785
2149
|
);
|
|
1786
2150
|
}
|
|
1787
2151
|
} else if (inEscape) {
|
|
1788
2152
|
// part-way through escape sequence
|
|
1789
2153
|
inEscape = false;
|
|
1790
|
-
if (
|
|
1791
|
-
partialPacket.push(
|
|
1792
|
-
} else if (
|
|
1793
|
-
partialPacket.push(
|
|
2154
|
+
if (byte == this.SLIP_ESC_END) {
|
|
2155
|
+
partialPacket.push(this.SLIP_END);
|
|
2156
|
+
} else if (byte == this.SLIP_ESC_ESC) {
|
|
2157
|
+
partialPacket.push(this.SLIP_ESC);
|
|
1794
2158
|
} else {
|
|
1795
2159
|
if (this.debug) {
|
|
1796
|
-
this.logger.debug("Read invalid data: " + toHex(
|
|
2160
|
+
this.logger.debug("Read invalid data: " + toHex(byte));
|
|
1797
2161
|
this.logger.debug(
|
|
1798
2162
|
"Remaining data in serial buffer: " +
|
|
1799
2163
|
hexFormatter(this._inputBuffer),
|
|
1800
2164
|
);
|
|
1801
2165
|
}
|
|
1802
2166
|
throw new SlipReadError(
|
|
1803
|
-
"Invalid SLIP escape (0xdb, " + toHex(
|
|
2167
|
+
"Invalid SLIP escape (0xdb, " + toHex(byte) + ")",
|
|
1804
2168
|
);
|
|
1805
2169
|
}
|
|
1806
|
-
} else if (
|
|
2170
|
+
} else if (byte == this.SLIP_ESC) {
|
|
1807
2171
|
// start of escape sequence
|
|
1808
2172
|
inEscape = true;
|
|
1809
|
-
} else if (
|
|
2173
|
+
} else if (byte == this.SLIP_END) {
|
|
1810
2174
|
// end of packet
|
|
1811
2175
|
if (this.debug)
|
|
1812
2176
|
this.logger.debug(
|
|
@@ -1817,7 +2181,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1817
2181
|
return partialPacket;
|
|
1818
2182
|
} else {
|
|
1819
2183
|
// normal byte in packet
|
|
1820
|
-
partialPacket.push(
|
|
2184
|
+
partialPacket.push(byte);
|
|
1821
2185
|
}
|
|
1822
2186
|
}
|
|
1823
2187
|
}
|
|
@@ -1851,46 +2215,46 @@ export class ESPLoader extends EventTarget {
|
|
|
1851
2215
|
this.logger.debug(
|
|
1852
2216
|
"Read " + readBytes.length + " bytes: " + hexFormatter(readBytes),
|
|
1853
2217
|
);
|
|
1854
|
-
for (const
|
|
2218
|
+
for (const byte of readBytes) {
|
|
1855
2219
|
if (partialPacket === null) {
|
|
1856
2220
|
// waiting for packet header
|
|
1857
|
-
if (
|
|
2221
|
+
if (byte == this.SLIP_END) {
|
|
1858
2222
|
partialPacket = [];
|
|
1859
2223
|
} else {
|
|
1860
2224
|
if (this.debug) {
|
|
1861
|
-
this.logger.debug("Read invalid data: " + toHex(
|
|
2225
|
+
this.logger.debug("Read invalid data: " + toHex(byte));
|
|
1862
2226
|
this.logger.debug(
|
|
1863
2227
|
"Remaining data in serial buffer: " +
|
|
1864
2228
|
hexFormatter(this._inputBuffer),
|
|
1865
2229
|
);
|
|
1866
2230
|
}
|
|
1867
2231
|
throw new SlipReadError(
|
|
1868
|
-
"Invalid head of packet (" + toHex(
|
|
2232
|
+
"Invalid head of packet (" + toHex(byte) + ")",
|
|
1869
2233
|
);
|
|
1870
2234
|
}
|
|
1871
2235
|
} else if (inEscape) {
|
|
1872
2236
|
// part-way through escape sequence
|
|
1873
2237
|
inEscape = false;
|
|
1874
|
-
if (
|
|
1875
|
-
partialPacket.push(
|
|
1876
|
-
} else if (
|
|
1877
|
-
partialPacket.push(
|
|
2238
|
+
if (byte == this.SLIP_ESC_END) {
|
|
2239
|
+
partialPacket.push(this.SLIP_END);
|
|
2240
|
+
} else if (byte == this.SLIP_ESC_ESC) {
|
|
2241
|
+
partialPacket.push(this.SLIP_ESC);
|
|
1878
2242
|
} else {
|
|
1879
2243
|
if (this.debug) {
|
|
1880
|
-
this.logger.debug("Read invalid data: " + toHex(
|
|
2244
|
+
this.logger.debug("Read invalid data: " + toHex(byte));
|
|
1881
2245
|
this.logger.debug(
|
|
1882
2246
|
"Remaining data in serial buffer: " +
|
|
1883
2247
|
hexFormatter(this._inputBuffer),
|
|
1884
2248
|
);
|
|
1885
2249
|
}
|
|
1886
2250
|
throw new SlipReadError(
|
|
1887
|
-
"Invalid SLIP escape (0xdb, " + toHex(
|
|
2251
|
+
"Invalid SLIP escape (0xdb, " + toHex(byte) + ")",
|
|
1888
2252
|
);
|
|
1889
2253
|
}
|
|
1890
|
-
} else if (
|
|
2254
|
+
} else if (byte == this.SLIP_ESC) {
|
|
1891
2255
|
// start of escape sequence
|
|
1892
2256
|
inEscape = true;
|
|
1893
|
-
} else if (
|
|
2257
|
+
} else if (byte == this.SLIP_END) {
|
|
1894
2258
|
// end of packet
|
|
1895
2259
|
if (this.debug)
|
|
1896
2260
|
this.logger.debug(
|
|
@@ -1901,7 +2265,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1901
2265
|
return partialPacket;
|
|
1902
2266
|
} else {
|
|
1903
2267
|
// normal byte in packet
|
|
1904
|
-
partialPacket.push(
|
|
2268
|
+
partialPacket.push(byte);
|
|
1905
2269
|
}
|
|
1906
2270
|
}
|
|
1907
2271
|
}
|
|
@@ -1978,9 +2342,9 @@ export class ESPLoader extends EventTarget {
|
|
|
1978
2342
|
|
|
1979
2343
|
// Track current baudrate for reconnect
|
|
1980
2344
|
if (this._parent) {
|
|
1981
|
-
this._parent.
|
|
2345
|
+
this._parent.currentBaudRate = baud;
|
|
1982
2346
|
} else {
|
|
1983
|
-
this.
|
|
2347
|
+
this.currentBaudRate = baud;
|
|
1984
2348
|
}
|
|
1985
2349
|
|
|
1986
2350
|
// Warn if baudrate exceeds USB-Serial chip capability
|
|
@@ -2066,8 +2430,8 @@ export class ESPLoader extends EventTarget {
|
|
|
2066
2430
|
// Restart Readloop
|
|
2067
2431
|
this.readLoop();
|
|
2068
2432
|
} catch (e) {
|
|
2069
|
-
this.logger.error(`Reconfigure port error: ${e}`);
|
|
2070
|
-
throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
|
|
2433
|
+
// this.logger.error(`Reconfigure port error: ${e}`);
|
|
2434
|
+
// throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
|
|
2071
2435
|
} finally {
|
|
2072
2436
|
// Always reset flag, even on error or early return
|
|
2073
2437
|
this._isReconfiguring = false;
|
|
@@ -2775,20 +3139,6 @@ export class ESPLoader extends EventTarget {
|
|
|
2775
3139
|
}
|
|
2776
3140
|
}
|
|
2777
3141
|
|
|
2778
|
-
private get _currentBaudRate(): number {
|
|
2779
|
-
return this._parent
|
|
2780
|
-
? this._parent._currentBaudRate
|
|
2781
|
-
: this.__currentBaudRate;
|
|
2782
|
-
}
|
|
2783
|
-
|
|
2784
|
-
private set _currentBaudRate(value: number) {
|
|
2785
|
-
if (this._parent) {
|
|
2786
|
-
this._parent._currentBaudRate = value;
|
|
2787
|
-
} else {
|
|
2788
|
-
this.__currentBaudRate = value;
|
|
2789
|
-
}
|
|
2790
|
-
}
|
|
2791
|
-
|
|
2792
3142
|
async writeToStream(data: number[]) {
|
|
2793
3143
|
if (!this.port.writable) {
|
|
2794
3144
|
this.logger.debug("Port writable stream not available, skipping write");
|
|
@@ -2868,7 +3218,7 @@ export class ESPLoader extends EventTarget {
|
|
|
2868
3218
|
return;
|
|
2869
3219
|
}
|
|
2870
3220
|
if (!this.port.writable) {
|
|
2871
|
-
this.logger.debug("Port already closed, skipping disconnect");
|
|
3221
|
+
// this.logger.debug("Port already closed, skipping disconnect");
|
|
2872
3222
|
return;
|
|
2873
3223
|
}
|
|
2874
3224
|
|
|
@@ -2876,7 +3226,7 @@ export class ESPLoader extends EventTarget {
|
|
|
2876
3226
|
try {
|
|
2877
3227
|
await this._writeChain;
|
|
2878
3228
|
} catch (err) {
|
|
2879
|
-
this.logger.debug(`Pending write error during disconnect: ${err}`);
|
|
3229
|
+
// this.logger.debug(`Pending write error during disconnect: ${err}`);
|
|
2880
3230
|
}
|
|
2881
3231
|
|
|
2882
3232
|
// Release persistent writer before closing
|
|
@@ -2885,7 +3235,7 @@ export class ESPLoader extends EventTarget {
|
|
|
2885
3235
|
await this._writer.close();
|
|
2886
3236
|
this._writer.releaseLock();
|
|
2887
3237
|
} catch (err) {
|
|
2888
|
-
this.logger.debug(`Writer close/release error: ${err}`);
|
|
3238
|
+
// this.logger.debug(`Writer close/release error: ${err}`);
|
|
2889
3239
|
}
|
|
2890
3240
|
this._writer = undefined;
|
|
2891
3241
|
} else {
|
|
@@ -2896,7 +3246,7 @@ export class ESPLoader extends EventTarget {
|
|
|
2896
3246
|
await writer.close();
|
|
2897
3247
|
writer.releaseLock();
|
|
2898
3248
|
} catch (err) {
|
|
2899
|
-
this.logger.debug(`Direct writer close error: ${err}`);
|
|
3249
|
+
// this.logger.debug(`Direct writer close error: ${err}`);
|
|
2900
3250
|
}
|
|
2901
3251
|
}
|
|
2902
3252
|
|
|
@@ -2925,7 +3275,7 @@ export class ESPLoader extends EventTarget {
|
|
|
2925
3275
|
try {
|
|
2926
3276
|
this._reader.cancel();
|
|
2927
3277
|
} catch (err) {
|
|
2928
|
-
this.logger.debug(`Reader cancel error: ${err}`);
|
|
3278
|
+
// this.logger.debug(`Reader cancel error: ${err}`);
|
|
2929
3279
|
// Reader already released, resolve immediately
|
|
2930
3280
|
clearTimeout(timeout);
|
|
2931
3281
|
resolve(undefined);
|
|
@@ -2942,6 +3292,270 @@ export class ESPLoader extends EventTarget {
|
|
|
2942
3292
|
}
|
|
2943
3293
|
}
|
|
2944
3294
|
|
|
3295
|
+
/**
|
|
3296
|
+
* @name releaseReaderWriter
|
|
3297
|
+
* Release reader and writer locks without closing the port
|
|
3298
|
+
* Used when switching to console mode
|
|
3299
|
+
*/
|
|
3300
|
+
async releaseReaderWriter() {
|
|
3301
|
+
if (this._parent) {
|
|
3302
|
+
await this._parent.releaseReaderWriter();
|
|
3303
|
+
return;
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
// Check if device is in JTAG mode and needs reset to boot into firmware
|
|
3307
|
+
const didReconnect = await this._resetToFirmwareIfNeeded();
|
|
3308
|
+
|
|
3309
|
+
// If we reconnected for console, the reader/writer are already released and restarted
|
|
3310
|
+
if (didReconnect) {
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
// Wait for pending writes to complete
|
|
3315
|
+
try {
|
|
3316
|
+
await this._writeChain;
|
|
3317
|
+
} catch (err) {
|
|
3318
|
+
// this.logger.debug(`Pending write error during release: ${err}`);
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
// Release writer
|
|
3322
|
+
if (this._writer) {
|
|
3323
|
+
try {
|
|
3324
|
+
this._writer.releaseLock();
|
|
3325
|
+
this.logger.debug("Writer released");
|
|
3326
|
+
} catch (err) {
|
|
3327
|
+
this.logger.debug(`Writer release error: ${err}`);
|
|
3328
|
+
}
|
|
3329
|
+
this._writer = undefined;
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
// Cancel and release reader
|
|
3333
|
+
if (this._reader) {
|
|
3334
|
+
const reader = this._reader;
|
|
3335
|
+
try {
|
|
3336
|
+
// Suppress disconnect event during console mode switching
|
|
3337
|
+
this._suppressDisconnect = true;
|
|
3338
|
+
await reader.cancel();
|
|
3339
|
+
this.logger.debug("Reader cancelled");
|
|
3340
|
+
} catch (err) {
|
|
3341
|
+
this.logger.debug(`Reader cancel error: ${err}`);
|
|
3342
|
+
} finally {
|
|
3343
|
+
try {
|
|
3344
|
+
reader.releaseLock();
|
|
3345
|
+
} catch (err) {
|
|
3346
|
+
this.logger.debug(`Reader release error: ${err}`);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
if (this._reader === reader) {
|
|
3350
|
+
this._reader = undefined;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
/**
|
|
3356
|
+
* @name resetToFirmware
|
|
3357
|
+
* Public method to reset device from bootloader to firmware for console mode
|
|
3358
|
+
* Automatically detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
|
|
3359
|
+
* @returns true if reset was performed, false if not needed
|
|
3360
|
+
*/
|
|
3361
|
+
public async resetToFirmware(): Promise<boolean> {
|
|
3362
|
+
return await this._resetToFirmwareIfNeeded();
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
/**
|
|
3366
|
+
* @name detectUsbConnectionType
|
|
3367
|
+
* Detect if device is using USB-JTAG/Serial or USB-OTG (not external serial chip)
|
|
3368
|
+
* This helper extracts the detection logic from initialize() for reuse
|
|
3369
|
+
* @returns true if USB-JTAG or USB-OTG, false if external serial chip
|
|
3370
|
+
* @throws Error if detection fails and chipFamily is not set
|
|
3371
|
+
*/
|
|
3372
|
+
private async detectUsbConnectionType(): Promise<boolean> {
|
|
3373
|
+
if (!this.chipFamily) {
|
|
3374
|
+
throw new Error("Cannot detect USB connection type: chipFamily not set");
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
if (
|
|
3378
|
+
this.chipFamily === CHIP_FAMILY_ESP32S2 ||
|
|
3379
|
+
this.chipFamily === CHIP_FAMILY_ESP32S3
|
|
3380
|
+
) {
|
|
3381
|
+
const isUsingUsbOtg = await this.usingUsbOtg();
|
|
3382
|
+
const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
|
|
3383
|
+
return isUsingUsbOtg || isUsingUsbJtagSerial;
|
|
3384
|
+
} else if (
|
|
3385
|
+
this.chipFamily === CHIP_FAMILY_ESP32C3 ||
|
|
3386
|
+
this.chipFamily === CHIP_FAMILY_ESP32C5 ||
|
|
3387
|
+
this.chipFamily === CHIP_FAMILY_ESP32C6
|
|
3388
|
+
) {
|
|
3389
|
+
const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
|
|
3390
|
+
return isUsingUsbJtagSerial;
|
|
3391
|
+
} else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
3392
|
+
const isUsingUsbOtg = await this.usingUsbOtg();
|
|
3393
|
+
const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
|
|
3394
|
+
return isUsingUsbOtg || isUsingUsbJtagSerial;
|
|
3395
|
+
} else {
|
|
3396
|
+
// Other chips don't have USB-JTAG/OTG
|
|
3397
|
+
return false;
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
/**
|
|
3402
|
+
* @name enterConsoleMode
|
|
3403
|
+
* Prepare device for console mode by resetting to firmware
|
|
3404
|
+
* Handles both USB-JTAG/OTG devices (closes port) and external serial chips (keeps port open)
|
|
3405
|
+
* @returns true if port was closed (USB-JTAG), false if port stays open (serial chip)
|
|
3406
|
+
*/
|
|
3407
|
+
public async enterConsoleMode(): Promise<boolean> {
|
|
3408
|
+
// Set console mode flag
|
|
3409
|
+
this._consoleMode = true;
|
|
3410
|
+
|
|
3411
|
+
// Re-detect USB connection type to ensure we have a definitive value
|
|
3412
|
+
// This handles cases where isUsbJtagOrOtg might be undefined
|
|
3413
|
+
let isUsbJtag: boolean;
|
|
3414
|
+
try {
|
|
3415
|
+
isUsbJtag = await this.detectUsbConnectionType();
|
|
3416
|
+
this.logger.debug(
|
|
3417
|
+
`USB connection type detected: ${isUsbJtag ? "USB-JTAG/OTG" : "External Serial Chip"}`,
|
|
3418
|
+
);
|
|
3419
|
+
} catch (err) {
|
|
3420
|
+
// If detection fails, fall back to cached value or fail-fast
|
|
3421
|
+
if (this.isUsbJtagOrOtg === undefined) {
|
|
3422
|
+
throw new Error(
|
|
3423
|
+
`Cannot enter console mode: USB connection type unknown and detection failed: ${err}`,
|
|
3424
|
+
);
|
|
3425
|
+
}
|
|
3426
|
+
this.logger.debug(
|
|
3427
|
+
`USB detection failed, using cached value: ${this.isUsbJtagOrOtg}`,
|
|
3428
|
+
);
|
|
3429
|
+
isUsbJtag = this.isUsbJtagOrOtg;
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
// Release reader/writer so console can create new ones
|
|
3433
|
+
// This is needed for Desktop (Web Serial) to unlock streams
|
|
3434
|
+
if (isUsbJtag) {
|
|
3435
|
+
// USB-JTAG/OTG devices: Use watchdog reset which closes port
|
|
3436
|
+
const wasReset = await this._resetToFirmwareIfNeeded();
|
|
3437
|
+
return wasReset; // true = port closed, caller must reopen
|
|
3438
|
+
} else {
|
|
3439
|
+
// External serial chip devices: Release locks and do simple reset
|
|
3440
|
+
try {
|
|
3441
|
+
await this.releaseReaderWriter();
|
|
3442
|
+
await this.sleep(100);
|
|
3443
|
+
} catch (err) {
|
|
3444
|
+
this.logger.debug(`Failed to release locks: ${err}`);
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
// Hardware reset to firmware mode (IO0=HIGH)
|
|
3448
|
+
try {
|
|
3449
|
+
await this.hardReset(false);
|
|
3450
|
+
this.logger.log("Device reset to firmware mode");
|
|
3451
|
+
} catch (err) {
|
|
3452
|
+
this.logger.debug(`Could not reset device: ${err}`);
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
// For WebUSB (Android), recreate streams after hardware reset
|
|
3456
|
+
if (this.isWebUSB()) {
|
|
3457
|
+
try {
|
|
3458
|
+
// Use the public recreateStreams() method to safely recreate streams
|
|
3459
|
+
// without closing the port (important after hardware reset)
|
|
3460
|
+
await (this.port as any).recreateStreams();
|
|
3461
|
+
this.logger.debug("WebUSB streams recreated for console mode");
|
|
3462
|
+
} catch (err) {
|
|
3463
|
+
this.logger.debug(`Failed to recreate WebUSB streams: ${err}`);
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
return false; // Port stays open
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
/**
|
|
3472
|
+
* @name _resetToFirmwareIfNeeded
|
|
3473
|
+
* Reset device from bootloader to firmware when switching to console mode
|
|
3474
|
+
* Detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
|
|
3475
|
+
* @returns true if reconnect was performed, false otherwise
|
|
3476
|
+
*/
|
|
3477
|
+
private async _resetToFirmwareIfNeeded(): Promise<boolean> {
|
|
3478
|
+
try {
|
|
3479
|
+
// Check if device is using USB-JTAG/Serial or USB-OTG
|
|
3480
|
+
// Value should already be set during main() connection
|
|
3481
|
+
// Use getter to access parent's value if this is a stub
|
|
3482
|
+
const needsReset = this.isUsbJtagOrOtg === true;
|
|
3483
|
+
|
|
3484
|
+
if (needsReset) {
|
|
3485
|
+
const resetMethod =
|
|
3486
|
+
this.chipFamily === CHIP_FAMILY_ESP32S2 ||
|
|
3487
|
+
this.chipFamily === CHIP_FAMILY_ESP32S3
|
|
3488
|
+
? "USB-JTAG/Serial or USB-OTG"
|
|
3489
|
+
: "USB-JTAG/Serial";
|
|
3490
|
+
|
|
3491
|
+
this.logger.log(
|
|
3492
|
+
`Resetting ${this.chipName || "device"} (${resetMethod}) to boot into firmware...`,
|
|
3493
|
+
);
|
|
3494
|
+
|
|
3495
|
+
// Set console mode flag before reset to prevent subsequent hardReset calls
|
|
3496
|
+
this._consoleMode = true;
|
|
3497
|
+
|
|
3498
|
+
// For S2/S3: Clear force download boot mask before WDT reset
|
|
3499
|
+
if (
|
|
3500
|
+
this.chipFamily === CHIP_FAMILY_ESP32S2 ||
|
|
3501
|
+
this.chipFamily === CHIP_FAMILY_ESP32S3
|
|
3502
|
+
) {
|
|
3503
|
+
const OPTION1_REG =
|
|
3504
|
+
this.chipFamily === CHIP_FAMILY_ESP32S2
|
|
3505
|
+
? ESP32S2_RTC_CNTL_OPTION1_REG
|
|
3506
|
+
: ESP32S3_RTC_CNTL_OPTION1_REG;
|
|
3507
|
+
const FORCE_DOWNLOAD_BOOT_MASK =
|
|
3508
|
+
this.chipFamily === CHIP_FAMILY_ESP32S2
|
|
3509
|
+
? ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK
|
|
3510
|
+
: ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK;
|
|
3511
|
+
|
|
3512
|
+
try {
|
|
3513
|
+
// Clear force download boot mode to avoid chip being stuck in download mode
|
|
3514
|
+
await this.writeRegister(
|
|
3515
|
+
OPTION1_REG,
|
|
3516
|
+
0,
|
|
3517
|
+
FORCE_DOWNLOAD_BOOT_MASK,
|
|
3518
|
+
0,
|
|
3519
|
+
);
|
|
3520
|
+
this.logger.debug("Cleared force download boot mask");
|
|
3521
|
+
} catch (err) {
|
|
3522
|
+
this.logger.debug(
|
|
3523
|
+
`Expected error clearing force download boot mask: ${err}`,
|
|
3524
|
+
);
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
// Perform watchdog reset to reboot into firmware
|
|
3529
|
+
try {
|
|
3530
|
+
await this.rtcWdtResetChipSpecific();
|
|
3531
|
+
this.logger.debug("Watchdog reset triggered successfully");
|
|
3532
|
+
} catch (err) {
|
|
3533
|
+
// Error is expected - device resets before responding
|
|
3534
|
+
this.logger.debug(
|
|
3535
|
+
`Watchdog reset initiated (connection lost as expected: ${err})`,
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
// Wait for device to fully boot into firmware
|
|
3540
|
+
this.logger.log("Waiting for device to boot into firmware...");
|
|
3541
|
+
await this.sleep(1000);
|
|
3542
|
+
|
|
3543
|
+
// After WDT reset, streams are dead/locked - don't try to manipulate them
|
|
3544
|
+
// Just mark everything as disconnected and let browser clean up
|
|
3545
|
+
this.connected = false;
|
|
3546
|
+
this._writer = undefined;
|
|
3547
|
+
this._reader = undefined;
|
|
3548
|
+
|
|
3549
|
+
this.logger.debug("Device reset to firmware mode (port closed)");
|
|
3550
|
+
return true;
|
|
3551
|
+
}
|
|
3552
|
+
} catch (err) {
|
|
3553
|
+
this.logger.debug(`Could not reset device to firmware mode: ${err}`);
|
|
3554
|
+
// Continue anyway - console mode might still work
|
|
3555
|
+
}
|
|
3556
|
+
return false;
|
|
3557
|
+
}
|
|
3558
|
+
|
|
2945
3559
|
/**
|
|
2946
3560
|
* @name reconnectAndResume
|
|
2947
3561
|
* Reconnect the serial port to flush browser buffers and reload stub
|
|
@@ -2954,6 +3568,7 @@ export class ESPLoader extends EventTarget {
|
|
|
2954
3568
|
|
|
2955
3569
|
try {
|
|
2956
3570
|
this.logger.log("Reconnecting serial port...");
|
|
3571
|
+
const savedBaudRate = this.currentBaudRate;
|
|
2957
3572
|
|
|
2958
3573
|
this.connected = false;
|
|
2959
3574
|
this.__inputBuffer = [];
|
|
@@ -2992,7 +3607,7 @@ export class ESPLoader extends EventTarget {
|
|
|
2992
3607
|
// Close port
|
|
2993
3608
|
try {
|
|
2994
3609
|
await this.port.close();
|
|
2995
|
-
this.logger.
|
|
3610
|
+
this.logger.debug("Port closed");
|
|
2996
3611
|
} catch (err) {
|
|
2997
3612
|
this.logger.debug(`Port close error: ${err}`);
|
|
2998
3613
|
}
|
|
@@ -3002,6 +3617,7 @@ export class ESPLoader extends EventTarget {
|
|
|
3002
3617
|
try {
|
|
3003
3618
|
await this.port.open({ baudRate: ESP_ROM_BAUD });
|
|
3004
3619
|
this.connected = true;
|
|
3620
|
+
this.currentBaudRate = ESP_ROM_BAUD;
|
|
3005
3621
|
} catch (err) {
|
|
3006
3622
|
throw new Error(`Failed to open port: ${err}`);
|
|
3007
3623
|
}
|
|
@@ -3055,8 +3671,8 @@ export class ESPLoader extends EventTarget {
|
|
|
3055
3671
|
this.logger.debug("Stub loaded");
|
|
3056
3672
|
|
|
3057
3673
|
// Restore baudrate if it was changed
|
|
3058
|
-
if (
|
|
3059
|
-
await stubLoader.setBaudrate(
|
|
3674
|
+
if (savedBaudRate !== ESP_ROM_BAUD) {
|
|
3675
|
+
await stubLoader.setBaudrate(savedBaudRate);
|
|
3060
3676
|
|
|
3061
3677
|
// Verify port is still ready after baudrate change
|
|
3062
3678
|
if (!this.port.writable || !this.port.readable) {
|
|
@@ -3093,6 +3709,9 @@ export class ESPLoader extends EventTarget {
|
|
|
3093
3709
|
try {
|
|
3094
3710
|
this.logger.log("Reconnecting to bootloader mode...");
|
|
3095
3711
|
|
|
3712
|
+
// Clear console mode flag when reconnecting to bootloader
|
|
3713
|
+
this._consoleMode = false;
|
|
3714
|
+
|
|
3096
3715
|
this.connected = false;
|
|
3097
3716
|
this.__inputBuffer = [];
|
|
3098
3717
|
this.__inputBufferReadIndex = 0;
|
|
@@ -3130,7 +3749,7 @@ export class ESPLoader extends EventTarget {
|
|
|
3130
3749
|
// Close port
|
|
3131
3750
|
try {
|
|
3132
3751
|
await this.port.close();
|
|
3133
|
-
this.logger.
|
|
3752
|
+
this.logger.debug("Port closed");
|
|
3134
3753
|
} catch (err) {
|
|
3135
3754
|
this.logger.debug(`Port close error: ${err}`);
|
|
3136
3755
|
}
|
|
@@ -3140,6 +3759,7 @@ export class ESPLoader extends EventTarget {
|
|
|
3140
3759
|
try {
|
|
3141
3760
|
await this.port.open({ baudRate: ESP_ROM_BAUD });
|
|
3142
3761
|
this.connected = true;
|
|
3762
|
+
this.currentBaudRate = ESP_ROM_BAUD;
|
|
3143
3763
|
} catch (err) {
|
|
3144
3764
|
throw new Error(`Failed to open port: ${err}`);
|
|
3145
3765
|
}
|
|
@@ -3157,6 +3777,8 @@ export class ESPLoader extends EventTarget {
|
|
|
3157
3777
|
// Reset chip info and stub state
|
|
3158
3778
|
this.__chipFamily = undefined;
|
|
3159
3779
|
this.chipName = "Unknown Chip";
|
|
3780
|
+
this.chipRevision = null;
|
|
3781
|
+
this.chipVariant = null;
|
|
3160
3782
|
this.IS_STUB = false;
|
|
3161
3783
|
|
|
3162
3784
|
// Start read loop
|
|
@@ -3184,6 +3806,115 @@ export class ESPLoader extends EventTarget {
|
|
|
3184
3806
|
}
|
|
3185
3807
|
}
|
|
3186
3808
|
|
|
3809
|
+
/**
|
|
3810
|
+
* @name exitConsoleMode
|
|
3811
|
+
* Exit console mode and return to bootloader
|
|
3812
|
+
* For ESP32-S2, uses reconnectToBootloader which will trigger port change
|
|
3813
|
+
* @returns true if manual reconnection is needed (ESP32-S2), false otherwise
|
|
3814
|
+
*/
|
|
3815
|
+
async exitConsoleMode(): Promise<boolean> {
|
|
3816
|
+
if (this._parent) {
|
|
3817
|
+
return await this._parent.exitConsoleMode();
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
// Clear console mode flag
|
|
3821
|
+
this._consoleMode = false;
|
|
3822
|
+
|
|
3823
|
+
// Check if this is ESP32-S2 with USB-JTAG/OTG
|
|
3824
|
+
const isESP32S2 = this.chipFamily === CHIP_FAMILY_ESP32S2;
|
|
3825
|
+
|
|
3826
|
+
// For ESP32-S2: if _isUsbJtagOrOtg is undefined, try to detect it
|
|
3827
|
+
// If detection fails or is undefined, assume USB-JTAG/OTG (conservative/safe path)
|
|
3828
|
+
let isUsbJtagOrOtg = this._isUsbJtagOrOtg;
|
|
3829
|
+
if (isESP32S2 && isUsbJtagOrOtg === undefined) {
|
|
3830
|
+
try {
|
|
3831
|
+
isUsbJtagOrOtg = await this.detectUsbConnectionType();
|
|
3832
|
+
} catch (err) {
|
|
3833
|
+
this.logger.debug(
|
|
3834
|
+
`USB detection failed, assuming USB-JTAG/OTG for ESP32-S2: ${err}`,
|
|
3835
|
+
);
|
|
3836
|
+
isUsbJtagOrOtg = true; // Conservative fallback for ESP32-S2
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
if (isESP32S2 && isUsbJtagOrOtg) {
|
|
3841
|
+
// ESP32-S2 USB: Use reconnectToBootloader which handles the mode switch
|
|
3842
|
+
// This will close the port and the device will reboot to bootloader
|
|
3843
|
+
this.logger.log("ESP32-S2 USB detected - reconnecting to bootloader");
|
|
3844
|
+
|
|
3845
|
+
try {
|
|
3846
|
+
await this.reconnectToBootloader();
|
|
3847
|
+
} catch (err) {
|
|
3848
|
+
this.logger.debug(`Reconnect error (expected for ESP32-S2): ${err}`);
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
// For ESP32-S2, port will change, so return true to indicate manual reconnection needed
|
|
3852
|
+
return true;
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
// For other devices, use standard reconnectToBootloader
|
|
3856
|
+
await this.reconnectToBootloader();
|
|
3857
|
+
return false; // No manual reconnection needed
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
/**
|
|
3861
|
+
* @name isConsoleResetSupported
|
|
3862
|
+
* Check if console reset is supported for this device
|
|
3863
|
+
* ESP32-S2 USB-JTAG/CDC does not support reset in console mode
|
|
3864
|
+
* because any reset causes USB port to be lost (hardware limitation)
|
|
3865
|
+
*/
|
|
3866
|
+
isConsoleResetSupported(): boolean {
|
|
3867
|
+
if (this._parent) {
|
|
3868
|
+
return this._parent.isConsoleResetSupported();
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
// For ESP32-S2: if _isUsbJtagOrOtg is undefined, assume USB-JTAG/OTG (conservative)
|
|
3872
|
+
// This means console reset is NOT supported (safer default)
|
|
3873
|
+
const isS2UsbJtag =
|
|
3874
|
+
this.chipFamily === CHIP_FAMILY_ESP32S2 &&
|
|
3875
|
+
(this._isUsbJtagOrOtg === true || this._isUsbJtagOrOtg === undefined);
|
|
3876
|
+
return !isS2UsbJtag; // Not supported for ESP32-S2 USB-JTAG/CDC
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
/**
|
|
3880
|
+
* @name resetInConsoleMode
|
|
3881
|
+
* Reset device while in console mode (firmware mode)
|
|
3882
|
+
*
|
|
3883
|
+
* NOTE: For ESP32-S2 USB-JTAG/CDC, ANY reset (hardware or software) causes
|
|
3884
|
+
* the USB port to be lost because the device switches USB modes during reset.
|
|
3885
|
+
* This is a hardware limitation - use isConsoleResetSupported() to check first.
|
|
3886
|
+
*/
|
|
3887
|
+
async resetInConsoleMode(): Promise<void> {
|
|
3888
|
+
if (this._parent) {
|
|
3889
|
+
return await this._parent.resetInConsoleMode();
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
if (!this.isConsoleResetSupported()) {
|
|
3893
|
+
this.logger.debug(
|
|
3894
|
+
"Console reset not supported for ESP32-S2 USB-JTAG/CDC",
|
|
3895
|
+
);
|
|
3896
|
+
return; // Do nothing
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3899
|
+
// For other devices: Use standard firmware reset
|
|
3900
|
+
const isWebUSB = (this.port as any).isWebUSB === true;
|
|
3901
|
+
|
|
3902
|
+
try {
|
|
3903
|
+
this.logger.debug("Resetting device in console mode");
|
|
3904
|
+
|
|
3905
|
+
if (isWebUSB) {
|
|
3906
|
+
await this.hardResetToFirmwareWebUSB();
|
|
3907
|
+
} else {
|
|
3908
|
+
await this.hardResetToFirmware();
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
this.logger.debug("Device reset complete");
|
|
3912
|
+
} catch (err) {
|
|
3913
|
+
this.logger.error(`Reset failed: ${err}`);
|
|
3914
|
+
throw err;
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3187
3918
|
/**
|
|
3188
3919
|
* @name drainInputBuffer
|
|
3189
3920
|
* Actively drain the input buffer by reading data for a specified time.
|
|
@@ -3199,7 +3930,7 @@ export class ESPLoader extends EventTarget {
|
|
|
3199
3930
|
await sleep(bufferingTime);
|
|
3200
3931
|
|
|
3201
3932
|
// Unsupported command response is sent 8 times and has
|
|
3202
|
-
// 14 bytes length including delimiter 0xC0 bytes.
|
|
3933
|
+
// 14 bytes length including delimiter SLIP_END (0xC0) bytes.
|
|
3203
3934
|
// At least part of it is read as a command response,
|
|
3204
3935
|
// but to be safe, read it all.
|
|
3205
3936
|
const bytesToDrain = 14 * 8;
|
|
@@ -3262,6 +3993,10 @@ export class ESPLoader extends EventTarget {
|
|
|
3262
3993
|
* @param addr - Address to read from
|
|
3263
3994
|
* @param size - Number of bytes to read
|
|
3264
3995
|
* @param onPacketReceived - Optional callback function called when packet is received
|
|
3996
|
+
* @param options - Optional parameters for advanced control
|
|
3997
|
+
* - chunkSize: Amount of data to request from ESP in one command (bytes)
|
|
3998
|
+
* - blockSize: Size of each data block sent by ESP (bytes)
|
|
3999
|
+
* - maxInFlight: Maximum unacknowledged bytes (bytes)
|
|
3265
4000
|
* @returns Uint8Array containing the flash data
|
|
3266
4001
|
*/
|
|
3267
4002
|
async readFlash(
|
|
@@ -3272,6 +4007,11 @@ export class ESPLoader extends EventTarget {
|
|
|
3272
4007
|
progress: number,
|
|
3273
4008
|
totalSize: number,
|
|
3274
4009
|
) => void,
|
|
4010
|
+
options?: {
|
|
4011
|
+
chunkSize?: number;
|
|
4012
|
+
blockSize?: number;
|
|
4013
|
+
maxInFlight?: number;
|
|
4014
|
+
},
|
|
3275
4015
|
): Promise<Uint8Array> {
|
|
3276
4016
|
if (!this.IS_STUB) {
|
|
3277
4017
|
throw new Error(
|
|
@@ -3311,7 +4051,13 @@ export class ESPLoader extends EventTarget {
|
|
|
3311
4051
|
// For WebUSB (Android), use smaller chunks to avoid timeouts and buffer issues
|
|
3312
4052
|
// For Web Serial (Desktop), use larger chunks for better performance
|
|
3313
4053
|
let CHUNK_SIZE: number;
|
|
3314
|
-
if (
|
|
4054
|
+
if (options?.chunkSize !== undefined) {
|
|
4055
|
+
// Use user-provided chunkSize if in advanced mode
|
|
4056
|
+
CHUNK_SIZE = options.chunkSize;
|
|
4057
|
+
this.logger.log(
|
|
4058
|
+
`Using custom chunk size: 0x${CHUNK_SIZE.toString(16)} bytes`,
|
|
4059
|
+
);
|
|
4060
|
+
} else if (this.isWebUSB()) {
|
|
3315
4061
|
// WebUSB: Use smaller chunks to avoid SLIP timeout issues
|
|
3316
4062
|
CHUNK_SIZE = 0x4 * 0x1000; // 4KB = 16384 bytes
|
|
3317
4063
|
} else {
|
|
@@ -3346,7 +4092,19 @@ export class ESPLoader extends EventTarget {
|
|
|
3346
4092
|
let blockSize: number;
|
|
3347
4093
|
let maxInFlight: number;
|
|
3348
4094
|
|
|
3349
|
-
if (
|
|
4095
|
+
if (
|
|
4096
|
+
options?.blockSize !== undefined &&
|
|
4097
|
+
options?.maxInFlight !== undefined
|
|
4098
|
+
) {
|
|
4099
|
+
// Use user-provided values if in advanced mode
|
|
4100
|
+
blockSize = options.blockSize;
|
|
4101
|
+
maxInFlight = options.maxInFlight;
|
|
4102
|
+
if (retryCount === 0) {
|
|
4103
|
+
this.logger.debug(
|
|
4104
|
+
`Using custom parameters: blockSize=${blockSize}, maxInFlight=${maxInFlight}`,
|
|
4105
|
+
);
|
|
4106
|
+
}
|
|
4107
|
+
} else if (this.isWebUSB()) {
|
|
3350
4108
|
// WebUSB (Android): All devices use adaptive speed
|
|
3351
4109
|
// All have maxTransferSize=64, baseBlockSize=31
|
|
3352
4110
|
const maxTransferSize =
|
|
@@ -3392,7 +4150,7 @@ export class ESPLoader extends EventTarget {
|
|
|
3392
4150
|
// The stub expects 4 bytes (ACK), if we send less it will break out
|
|
3393
4151
|
try {
|
|
3394
4152
|
// Send SLIP frame with no data (just delimiters)
|
|
3395
|
-
const abortFrame = [
|
|
4153
|
+
const abortFrame = [this.SLIP_END, this.SLIP_END]; // Empty SLIP frame
|
|
3396
4154
|
await this.writeToStream(abortFrame);
|
|
3397
4155
|
this.logger.debug(`Sent abort frame to stub`);
|
|
3398
4156
|
|
|
@@ -3538,7 +4296,7 @@ export class ESPLoader extends EventTarget {
|
|
|
3538
4296
|
// Check if it's a timeout error or SLIP error
|
|
3539
4297
|
if (err instanceof SlipReadError) {
|
|
3540
4298
|
if (retryCount <= MAX_RETRIES) {
|
|
3541
|
-
this.logger.
|
|
4299
|
+
this.logger.debug(
|
|
3542
4300
|
`${err.message} at 0x${currentAddr.toString(16)}. Draining buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`,
|
|
3543
4301
|
);
|
|
3544
4302
|
|