esp32tool 1.1.9 → 1.3.0
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/.nojekyll +0 -0
- package/README.md +100 -6
- package/apple-touch-icon.png +0 -0
- package/build-electron-cli.cjs +177 -0
- package/build-single-binary.cjs +295 -0
- package/css/light.css +11 -0
- package/css/style.css +261 -41
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +458 -0
- package/dist/console.d.ts +15 -0
- package/dist/console.js +237 -0
- package/dist/const.d.ts +99 -0
- package/dist/const.js +129 -8
- package/dist/esp_loader.d.ts +244 -22
- package/dist/esp_loader.js +1960 -251
- package/dist/index.d.ts +2 -1
- package/dist/index.js +37 -4
- package/dist/node-usb-adapter.d.ts +47 -0
- package/dist/node-usb-adapter.js +725 -0
- package/dist/stubs/index.d.ts +1 -2
- package/dist/stubs/index.js +4 -0
- package/dist/util/console-color.d.ts +19 -0
- package/dist/util/console-color.js +272 -0
- package/dist/util/line-break-transformer.d.ts +5 -0
- package/dist/util/line-break-transformer.js +17 -0
- package/dist/web/index.js +1 -1
- package/electron/cli-main.cjs +74 -0
- package/electron/main.cjs +338 -0
- package/electron/main.js +7 -2
- package/favicon.ico +0 -0
- package/fix-cli-imports.cjs +127 -0
- package/generate-icons.sh +89 -0
- package/icons/icon-128.png +0 -0
- package/icons/icon-144.png +0 -0
- package/icons/icon-152.png +0 -0
- package/icons/icon-192.png +0 -0
- package/icons/icon-384.png +0 -0
- package/icons/icon-512.png +0 -0
- package/icons/icon-72.png +0 -0
- package/icons/icon-96.png +0 -0
- package/index.html +143 -73
- package/install-android.html +411 -0
- package/js/console.js +269 -0
- package/js/modules/esptool.js +1 -1
- package/js/script.js +750 -175
- package/js/util/console-color.js +282 -0
- package/js/util/line-break-transformer.js +19 -0
- package/js/webusb-serial.js +1017 -0
- package/license.md +1 -1
- package/manifest.json +89 -0
- package/package.cli.json +29 -0
- package/package.json +35 -24
- package/screenshots/desktop.png +0 -0
- package/screenshots/mobile.png +0 -0
- package/src/cli.ts +618 -0
- package/src/console.ts +278 -0
- package/src/const.ts +165 -8
- package/src/esp_loader.ts +2354 -302
- package/src/index.ts +69 -3
- package/src/node-usb-adapter.ts +924 -0
- package/src/stubs/index.ts +4 -1
- package/src/util/console-color.ts +290 -0
- package/src/util/line-break-transformer.ts +20 -0
- package/sw.js +155 -0
package/dist/esp_loader.js
CHANGED
|
@@ -1,35 +1,158 @@
|
|
|
1
1
|
/// <reference types="@types/w3c-web-serial" />
|
|
2
|
-
import { CHIP_FAMILY_ESP32, CHIP_FAMILY_ESP32S2, CHIP_FAMILY_ESP32S3, CHIP_FAMILY_ESP32C2, CHIP_FAMILY_ESP32C3, CHIP_FAMILY_ESP32C5, CHIP_FAMILY_ESP32C6, CHIP_FAMILY_ESP32C61, CHIP_FAMILY_ESP32H2, CHIP_FAMILY_ESP32H4, CHIP_FAMILY_ESP32H21, CHIP_FAMILY_ESP32P4, CHIP_FAMILY_ESP32S31, CHIP_FAMILY_ESP8266, MAX_TIMEOUT, DEFAULT_TIMEOUT, ERASE_REGION_TIMEOUT_PER_MB, ESP_CHANGE_BAUDRATE, ESP_CHECKSUM_MAGIC, ESP_FLASH_BEGIN, ESP_FLASH_DATA, ESP_FLASH_END, ESP_MEM_BEGIN, ESP_MEM_DATA, ESP_MEM_END, ESP_READ_REG, ESP_WRITE_REG, ESP_SPI_ATTACH, ESP_SYNC, ESP_GET_SECURITY_INFO, FLASH_SECTOR_SIZE, FLASH_WRITE_SIZE, STUB_FLASH_WRITE_SIZE, MEM_END_ROM_TIMEOUT, ROM_INVALID_RECV_MSG, SYNC_PACKET, SYNC_TIMEOUT, USB_RAM_BLOCK, ESP_ERASE_FLASH, ESP_READ_FLASH, CHIP_ERASE_TIMEOUT, FLASH_READ_TIMEOUT, timeoutPerMb, ESP_ROM_BAUD, USB_JTAG_SERIAL_PID, ESP_FLASH_DEFL_BEGIN, ESP_FLASH_DEFL_DATA, ESP_FLASH_DEFL_END, getSpiFlashAddresses, DETECTED_FLASH_SIZES, CHIP_DETECT_MAGIC_REG_ADDR, CHIP_DETECT_MAGIC_VALUES, CHIP_ID_TO_INFO, ESP32P4_EFUSE_BLOCK1_ADDR, SlipReadError, } from "./const";
|
|
2
|
+
import { CHIP_FAMILY_ESP32, CHIP_FAMILY_ESP32S2, CHIP_FAMILY_ESP32S3, CHIP_FAMILY_ESP32C2, CHIP_FAMILY_ESP32C3, CHIP_FAMILY_ESP32C5, CHIP_FAMILY_ESP32C6, CHIP_FAMILY_ESP32C61, CHIP_FAMILY_ESP32H2, CHIP_FAMILY_ESP32H4, CHIP_FAMILY_ESP32H21, CHIP_FAMILY_ESP32P4, CHIP_FAMILY_ESP32S31, CHIP_FAMILY_ESP8266, MAX_TIMEOUT, DEFAULT_TIMEOUT, ERASE_REGION_TIMEOUT_PER_MB, ESP_CHANGE_BAUDRATE, ESP_CHECKSUM_MAGIC, ESP_FLASH_BEGIN, ESP_FLASH_DATA, ESP_FLASH_END, ESP_MEM_BEGIN, ESP_MEM_DATA, ESP_MEM_END, ESP_READ_REG, ESP_WRITE_REG, ESP_SPI_ATTACH, ESP_SYNC, ESP_GET_SECURITY_INFO, FLASH_SECTOR_SIZE, FLASH_WRITE_SIZE, STUB_FLASH_WRITE_SIZE, MEM_END_ROM_TIMEOUT, ROM_INVALID_RECV_MSG, SYNC_PACKET, SYNC_TIMEOUT, USB_RAM_BLOCK, ESP_ERASE_FLASH, ESP_ERASE_REGION, ESP_READ_FLASH, CHIP_ERASE_TIMEOUT, FLASH_READ_TIMEOUT, timeoutPerMb, ESP_ROM_BAUD, USB_JTAG_SERIAL_PID, ESP_FLASH_DEFL_BEGIN, ESP_FLASH_DEFL_DATA, ESP_FLASH_DEFL_END, getSpiFlashAddresses, DETECTED_FLASH_SIZES, CHIP_DETECT_MAGIC_REG_ADDR, CHIP_DETECT_MAGIC_VALUES, CHIP_ID_TO_INFO, ESP32P4_EFUSE_BLOCK1_ADDR, SlipReadError, ESP32S2_RTC_CNTL_WDTWPROTECT_REG, ESP32S2_RTC_CNTL_WDTCONFIG0_REG, ESP32S2_RTC_CNTL_WDTCONFIG1_REG, ESP32S2_RTC_CNTL_WDT_WKEY, ESP32S2_GPIO_STRAP_REG, ESP32S2_GPIO_STRAP_SPI_BOOT_MASK, ESP32S2_RTC_CNTL_OPTION1_REG, ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, ESP32S3_RTC_CNTL_WDTWPROTECT_REG, ESP32S3_RTC_CNTL_WDTCONFIG0_REG, ESP32S3_RTC_CNTL_WDTCONFIG1_REG, ESP32S3_RTC_CNTL_WDT_WKEY, ESP32S3_GPIO_STRAP_REG, ESP32S3_GPIO_STRAP_SPI_BOOT_MASK, ESP32S3_RTC_CNTL_OPTION1_REG, ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, ESP32S2_UARTDEV_BUF_NO, ESP32S2_UARTDEV_BUF_NO_USB_OTG, ESP32S3_UARTDEV_BUF_NO, ESP32S3_UARTDEV_BUF_NO_USB_OTG, ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL, ESP32C3_UARTDEV_BUF_NO_USB_JTAG_SERIAL, ESP32C3_BUF_UART_NO_OFFSET, ESP32C3_EFUSE_RD_MAC_SPI_SYS_3_REG, ESP32C3_EFUSE_RD_MAC_SPI_SYS_5_REG, ESP32C3_RTC_CNTL_WDTWPROTECT_REG, ESP32C3_RTC_CNTL_WDTCONFIG0_REG, ESP32C3_RTC_CNTL_WDTCONFIG1_REG, ESP32C3_RTC_CNTL_WDT_WKEY, ESP32C5_C6_RTC_CNTL_WDTWPROTECT_REG, ESP32C5_C6_RTC_CNTL_WDTCONFIG0_REG, ESP32C5_C6_RTC_CNTL_WDTCONFIG1_REG, ESP32C5_C6_RTC_CNTL_WDT_WKEY, ESP32C5_UARTDEV_BUF_NO, ESP32C5_UARTDEV_BUF_NO_USB_JTAG_SERIAL, ESP32C6_UARTDEV_BUF_NO, ESP32C6_UARTDEV_BUF_NO_USB_JTAG_SERIAL, ESP32P4_RTC_CNTL_WDTWPROTECT_REG, ESP32P4_RTC_CNTL_WDTCONFIG0_REG, ESP32P4_RTC_CNTL_WDTCONFIG1_REG, ESP32P4_RTC_CNTL_WDT_WKEY, ESP32P4_UARTDEV_BUF_NO_REV0, ESP32P4_UARTDEV_BUF_NO_REV300, ESP32P4_UARTDEV_BUF_NO_USB_OTG, ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL, ESP32P4_RTC_CNTL_OPTION1_REG, ESP32P4_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, ESP32H2_UARTDEV_BUF_NO, ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL, } from "./const";
|
|
3
3
|
import { getStubCode } from "./stubs";
|
|
4
4
|
import { hexFormatter, sleep, slipEncode, toHex } from "./util";
|
|
5
|
-
|
|
6
|
-
import { deflate } from "pako/dist/pako.esm.mjs";
|
|
5
|
+
import { deflate } from "pako";
|
|
7
6
|
import { pack, unpack } from "./struct";
|
|
8
7
|
export class ESPLoader extends EventTarget {
|
|
8
|
+
/**
|
|
9
|
+
* Check if device is using USB-JTAG or USB-OTG (not external serial chip)
|
|
10
|
+
* Returns undefined if not yet determined
|
|
11
|
+
*/
|
|
12
|
+
get isUsbJtagOrOtg() {
|
|
13
|
+
return this._parent ? this._parent._isUsbJtagOrOtg : this._isUsbJtagOrOtg;
|
|
14
|
+
}
|
|
9
15
|
constructor(port, logger, _parent) {
|
|
10
16
|
super();
|
|
11
17
|
this.port = port;
|
|
12
18
|
this.logger = logger;
|
|
13
19
|
this._parent = _parent;
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
20
|
+
this.__chipName = null;
|
|
21
|
+
this.__chipRevision = null;
|
|
22
|
+
this.__chipVariant = null;
|
|
17
23
|
this._efuses = new Array(4).fill(0);
|
|
18
24
|
this._flashsize = 4 * 1024 * 1024;
|
|
19
25
|
this.debug = false;
|
|
20
26
|
this.IS_STUB = false;
|
|
21
27
|
this.connected = true;
|
|
22
28
|
this.flashSize = null;
|
|
23
|
-
this.
|
|
29
|
+
this.currentBaudRate = ESP_ROM_BAUD;
|
|
24
30
|
this._isESP32S2NativeUSB = false;
|
|
25
31
|
this._initializationSucceeded = false;
|
|
26
32
|
this.__commandLock = Promise.resolve([0, []]);
|
|
27
33
|
this.__isReconfiguring = false;
|
|
34
|
+
this.__abandonCurrentOperation = false;
|
|
35
|
+
this._suppressDisconnect = false;
|
|
36
|
+
this.__consoleMode = false;
|
|
37
|
+
this._isUsbJtagOrOtg = undefined;
|
|
38
|
+
// Adaptive speed adjustment for flash read operations
|
|
39
|
+
this.__adaptiveBlockMultiplier = 1;
|
|
40
|
+
this.__adaptiveMaxInFlightMultiplier = 1;
|
|
41
|
+
this.__consecutiveSuccessfulChunks = 0;
|
|
42
|
+
this.__lastAdaptiveAdjustment = 0;
|
|
43
|
+
this.__isCDCDevice = false;
|
|
28
44
|
this.state_DTR = false;
|
|
45
|
+
this.state_RTS = false;
|
|
29
46
|
this.__writeChain = Promise.resolve();
|
|
30
47
|
}
|
|
48
|
+
// Chip properties with parent delegation
|
|
49
|
+
// chipFamily accessed before initialization as designed
|
|
50
|
+
get chipFamily() {
|
|
51
|
+
return this._parent ? this._parent.chipFamily : this.__chipFamily;
|
|
52
|
+
}
|
|
53
|
+
set chipFamily(value) {
|
|
54
|
+
if (this._parent) {
|
|
55
|
+
this._parent.chipFamily = value;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
this.__chipFamily = value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
get chipName() {
|
|
62
|
+
return this._parent ? this._parent.chipName : this.__chipName;
|
|
63
|
+
}
|
|
64
|
+
set chipName(value) {
|
|
65
|
+
if (this._parent) {
|
|
66
|
+
this._parent.chipName = value;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
this.__chipName = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
get chipRevision() {
|
|
73
|
+
return this._parent ? this._parent.chipRevision : this.__chipRevision;
|
|
74
|
+
}
|
|
75
|
+
set chipRevision(value) {
|
|
76
|
+
if (this._parent) {
|
|
77
|
+
this._parent.chipRevision = value;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
this.__chipRevision = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
get chipVariant() {
|
|
84
|
+
return this._parent ? this._parent.chipVariant : this.__chipVariant;
|
|
85
|
+
}
|
|
86
|
+
set chipVariant(value) {
|
|
87
|
+
if (this._parent) {
|
|
88
|
+
this._parent.chipVariant = value;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.__chipVariant = value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Console mode with parent delegation
|
|
95
|
+
get _consoleMode() {
|
|
96
|
+
return this._parent ? this._parent._consoleMode : this.__consoleMode;
|
|
97
|
+
}
|
|
98
|
+
set _consoleMode(value) {
|
|
99
|
+
if (this._parent) {
|
|
100
|
+
this._parent._consoleMode = value;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
this.__consoleMode = value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Public setter for console mode (used by script.js)
|
|
107
|
+
setConsoleMode(value) {
|
|
108
|
+
this._consoleMode = value;
|
|
109
|
+
}
|
|
31
110
|
get _inputBuffer() {
|
|
32
|
-
|
|
111
|
+
if (this._parent) {
|
|
112
|
+
return this._parent._inputBuffer;
|
|
113
|
+
}
|
|
114
|
+
if (this.__inputBuffer === undefined) {
|
|
115
|
+
throw new Error("_inputBuffer accessed before initialization");
|
|
116
|
+
}
|
|
117
|
+
return this.__inputBuffer;
|
|
118
|
+
}
|
|
119
|
+
get _inputBufferReadIndex() {
|
|
120
|
+
return this._parent
|
|
121
|
+
? this._parent._inputBufferReadIndex
|
|
122
|
+
: this.__inputBufferReadIndex || 0;
|
|
123
|
+
}
|
|
124
|
+
set _inputBufferReadIndex(value) {
|
|
125
|
+
if (this._parent) {
|
|
126
|
+
this._parent._inputBufferReadIndex = value;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
this.__inputBufferReadIndex = value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Get available bytes in buffer (from read index to end)
|
|
133
|
+
get _inputBufferAvailable() {
|
|
134
|
+
return this._inputBuffer.length - this._inputBufferReadIndex;
|
|
135
|
+
}
|
|
136
|
+
// Read one byte from buffer (ring-buffer style with index pointer)
|
|
137
|
+
_readByte() {
|
|
138
|
+
if (this._inputBufferReadIndex >= this._inputBuffer.length) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
return this._inputBuffer[this._inputBufferReadIndex++];
|
|
142
|
+
}
|
|
143
|
+
// Clear input buffer and reset read index
|
|
144
|
+
_clearInputBuffer() {
|
|
145
|
+
this._inputBuffer.length = 0;
|
|
146
|
+
this._inputBufferReadIndex = 0;
|
|
147
|
+
}
|
|
148
|
+
// Compact buffer when read index gets too large (prevent memory growth)
|
|
149
|
+
_compactInputBuffer() {
|
|
150
|
+
if (this._inputBufferReadIndex > 1000 &&
|
|
151
|
+
this._inputBufferReadIndex > this._inputBuffer.length / 2) {
|
|
152
|
+
// Remove already-read bytes and reset index
|
|
153
|
+
this._inputBuffer.splice(0, this._inputBufferReadIndex);
|
|
154
|
+
this._inputBufferReadIndex = 0;
|
|
155
|
+
}
|
|
33
156
|
}
|
|
34
157
|
get _totalBytesRead() {
|
|
35
158
|
return this._parent
|
|
@@ -68,6 +191,82 @@ export class ESPLoader extends EventTarget {
|
|
|
68
191
|
this.__isReconfiguring = value;
|
|
69
192
|
}
|
|
70
193
|
}
|
|
194
|
+
get _abandonCurrentOperation() {
|
|
195
|
+
return this._parent
|
|
196
|
+
? this._parent._abandonCurrentOperation
|
|
197
|
+
: this.__abandonCurrentOperation;
|
|
198
|
+
}
|
|
199
|
+
set _abandonCurrentOperation(value) {
|
|
200
|
+
if (this._parent) {
|
|
201
|
+
this._parent._abandonCurrentOperation = value;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
this.__abandonCurrentOperation = value;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
get _adaptiveBlockMultiplier() {
|
|
208
|
+
return this._parent
|
|
209
|
+
? this._parent._adaptiveBlockMultiplier
|
|
210
|
+
: this.__adaptiveBlockMultiplier;
|
|
211
|
+
}
|
|
212
|
+
set _adaptiveBlockMultiplier(value) {
|
|
213
|
+
if (this._parent) {
|
|
214
|
+
this._parent._adaptiveBlockMultiplier = value;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
this.__adaptiveBlockMultiplier = value;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
get _adaptiveMaxInFlightMultiplier() {
|
|
221
|
+
return this._parent
|
|
222
|
+
? this._parent._adaptiveMaxInFlightMultiplier
|
|
223
|
+
: this.__adaptiveMaxInFlightMultiplier;
|
|
224
|
+
}
|
|
225
|
+
set _adaptiveMaxInFlightMultiplier(value) {
|
|
226
|
+
if (this._parent) {
|
|
227
|
+
this._parent._adaptiveMaxInFlightMultiplier = value;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
this.__adaptiveMaxInFlightMultiplier = value;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
get _consecutiveSuccessfulChunks() {
|
|
234
|
+
return this._parent
|
|
235
|
+
? this._parent._consecutiveSuccessfulChunks
|
|
236
|
+
: this.__consecutiveSuccessfulChunks;
|
|
237
|
+
}
|
|
238
|
+
set _consecutiveSuccessfulChunks(value) {
|
|
239
|
+
if (this._parent) {
|
|
240
|
+
this._parent._consecutiveSuccessfulChunks = value;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
this.__consecutiveSuccessfulChunks = value;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
get _lastAdaptiveAdjustment() {
|
|
247
|
+
return this._parent
|
|
248
|
+
? this._parent._lastAdaptiveAdjustment
|
|
249
|
+
: this.__lastAdaptiveAdjustment;
|
|
250
|
+
}
|
|
251
|
+
set _lastAdaptiveAdjustment(value) {
|
|
252
|
+
if (this._parent) {
|
|
253
|
+
this._parent._lastAdaptiveAdjustment = value;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
this.__lastAdaptiveAdjustment = value;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
get _isCDCDevice() {
|
|
260
|
+
return this._parent ? this._parent._isCDCDevice : this.__isCDCDevice;
|
|
261
|
+
}
|
|
262
|
+
set _isCDCDevice(value) {
|
|
263
|
+
if (this._parent) {
|
|
264
|
+
this._parent._isCDCDevice = value;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
this.__isCDCDevice = value;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
71
270
|
detectUSBSerialChip(vendorId, productId) {
|
|
72
271
|
// Common USB-Serial chip vendors and their products
|
|
73
272
|
const chips = {
|
|
@@ -115,6 +314,7 @@ export class ESPLoader extends EventTarget {
|
|
|
115
314
|
async initialize() {
|
|
116
315
|
if (!this._parent) {
|
|
117
316
|
this.__inputBuffer = [];
|
|
317
|
+
this.__inputBufferReadIndex = 0;
|
|
118
318
|
this.__totalBytesRead = 0;
|
|
119
319
|
// Detect and log USB-Serial chip info
|
|
120
320
|
const portInfo = this.port.getInfo();
|
|
@@ -129,6 +329,12 @@ export class ESPLoader extends EventTarget {
|
|
|
129
329
|
if (portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x2) {
|
|
130
330
|
this._isESP32S2NativeUSB = true;
|
|
131
331
|
}
|
|
332
|
+
// Detect CDC devices for adaptive speed adjustment
|
|
333
|
+
// Espressif Native USB (VID: 0x303a) or CH343 (VID: 0x1a86, PID: 0x55d3)
|
|
334
|
+
if (portInfo.usbVendorId === 0x303a ||
|
|
335
|
+
(portInfo.usbVendorId === 0x1a86 && portInfo.usbProductId === 0x55d3)) {
|
|
336
|
+
this._isCDCDevice = true;
|
|
337
|
+
}
|
|
132
338
|
}
|
|
133
339
|
// Don't await this promise so it doesn't block rest of method.
|
|
134
340
|
this.readLoop();
|
|
@@ -137,6 +343,36 @@ export class ESPLoader extends EventTarget {
|
|
|
137
343
|
await this.connectWithResetStrategies();
|
|
138
344
|
// Detect chip type
|
|
139
345
|
await this.detectChip();
|
|
346
|
+
// Detect if device is using USB-JTAG/Serial or USB-OTG (not external serial chip)
|
|
347
|
+
// This is needed to determine the correct reset strategy for console mode
|
|
348
|
+
try {
|
|
349
|
+
if (this.chipFamily === CHIP_FAMILY_ESP32S2 ||
|
|
350
|
+
this.chipFamily === CHIP_FAMILY_ESP32S3) {
|
|
351
|
+
const isUsingUsbOtg = await this.usingUsbOtg();
|
|
352
|
+
const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
|
|
353
|
+
this._isUsbJtagOrOtg = isUsingUsbOtg || isUsingUsbJtagSerial;
|
|
354
|
+
}
|
|
355
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32C3 ||
|
|
356
|
+
this.chipFamily === CHIP_FAMILY_ESP32C5 ||
|
|
357
|
+
this.chipFamily === CHIP_FAMILY_ESP32C6) {
|
|
358
|
+
const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
|
|
359
|
+
this._isUsbJtagOrOtg = isUsingUsbJtagSerial;
|
|
360
|
+
}
|
|
361
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
362
|
+
const isUsingUsbOtg = await this.usingUsbOtg();
|
|
363
|
+
const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
|
|
364
|
+
this._isUsbJtagOrOtg = isUsingUsbOtg || isUsingUsbJtagSerial;
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
// Other chips don't have USB-JTAG/OTG
|
|
368
|
+
this._isUsbJtagOrOtg = false;
|
|
369
|
+
}
|
|
370
|
+
this.logger.debug(`USB connection type: ${this._isUsbJtagOrOtg ? "USB-JTAG/OTG" : "External Serial Chip"}`);
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
this.logger.debug(`Could not detect USB connection type: ${err}`);
|
|
374
|
+
// Leave as undefined if detection fails
|
|
375
|
+
}
|
|
140
376
|
// Read the OTP data for this chip and store into this.efuses array
|
|
141
377
|
const FlAddr = getSpiFlashAddresses(this.getChipFamily());
|
|
142
378
|
const AddrMAC = FlAddr.macFuse;
|
|
@@ -160,7 +396,7 @@ export class ESPLoader extends EventTarget {
|
|
|
160
396
|
if (chipInfo) {
|
|
161
397
|
this.chipName = chipInfo.name;
|
|
162
398
|
this.chipFamily = chipInfo.family;
|
|
163
|
-
// Get chip revision for ESP32-P4
|
|
399
|
+
// Get chip revision for ESP32-P4 and ESP32-C3
|
|
164
400
|
if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
165
401
|
this.chipRevision = await this.getChipRevision();
|
|
166
402
|
this.logger.debug(`ESP32-P4 revision: ${this.chipRevision}`);
|
|
@@ -173,6 +409,10 @@ export class ESPLoader extends EventTarget {
|
|
|
173
409
|
}
|
|
174
410
|
this.logger.debug(`ESP32-P4 variant: ${this.chipVariant}`);
|
|
175
411
|
}
|
|
412
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
|
|
413
|
+
this.chipRevision = await this.getChipRevision();
|
|
414
|
+
this.logger.debug(`ESP32-C3 revision: ${this.chipRevision}`);
|
|
415
|
+
}
|
|
176
416
|
this.logger.debug(`Detected chip via IMAGE_CHIP_ID: ${chipId} (${this.chipName})`);
|
|
177
417
|
return;
|
|
178
418
|
}
|
|
@@ -185,7 +425,7 @@ export class ESPLoader extends EventTarget {
|
|
|
185
425
|
// This ensures all error responses are cleared before continuing
|
|
186
426
|
await this.drainInputBuffer(200);
|
|
187
427
|
// Clear input buffer and re-sync to recover from failed command
|
|
188
|
-
this.
|
|
428
|
+
this._clearInputBuffer();
|
|
189
429
|
await sleep(SYNC_TIMEOUT);
|
|
190
430
|
// Re-sync with the chip to ensure clean communication
|
|
191
431
|
try {
|
|
@@ -214,24 +454,31 @@ export class ESPLoader extends EventTarget {
|
|
|
214
454
|
}
|
|
215
455
|
this.logger.debug(`ESP32-P4 variant: ${this.chipVariant}`);
|
|
216
456
|
}
|
|
457
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
|
|
458
|
+
this.chipRevision = await this.getChipRevision();
|
|
459
|
+
this.logger.debug(`ESP32-C3 revision: ${this.chipRevision}`);
|
|
460
|
+
}
|
|
217
461
|
this.logger.debug(`Detected chip via magic value: ${toHex(chipMagicValue >>> 0, 8)} (${this.chipName})`);
|
|
218
462
|
}
|
|
219
463
|
/**
|
|
220
464
|
* Get chip revision for ESP32-P4
|
|
221
465
|
*/
|
|
222
466
|
async getChipRevision() {
|
|
223
|
-
if (this.chipFamily
|
|
224
|
-
|
|
467
|
+
if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
468
|
+
// Read from EFUSE_BLOCK1 to get chip revision
|
|
469
|
+
// Word 2 contains revision info for ESP32-P4
|
|
470
|
+
const word2 = await this.readRegister(ESP32P4_EFUSE_BLOCK1_ADDR + 8);
|
|
471
|
+
// Minor revision: bits [3:0]
|
|
472
|
+
const minorRev = word2 & 0x0f;
|
|
473
|
+
// Major revision: bits [23] << 2 | bits [5:4]
|
|
474
|
+
const majorRev = (((word2 >> 23) & 1) << 2) | ((word2 >> 4) & 0x03);
|
|
475
|
+
// Revision is major * 100 + minor
|
|
476
|
+
return majorRev * 100 + minorRev;
|
|
477
|
+
}
|
|
478
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
|
|
479
|
+
return await this.getChipRevisionC3();
|
|
225
480
|
}
|
|
226
|
-
|
|
227
|
-
// Word 2 contains revision info for ESP32-P4
|
|
228
|
-
const word2 = await this.readRegister(ESP32P4_EFUSE_BLOCK1_ADDR + 8);
|
|
229
|
-
// Minor revision: bits [3:0]
|
|
230
|
-
const minorRev = word2 & 0x0f;
|
|
231
|
-
// Major revision: bits [23] << 2 | bits [5:4]
|
|
232
|
-
const majorRev = (((word2 >> 23) & 1) << 2) | ((word2 >> 4) & 0x03);
|
|
233
|
-
// Revision is major * 100 + minor
|
|
234
|
-
return majorRev * 100 + minorRev;
|
|
481
|
+
return 0;
|
|
235
482
|
}
|
|
236
483
|
/**
|
|
237
484
|
* Get security info including chip ID (ESP32-C3 and later)
|
|
@@ -262,6 +509,18 @@ export class ESPLoader extends EventTarget {
|
|
|
262
509
|
apiVersion,
|
|
263
510
|
};
|
|
264
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* Get MAC address from efuses
|
|
514
|
+
*/
|
|
515
|
+
async getMacAddress() {
|
|
516
|
+
if (!this._initializationSucceeded) {
|
|
517
|
+
throw new Error("getMacAddress() requires initialize() to have completed successfully");
|
|
518
|
+
}
|
|
519
|
+
const macBytes = this.macAddr(); // chip-family-aware
|
|
520
|
+
return macBytes
|
|
521
|
+
.map((b) => b.toString(16).padStart(2, "0").toUpperCase())
|
|
522
|
+
.join(":");
|
|
523
|
+
}
|
|
265
524
|
/**
|
|
266
525
|
* @name readLoop
|
|
267
526
|
* Reads data from the input stream and places it in the inputBuffer
|
|
@@ -292,7 +551,27 @@ export class ESPLoader extends EventTarget {
|
|
|
292
551
|
}
|
|
293
552
|
}
|
|
294
553
|
catch {
|
|
295
|
-
|
|
554
|
+
// Don't log error if this is an expected disconnect during console mode transition
|
|
555
|
+
if (!this._consoleMode) {
|
|
556
|
+
this.logger.error("Read loop got disconnected");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
finally {
|
|
560
|
+
// Always reset reconfiguring flag when read loop ends
|
|
561
|
+
// This prevents "Cannot write during port reconfiguration" errors
|
|
562
|
+
// when the read loop dies unexpectedly
|
|
563
|
+
this._isReconfiguring = false;
|
|
564
|
+
// Release reader if still locked
|
|
565
|
+
if (this._reader) {
|
|
566
|
+
try {
|
|
567
|
+
this._reader.releaseLock();
|
|
568
|
+
this.logger.debug("Reader released in readLoop cleanup");
|
|
569
|
+
}
|
|
570
|
+
catch (err) {
|
|
571
|
+
this.logger.debug(`Reader release error in readLoop: ${err}`);
|
|
572
|
+
}
|
|
573
|
+
this._reader = undefined;
|
|
574
|
+
}
|
|
296
575
|
}
|
|
297
576
|
// Disconnected!
|
|
298
577
|
this.connected = false;
|
|
@@ -304,12 +583,19 @@ export class ESPLoader extends EventTarget {
|
|
|
304
583
|
detail: { message: "ESP32-S2 Native USB requires port reselection" },
|
|
305
584
|
}));
|
|
306
585
|
}
|
|
307
|
-
|
|
586
|
+
// Only dispatch disconnect event if not suppressed
|
|
587
|
+
if (!this._suppressDisconnect) {
|
|
588
|
+
this.dispatchEvent(new Event("disconnect"));
|
|
589
|
+
}
|
|
590
|
+
this._suppressDisconnect = false;
|
|
308
591
|
this.logger.debug("Finished read loop");
|
|
309
592
|
}
|
|
310
593
|
sleep(ms = 100) {
|
|
311
594
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
312
595
|
}
|
|
596
|
+
// ============================================================================
|
|
597
|
+
// Web Serial (Desktop) - DTR/RTS Signal Handling & Reset Strategies
|
|
598
|
+
// ============================================================================
|
|
313
599
|
async setRTS(state) {
|
|
314
600
|
await this.port.setSignals({ requestToSend: state });
|
|
315
601
|
// Work-around for adapters on Windows using the usbser.sys driver:
|
|
@@ -322,24 +608,928 @@ export class ESPLoader extends EventTarget {
|
|
|
322
608
|
this.state_DTR = state;
|
|
323
609
|
await this.port.setSignals({ dataTerminalReady: state });
|
|
324
610
|
}
|
|
611
|
+
async setDTRandRTS(dtr, rts) {
|
|
612
|
+
this.state_DTR = dtr;
|
|
613
|
+
this.state_RTS = rts;
|
|
614
|
+
await this.port.setSignals({
|
|
615
|
+
dataTerminalReady: dtr,
|
|
616
|
+
requestToSend: rts,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* @name hardResetUSBJTAGSerial
|
|
621
|
+
* USB-JTAG/Serial reset for Web Serial (Desktop)
|
|
622
|
+
*/
|
|
623
|
+
async hardResetUSBJTAGSerial() {
|
|
624
|
+
await this.setRTS(false);
|
|
625
|
+
await this.setDTR(false); // Idle
|
|
626
|
+
await this.sleep(100);
|
|
627
|
+
await this.setDTR(true); // Set IO0
|
|
628
|
+
await this.setRTS(false);
|
|
629
|
+
await this.sleep(100);
|
|
630
|
+
await this.setRTS(true); // Reset
|
|
631
|
+
await this.setDTR(false);
|
|
632
|
+
await this.setRTS(true);
|
|
633
|
+
await this.sleep(100);
|
|
634
|
+
await this.setDTR(false);
|
|
635
|
+
await this.setRTS(false); // Chip out of reset
|
|
636
|
+
await this.sleep(200);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* @name hardResetClassic
|
|
640
|
+
* Classic reset for Web Serial (Desktop) DTR = IO0, RTS = EN
|
|
641
|
+
*/
|
|
642
|
+
async hardResetClassic() {
|
|
643
|
+
await this.setDTR(false); // IO0=HIGH
|
|
644
|
+
await this.setRTS(true); // EN=LOW, chip in reset
|
|
645
|
+
await this.sleep(100);
|
|
646
|
+
await this.setDTR(true); // IO0=LOW
|
|
647
|
+
await this.setRTS(false); // EN=HIGH, chip out of reset
|
|
648
|
+
await this.sleep(50);
|
|
649
|
+
await this.setDTR(false); // IO0=HIGH, done
|
|
650
|
+
await this.sleep(200);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Reset to firmware mode (not bootloader) for Web Serial
|
|
654
|
+
* Keeps IO0=HIGH during reset so chip boots into firmware
|
|
655
|
+
*/
|
|
656
|
+
async hardResetToFirmware() {
|
|
657
|
+
await this.setDTR(false); // IO0=HIGH
|
|
658
|
+
await this.setRTS(true); // EN=LOW, chip in reset
|
|
659
|
+
await this.sleep(100);
|
|
660
|
+
await this.setRTS(false); // EN=HIGH, chip out of reset (IO0 stays HIGH)
|
|
661
|
+
await this.sleep(50);
|
|
662
|
+
await this.sleep(200);
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Reset to firmware mode (not bootloader) for WebUSB
|
|
666
|
+
* Keeps IO0=HIGH during reset so chip boots into firmware
|
|
667
|
+
*/
|
|
668
|
+
async hardResetToFirmwareWebUSB() {
|
|
669
|
+
await this.setDTRWebUSB(false); // IO0=HIGH
|
|
670
|
+
await this.setRTSWebUSB(true); // EN=LOW, chip in reset
|
|
671
|
+
await this.sleep(100);
|
|
672
|
+
await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset (IO0 stays HIGH)
|
|
673
|
+
await this.sleep(50);
|
|
674
|
+
await this.sleep(200);
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* @name hardResetUnixTight
|
|
678
|
+
* Unix Tight reset for Web Serial (Desktop) - sets DTR and RTS simultaneously
|
|
679
|
+
*/
|
|
680
|
+
async hardResetUnixTight() {
|
|
681
|
+
await this.setDTRandRTS(true, true);
|
|
682
|
+
await this.setDTRandRTS(false, false);
|
|
683
|
+
await this.setDTRandRTS(false, true); // IO0=HIGH & EN=LOW, chip in reset
|
|
684
|
+
await this.sleep(100);
|
|
685
|
+
await this.setDTRandRTS(true, false); // IO0=LOW & EN=HIGH, chip out of reset
|
|
686
|
+
await this.sleep(50);
|
|
687
|
+
await this.setDTRandRTS(false, false); // IO0=HIGH, done
|
|
688
|
+
await this.setDTR(false); // Needed in some environments to ensure IO0=HIGH
|
|
689
|
+
await this.sleep(200);
|
|
690
|
+
}
|
|
691
|
+
// ============================================================================
|
|
692
|
+
// WebUSB (Android) - DTR/RTS Signal Handling & Reset Strategies
|
|
693
|
+
// ============================================================================
|
|
694
|
+
async setRTSWebUSB(state) {
|
|
695
|
+
this.state_RTS = state;
|
|
696
|
+
// Always specify both signals to avoid flipping the other line
|
|
697
|
+
// The WebUSB setSignals() now preserves unspecified signals, but being explicit is safer
|
|
698
|
+
await this.port.setSignals({
|
|
699
|
+
requestToSend: state,
|
|
700
|
+
dataTerminalReady: this.state_DTR,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
async setDTRWebUSB(state) {
|
|
704
|
+
this.state_DTR = state;
|
|
705
|
+
// Always specify both signals to avoid flipping the other line
|
|
706
|
+
await this.port.setSignals({
|
|
707
|
+
dataTerminalReady: state,
|
|
708
|
+
requestToSend: this.state_RTS, // Explicitly preserve current RTS state
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
async setDTRandRTSWebUSB(dtr, rts) {
|
|
712
|
+
this.state_DTR = dtr;
|
|
713
|
+
this.state_RTS = rts;
|
|
714
|
+
await this.port.setSignals({
|
|
715
|
+
dataTerminalReady: dtr,
|
|
716
|
+
requestToSend: rts,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* @name hardResetUSBJTAGSerialWebUSB
|
|
721
|
+
* USB-JTAG/Serial reset for WebUSB (Android)
|
|
722
|
+
*/
|
|
723
|
+
async hardResetUSBJTAGSerialWebUSB() {
|
|
724
|
+
await this.setRTSWebUSB(false);
|
|
725
|
+
await this.setDTRWebUSB(false); // Idle
|
|
726
|
+
await this.sleep(100);
|
|
727
|
+
await this.setDTRWebUSB(true); // Set IO0
|
|
728
|
+
await this.setRTSWebUSB(false);
|
|
729
|
+
await this.sleep(100);
|
|
730
|
+
await this.setRTSWebUSB(true); // Reset
|
|
731
|
+
await this.setDTRWebUSB(false);
|
|
732
|
+
await this.setRTSWebUSB(true);
|
|
733
|
+
await this.sleep(100);
|
|
734
|
+
await this.setDTRWebUSB(false);
|
|
735
|
+
await this.setRTSWebUSB(false); // Chip out of reset
|
|
736
|
+
await this.sleep(200);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* @name hardResetUSBJTAGSerialInvertedDTRWebUSB
|
|
740
|
+
* USB-JTAG/Serial reset with inverted DTR for WebUSB (Android)
|
|
741
|
+
*/
|
|
742
|
+
async hardResetUSBJTAGSerialInvertedDTRWebUSB() {
|
|
743
|
+
await this.setRTSWebUSB(false);
|
|
744
|
+
await this.setDTRWebUSB(true); // Idle (DTR inverted)
|
|
745
|
+
await this.sleep(100);
|
|
746
|
+
await this.setDTRWebUSB(false); // Set IO0 (DTR inverted)
|
|
747
|
+
await this.setRTSWebUSB(false);
|
|
748
|
+
await this.sleep(100);
|
|
749
|
+
await this.setRTSWebUSB(true); // Reset
|
|
750
|
+
await this.setDTRWebUSB(true); // (DTR inverted)
|
|
751
|
+
await this.setRTSWebUSB(true);
|
|
752
|
+
await this.sleep(100);
|
|
753
|
+
await this.setDTRWebUSB(true); // (DTR inverted)
|
|
754
|
+
await this.setRTSWebUSB(false); // Chip out of reset
|
|
755
|
+
await this.sleep(200);
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* @name hardResetClassicWebUSB
|
|
759
|
+
* Classic reset for WebUSB (Android)
|
|
760
|
+
*/
|
|
761
|
+
async hardResetClassicWebUSB() {
|
|
762
|
+
await this.setDTRWebUSB(false); // IO0=HIGH
|
|
763
|
+
await this.setRTSWebUSB(true); // EN=LOW, chip in reset
|
|
764
|
+
await this.sleep(100);
|
|
765
|
+
await this.setDTRWebUSB(true); // IO0=LOW
|
|
766
|
+
await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
|
|
767
|
+
await this.sleep(50);
|
|
768
|
+
await this.setDTRWebUSB(false); // IO0=HIGH, done
|
|
769
|
+
await this.sleep(200);
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* @name hardResetUnixTightWebUSB
|
|
773
|
+
* Unix Tight reset for WebUSB (Android) - sets DTR and RTS simultaneously
|
|
774
|
+
*/
|
|
775
|
+
async hardResetUnixTightWebUSB() {
|
|
776
|
+
await this.setDTRandRTSWebUSB(false, false);
|
|
777
|
+
await this.setDTRandRTSWebUSB(true, true);
|
|
778
|
+
await this.setDTRandRTSWebUSB(false, true); // IO0=HIGH & EN=LOW, chip in reset
|
|
779
|
+
await this.sleep(100);
|
|
780
|
+
await this.setDTRandRTSWebUSB(true, false); // IO0=LOW & EN=HIGH, chip out of reset
|
|
781
|
+
await this.sleep(50);
|
|
782
|
+
await this.setDTRandRTSWebUSB(false, false); // IO0=HIGH, done
|
|
783
|
+
await this.setDTRWebUSB(false); // Ensure IO0=HIGH
|
|
784
|
+
await this.sleep(200);
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* @name hardResetClassicLongDelayWebUSB
|
|
788
|
+
* Classic reset with longer delays for WebUSB (Android)
|
|
789
|
+
* Specifically for CP2102/CH340 which may need more time
|
|
790
|
+
*/
|
|
791
|
+
async hardResetClassicLongDelayWebUSB() {
|
|
792
|
+
await this.setDTRWebUSB(false); // IO0=HIGH
|
|
793
|
+
await this.setRTSWebUSB(true); // EN=LOW, chip in reset
|
|
794
|
+
await this.sleep(500); // Extra long delay
|
|
795
|
+
await this.setDTRWebUSB(true); // IO0=LOW
|
|
796
|
+
await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
|
|
797
|
+
await this.sleep(200);
|
|
798
|
+
await this.setDTRWebUSB(false); // IO0=HIGH, done
|
|
799
|
+
await this.sleep(500); // Extra long delay
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* @name hardResetClassicShortDelayWebUSB
|
|
803
|
+
* Classic reset with shorter delays for WebUSB (Android)
|
|
804
|
+
*/
|
|
805
|
+
async hardResetClassicShortDelayWebUSB() {
|
|
806
|
+
await this.setDTRWebUSB(false); // IO0=HIGH
|
|
807
|
+
await this.setRTSWebUSB(true); // EN=LOW, chip in reset
|
|
808
|
+
await this.sleep(50);
|
|
809
|
+
await this.setDTRWebUSB(true); // IO0=LOW
|
|
810
|
+
await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
|
|
811
|
+
await this.sleep(25);
|
|
812
|
+
await this.setDTRWebUSB(false); // IO0=HIGH, done
|
|
813
|
+
await this.sleep(100);
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* @name hardResetInvertedWebUSB
|
|
817
|
+
* Inverted reset sequence for WebUSB (Android) - both signals inverted
|
|
818
|
+
*/
|
|
819
|
+
async hardResetInvertedWebUSB() {
|
|
820
|
+
await this.setDTRWebUSB(true); // IO0=HIGH (inverted)
|
|
821
|
+
await this.setRTSWebUSB(false); // EN=LOW, chip in reset (inverted)
|
|
822
|
+
await this.sleep(100);
|
|
823
|
+
await this.setDTRWebUSB(false); // IO0=LOW (inverted)
|
|
824
|
+
await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (inverted)
|
|
825
|
+
await this.sleep(50);
|
|
826
|
+
await this.setDTRWebUSB(true); // IO0=HIGH, done (inverted)
|
|
827
|
+
await this.sleep(200);
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* @name hardResetInvertedDTRWebUSB
|
|
831
|
+
* Only DTR inverted for WebUSB (Android)
|
|
832
|
+
*/
|
|
833
|
+
async hardResetInvertedDTRWebUSB() {
|
|
834
|
+
await this.setDTRWebUSB(true); // IO0=HIGH (DTR inverted)
|
|
835
|
+
await this.setRTSWebUSB(true); // EN=LOW, chip in reset (RTS normal)
|
|
836
|
+
await this.sleep(100);
|
|
837
|
+
await this.setDTRWebUSB(false); // IO0=LOW (DTR inverted)
|
|
838
|
+
await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset (RTS normal)
|
|
839
|
+
await this.sleep(50);
|
|
840
|
+
await this.setDTRWebUSB(true); // IO0=HIGH, done (DTR inverted)
|
|
841
|
+
await this.sleep(200);
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* @name hardResetInvertedRTSWebUSB
|
|
845
|
+
* Only RTS inverted for WebUSB (Android)
|
|
846
|
+
*/
|
|
847
|
+
async hardResetInvertedRTSWebUSB() {
|
|
848
|
+
await this.setDTRWebUSB(false); // IO0=HIGH (DTR normal)
|
|
849
|
+
await this.setRTSWebUSB(false); // EN=LOW, chip in reset (RTS inverted)
|
|
850
|
+
await this.sleep(100);
|
|
851
|
+
await this.setDTRWebUSB(true); // IO0=LOW (DTR normal)
|
|
852
|
+
await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (RTS inverted)
|
|
853
|
+
await this.sleep(50);
|
|
854
|
+
await this.setDTRWebUSB(false); // IO0=HIGH, done (DTR normal)
|
|
855
|
+
await this.sleep(200);
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Check if we're using WebUSB (Android) or Web Serial (Desktop)
|
|
859
|
+
*/
|
|
860
|
+
isWebUSB() {
|
|
861
|
+
// WebUSBSerial class has isWebUSB flag - this is the most reliable check
|
|
862
|
+
return this.port.isWebUSB === true;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* @name connectWithResetStrategies
|
|
866
|
+
* Try different reset strategies to enter bootloader mode
|
|
867
|
+
* Similar to esptool.py's connect() method with multiple reset strategies
|
|
868
|
+
*/
|
|
869
|
+
async connectWithResetStrategies() {
|
|
870
|
+
const portInfo = this.port.getInfo();
|
|
871
|
+
const isUSBJTAGSerial = portInfo.usbProductId === USB_JTAG_SERIAL_PID;
|
|
872
|
+
const isEspressifUSB = portInfo.usbVendorId === 0x303a;
|
|
873
|
+
// this.logger.log(
|
|
874
|
+
// `Detected USB: VID=0x${portInfo.usbVendorId?.toString(16) || "unknown"}, PID=0x${portInfo.usbProductId?.toString(16) || "unknown"}`,
|
|
875
|
+
// );
|
|
876
|
+
// Define reset strategies to try in order
|
|
877
|
+
const resetStrategies = [];
|
|
878
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
879
|
+
const self = this;
|
|
880
|
+
// Detect if this is a USB-Serial chip (needs different sync approach)
|
|
881
|
+
const isUSBSerialChip = !isUSBJTAGSerial && !isEspressifUSB;
|
|
882
|
+
// WebUSB (Android) uses different reset methods than Web Serial (Desktop)
|
|
883
|
+
if (this.isWebUSB()) {
|
|
884
|
+
// For USB-Serial chips (CP2102, CH340, etc.), try inverted strategies first
|
|
885
|
+
// Detect specific chip types once
|
|
886
|
+
const isCP2102 = portInfo.usbVendorId === 0x10c4;
|
|
887
|
+
const isCH34x = portInfo.usbVendorId === 0x1a86;
|
|
888
|
+
// Check for ESP32-S2 Native USB (VID: 0x303a, PID: 0x0002)
|
|
889
|
+
const isESP32S2NativeUSB = portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x0002;
|
|
890
|
+
// WebUSB Strategy 1: USB-JTAG/Serial reset (for Native USB only)
|
|
891
|
+
if (isUSBJTAGSerial || isEspressifUSB) {
|
|
892
|
+
if (isESP32S2NativeUSB) {
|
|
893
|
+
// ESP32-S2 Native USB: Try multiple strategies
|
|
894
|
+
// The device might be in JTAG mode OR CDC mode
|
|
895
|
+
// Strategy 1: USB-JTAG/Serial (works in CDC mode on Desktop)
|
|
896
|
+
resetStrategies.push({
|
|
897
|
+
name: "USB-JTAG/Serial (WebUSB) - ESP32-S2",
|
|
898
|
+
fn: async () => {
|
|
899
|
+
return await self.hardResetUSBJTAGSerialWebUSB();
|
|
900
|
+
},
|
|
901
|
+
});
|
|
902
|
+
// Strategy 2: USB-JTAG/Serial Inverted DTR (works in JTAG mode)
|
|
903
|
+
resetStrategies.push({
|
|
904
|
+
name: "USB-JTAG/Serial Inverted DTR (WebUSB) - ESP32-S2",
|
|
905
|
+
fn: async () => {
|
|
906
|
+
return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
|
|
907
|
+
},
|
|
908
|
+
});
|
|
909
|
+
// Strategy 3: UnixTight (CDC fallback)
|
|
910
|
+
resetStrategies.push({
|
|
911
|
+
name: "UnixTight (WebUSB) - ESP32-S2 CDC",
|
|
912
|
+
fn: async () => {
|
|
913
|
+
return await self.hardResetUnixTightWebUSB();
|
|
914
|
+
},
|
|
915
|
+
});
|
|
916
|
+
// Strategy 4: Classic reset (CDC fallback)
|
|
917
|
+
resetStrategies.push({
|
|
918
|
+
name: "Classic (WebUSB) - ESP32-S2 CDC",
|
|
919
|
+
fn: async () => {
|
|
920
|
+
return await self.hardResetClassicWebUSB();
|
|
921
|
+
},
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
// Other USB-JTAG chips: Try Inverted DTR first - works best for ESP32-H2 and other JTAG chips
|
|
926
|
+
resetStrategies.push({
|
|
927
|
+
name: "USB-JTAG/Serial Inverted DTR (WebUSB)",
|
|
928
|
+
fn: async () => {
|
|
929
|
+
return await self.hardResetUSBJTAGSerialInvertedDTRWebUSB();
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
resetStrategies.push({
|
|
933
|
+
name: "USB-JTAG/Serial (WebUSB)",
|
|
934
|
+
fn: async () => {
|
|
935
|
+
return await self.hardResetUSBJTAGSerialWebUSB();
|
|
936
|
+
},
|
|
937
|
+
});
|
|
938
|
+
resetStrategies.push({
|
|
939
|
+
name: "Inverted DTR Classic (WebUSB)",
|
|
940
|
+
fn: async () => {
|
|
941
|
+
return await self.hardResetInvertedDTRWebUSB();
|
|
942
|
+
},
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
// For USB-Serial chips, try inverted strategies first
|
|
947
|
+
if (isUSBSerialChip) {
|
|
948
|
+
if (isCH34x) {
|
|
949
|
+
// CH340/CH343: UnixTight works best (like CP2102)
|
|
950
|
+
resetStrategies.push({
|
|
951
|
+
name: "UnixTight (WebUSB) - CH34x",
|
|
952
|
+
fn: async () => {
|
|
953
|
+
return await self.hardResetUnixTightWebUSB();
|
|
954
|
+
},
|
|
955
|
+
});
|
|
956
|
+
resetStrategies.push({
|
|
957
|
+
name: "Classic (WebUSB) - CH34x",
|
|
958
|
+
fn: async () => {
|
|
959
|
+
return await self.hardResetClassicWebUSB();
|
|
960
|
+
},
|
|
961
|
+
});
|
|
962
|
+
resetStrategies.push({
|
|
963
|
+
name: "Inverted Both (WebUSB) - CH34x",
|
|
964
|
+
fn: async () => {
|
|
965
|
+
return await self.hardResetInvertedWebUSB();
|
|
966
|
+
},
|
|
967
|
+
});
|
|
968
|
+
resetStrategies.push({
|
|
969
|
+
name: "Inverted RTS (WebUSB) - CH34x",
|
|
970
|
+
fn: async () => {
|
|
971
|
+
return await self.hardResetInvertedRTSWebUSB();
|
|
972
|
+
},
|
|
973
|
+
});
|
|
974
|
+
resetStrategies.push({
|
|
975
|
+
name: "Inverted DTR (WebUSB) - CH34x",
|
|
976
|
+
fn: async () => {
|
|
977
|
+
return await self.hardResetInvertedDTRWebUSB();
|
|
978
|
+
},
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
else if (isCP2102) {
|
|
982
|
+
// CP2102: UnixTight works best (tested and confirmed)
|
|
983
|
+
// Try it first, then fallback to other strategies
|
|
984
|
+
resetStrategies.push({
|
|
985
|
+
name: "UnixTight (WebUSB) - CP2102",
|
|
986
|
+
fn: async () => {
|
|
987
|
+
return await self.hardResetUnixTightWebUSB();
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
resetStrategies.push({
|
|
991
|
+
name: "Classic (WebUSB) - CP2102",
|
|
992
|
+
fn: async () => {
|
|
993
|
+
return await self.hardResetClassicWebUSB();
|
|
994
|
+
},
|
|
995
|
+
});
|
|
996
|
+
resetStrategies.push({
|
|
997
|
+
name: "Inverted Both (WebUSB) - CP2102",
|
|
998
|
+
fn: async () => {
|
|
999
|
+
return await self.hardResetInvertedWebUSB();
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
1002
|
+
resetStrategies.push({
|
|
1003
|
+
name: "Inverted RTS (WebUSB) - CP2102",
|
|
1004
|
+
fn: async () => {
|
|
1005
|
+
return await self.hardResetInvertedRTSWebUSB();
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
1008
|
+
resetStrategies.push({
|
|
1009
|
+
name: "Inverted DTR (WebUSB) - CP2102",
|
|
1010
|
+
fn: async () => {
|
|
1011
|
+
return await self.hardResetInvertedDTRWebUSB();
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
// For other USB-Serial chips, try UnixTight first, then multiple strategies
|
|
1017
|
+
resetStrategies.push({
|
|
1018
|
+
name: "UnixTight (WebUSB)",
|
|
1019
|
+
fn: async () => {
|
|
1020
|
+
return await self.hardResetUnixTightWebUSB();
|
|
1021
|
+
},
|
|
1022
|
+
});
|
|
1023
|
+
resetStrategies.push({
|
|
1024
|
+
name: "Classic (WebUSB)",
|
|
1025
|
+
fn: async function () {
|
|
1026
|
+
return await self.hardResetClassicWebUSB();
|
|
1027
|
+
},
|
|
1028
|
+
});
|
|
1029
|
+
resetStrategies.push({
|
|
1030
|
+
name: "Inverted Both (WebUSB)",
|
|
1031
|
+
fn: async function () {
|
|
1032
|
+
return await self.hardResetInvertedWebUSB();
|
|
1033
|
+
},
|
|
1034
|
+
});
|
|
1035
|
+
resetStrategies.push({
|
|
1036
|
+
name: "Inverted RTS (WebUSB)",
|
|
1037
|
+
fn: async function () {
|
|
1038
|
+
return await self.hardResetInvertedRTSWebUSB();
|
|
1039
|
+
},
|
|
1040
|
+
});
|
|
1041
|
+
resetStrategies.push({
|
|
1042
|
+
name: "Inverted DTR (WebUSB)",
|
|
1043
|
+
fn: async function () {
|
|
1044
|
+
return await self.hardResetInvertedDTRWebUSB();
|
|
1045
|
+
},
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
// Add general fallback strategies only for non-CP2102 and non-ESP32-S2 Native USB chips
|
|
1050
|
+
if (!isCP2102 && !isESP32S2NativeUSB) {
|
|
1051
|
+
// Classic reset (for chips not handled above)
|
|
1052
|
+
if (portInfo.usbVendorId !== 0x1a86) {
|
|
1053
|
+
resetStrategies.push({
|
|
1054
|
+
name: "Classic (WebUSB)",
|
|
1055
|
+
fn: async function () {
|
|
1056
|
+
return await self.hardResetClassicWebUSB();
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
// UnixTight reset (sets DTR/RTS simultaneously)
|
|
1061
|
+
resetStrategies.push({
|
|
1062
|
+
name: "UnixTight (WebUSB)",
|
|
1063
|
+
fn: async function () {
|
|
1064
|
+
return await self.hardResetUnixTightWebUSB();
|
|
1065
|
+
},
|
|
1066
|
+
});
|
|
1067
|
+
// WebUSB Strategy: Classic with long delays
|
|
1068
|
+
resetStrategies.push({
|
|
1069
|
+
name: "Classic Long Delay (WebUSB)",
|
|
1070
|
+
fn: async function () {
|
|
1071
|
+
return await self.hardResetClassicLongDelayWebUSB();
|
|
1072
|
+
},
|
|
1073
|
+
});
|
|
1074
|
+
// WebUSB Strategy: Classic with short delays
|
|
1075
|
+
resetStrategies.push({
|
|
1076
|
+
name: "Classic Short Delay (WebUSB)",
|
|
1077
|
+
fn: async function () {
|
|
1078
|
+
return await self.hardResetClassicShortDelayWebUSB();
|
|
1079
|
+
},
|
|
1080
|
+
});
|
|
1081
|
+
// WebUSB Strategy: USB-JTAG/Serial fallback
|
|
1082
|
+
if (!isUSBJTAGSerial && !isEspressifUSB) {
|
|
1083
|
+
resetStrategies.push({
|
|
1084
|
+
name: "USB-JTAG/Serial fallback (WebUSB)",
|
|
1085
|
+
fn: async function () {
|
|
1086
|
+
return await self.hardResetUSBJTAGSerialWebUSB();
|
|
1087
|
+
},
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
// Strategy: USB-JTAG/Serial reset
|
|
1094
|
+
if (isUSBJTAGSerial || isEspressifUSB) {
|
|
1095
|
+
resetStrategies.push({
|
|
1096
|
+
name: "USB-JTAG/Serial",
|
|
1097
|
+
fn: async function () {
|
|
1098
|
+
return await self.hardResetUSBJTAGSerial();
|
|
1099
|
+
},
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
// Strategy: UnixTight reset
|
|
1103
|
+
resetStrategies.push({
|
|
1104
|
+
name: "UnixTight",
|
|
1105
|
+
fn: async function () {
|
|
1106
|
+
return await self.hardResetUnixTight();
|
|
1107
|
+
},
|
|
1108
|
+
});
|
|
1109
|
+
// Strategy: USB-JTAG/Serial fallback
|
|
1110
|
+
if (!isUSBJTAGSerial && !isEspressifUSB) {
|
|
1111
|
+
resetStrategies.push({
|
|
1112
|
+
name: "USB-JTAG/Serial (fallback)",
|
|
1113
|
+
fn: async function () {
|
|
1114
|
+
return await self.hardResetUSBJTAGSerial();
|
|
1115
|
+
},
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
let lastError = null;
|
|
1120
|
+
// Try each reset strategy with timeout
|
|
1121
|
+
for (const strategy of resetStrategies) {
|
|
1122
|
+
try {
|
|
1123
|
+
// Check if port is still open, if not, skip this strategy
|
|
1124
|
+
if (!this.connected || !this.port.writable) {
|
|
1125
|
+
this.logger.debug(`Port disconnected, skipping ${strategy.name} reset`);
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
// Clear abandon flag before starting new strategy
|
|
1129
|
+
this._abandonCurrentOperation = false;
|
|
1130
|
+
await strategy.fn();
|
|
1131
|
+
// Try to sync after reset
|
|
1132
|
+
// USB-Serial / native USB chips needs different sync approaches
|
|
1133
|
+
if (isUSBSerialChip) {
|
|
1134
|
+
// USB-Serial chips: Use timeout strategy (2 seconds)
|
|
1135
|
+
// this.logger.log(`USB-Serial chip detected, using sync with timeout.`);
|
|
1136
|
+
const syncSuccess = await this.syncWithTimeout(2000);
|
|
1137
|
+
if (syncSuccess) {
|
|
1138
|
+
// Sync succeeded
|
|
1139
|
+
this.logger.log(`Connected USB Serial successfully with ${strategy.name} reset.`);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
throw new Error("Sync timeout or abandoned");
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
// Native USB chips
|
|
1148
|
+
// Note: We use Promise.race with sync() directly instead of syncWithTimeout()
|
|
1149
|
+
// because syncWithTimeout causes CDC/JTAG devices to hang for unknown reasons.
|
|
1150
|
+
// The abandon flag in readPacket() prevents overlapping I/O.
|
|
1151
|
+
// this.logger.log(`Native USB chip detected, using CDC/JTAG sync.`);
|
|
1152
|
+
const syncPromise = this.sync();
|
|
1153
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Sync timeout")), 1000));
|
|
1154
|
+
try {
|
|
1155
|
+
await Promise.race([syncPromise, timeoutPromise]);
|
|
1156
|
+
// Sync succeeded
|
|
1157
|
+
this.logger.log(`Connected CDC/JTAG successfully with ${strategy.name} reset.`);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
catch (error) {
|
|
1161
|
+
throw new Error("Sync timeout or abandoned");
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
lastError = error;
|
|
1167
|
+
this.logger.debug(`${strategy.name} reset failed: ${error.message}`);
|
|
1168
|
+
// Set abandon flag to stop any in-flight operations
|
|
1169
|
+
this._abandonCurrentOperation = true;
|
|
1170
|
+
// Wait a bit for in-flight operations to abort
|
|
1171
|
+
await sleep(100);
|
|
1172
|
+
// If port got disconnected, we can't try more strategies
|
|
1173
|
+
if (!this.connected || !this.port.writable) {
|
|
1174
|
+
this.logger.log(`Port disconnected during reset attempt`);
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
// Clear buffers before trying next strategy
|
|
1178
|
+
this._clearInputBuffer();
|
|
1179
|
+
await this.drainInputBuffer(200);
|
|
1180
|
+
await this.flushSerialBuffers();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
// All strategies failed - reset abandon flag before throwing
|
|
1184
|
+
this._abandonCurrentOperation = false;
|
|
1185
|
+
throw new Error(`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* @name watchdogReset
|
|
1189
|
+
* Watchdog reset for ESP32-S2/S3/C3 with USB-OTG or USB-JTAG/Serial
|
|
1190
|
+
* Uses RTC watchdog timer to reset the chip - works when DTR/RTS signals are not available
|
|
1191
|
+
* This is an alias for rtcWdtResetChipSpecific() for backwards compatibility
|
|
1192
|
+
*/
|
|
1193
|
+
async watchdogReset() {
|
|
1194
|
+
await this.rtcWdtResetChipSpecific();
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Check if current chip is using USB-OTG
|
|
1198
|
+
* Supports ESP32-S2 and ESP32-S3
|
|
1199
|
+
*/
|
|
1200
|
+
async usingUsbOtg() {
|
|
1201
|
+
let uartDevBufNo;
|
|
1202
|
+
let usbOtgValue;
|
|
1203
|
+
if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
|
|
1204
|
+
uartDevBufNo = ESP32S2_UARTDEV_BUF_NO;
|
|
1205
|
+
usbOtgValue = ESP32S2_UARTDEV_BUF_NO_USB_OTG;
|
|
1206
|
+
}
|
|
1207
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
|
|
1208
|
+
uartDevBufNo = ESP32S3_UARTDEV_BUF_NO;
|
|
1209
|
+
usbOtgValue = ESP32S3_UARTDEV_BUF_NO_USB_OTG;
|
|
1210
|
+
}
|
|
1211
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
1212
|
+
// P4: UARTDEV_BUF_NO depends on chip revision
|
|
1213
|
+
if (this.chipRevision === null) {
|
|
1214
|
+
this.chipRevision = await this.getChipRevision();
|
|
1215
|
+
}
|
|
1216
|
+
if (this.chipRevision < 300) {
|
|
1217
|
+
uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV0;
|
|
1218
|
+
}
|
|
1219
|
+
else {
|
|
1220
|
+
uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV300;
|
|
1221
|
+
}
|
|
1222
|
+
usbOtgValue = ESP32P4_UARTDEV_BUF_NO_USB_OTG;
|
|
1223
|
+
}
|
|
1224
|
+
else {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
const uartNo = (await this.readRegister(uartDevBufNo)) & 0xff;
|
|
1228
|
+
return uartNo === usbOtgValue;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Check if current chip is using USB-JTAG/Serial
|
|
1232
|
+
* Supports ESP32-S3 and ESP32-C3
|
|
1233
|
+
*/
|
|
1234
|
+
async usingUsbJtagSerial() {
|
|
1235
|
+
let uartDevBufNo;
|
|
1236
|
+
let usbJtagSerialValue;
|
|
1237
|
+
if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
|
|
1238
|
+
uartDevBufNo = ESP32S3_UARTDEV_BUF_NO;
|
|
1239
|
+
usbJtagSerialValue = ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
|
|
1240
|
+
}
|
|
1241
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
|
|
1242
|
+
// ESP32-C3: BSS_UART_DEV_ADDR depends on chip revision
|
|
1243
|
+
// Revision < 101: 0x3FCDF064
|
|
1244
|
+
// Revision >= 101: 0x3FCDF060
|
|
1245
|
+
let bssUartDevAddr;
|
|
1246
|
+
// Get chip revision if not already set
|
|
1247
|
+
if (this.chipRevision === null) {
|
|
1248
|
+
this.chipRevision = await this.getChipRevisionC3();
|
|
1249
|
+
}
|
|
1250
|
+
if (this.chipRevision < 101) {
|
|
1251
|
+
bssUartDevAddr = 0x3fcdf064;
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
bssUartDevAddr = 0x3fcdf060;
|
|
1255
|
+
}
|
|
1256
|
+
uartDevBufNo = bssUartDevAddr + ESP32C3_BUF_UART_NO_OFFSET;
|
|
1257
|
+
usbJtagSerialValue = ESP32C3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
|
|
1258
|
+
}
|
|
1259
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32C5) {
|
|
1260
|
+
uartDevBufNo = ESP32C5_UARTDEV_BUF_NO;
|
|
1261
|
+
usbJtagSerialValue = ESP32C5_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
|
|
1262
|
+
}
|
|
1263
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32C6) {
|
|
1264
|
+
uartDevBufNo = ESP32C6_UARTDEV_BUF_NO;
|
|
1265
|
+
usbJtagSerialValue = ESP32C6_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
|
|
1266
|
+
}
|
|
1267
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
1268
|
+
// P4: UARTDEV_BUF_NO depends on chip revision
|
|
1269
|
+
// Revision < 300: 0x4FF3FEC8
|
|
1270
|
+
// Revision >= 300: 0x4FFBFEC8
|
|
1271
|
+
if (this.chipRevision === null) {
|
|
1272
|
+
this.chipRevision = await this.getChipRevision();
|
|
1273
|
+
}
|
|
1274
|
+
if (this.chipRevision < 300) {
|
|
1275
|
+
uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV0;
|
|
1276
|
+
}
|
|
1277
|
+
else {
|
|
1278
|
+
uartDevBufNo = ESP32P4_UARTDEV_BUF_NO_REV300;
|
|
1279
|
+
}
|
|
1280
|
+
usbJtagSerialValue = ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
|
|
1281
|
+
}
|
|
1282
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32H2) {
|
|
1283
|
+
uartDevBufNo = ESP32H2_UARTDEV_BUF_NO;
|
|
1284
|
+
usbJtagSerialValue = ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
const uartNo = (await this.readRegister(uartDevBufNo)) & 0xff;
|
|
1290
|
+
return uartNo === usbJtagSerialValue;
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Get chip revision for ESP32-C3
|
|
1294
|
+
* Reads from EFUSE registers and calculates revision
|
|
1295
|
+
*/
|
|
1296
|
+
async getChipRevisionC3() {
|
|
1297
|
+
if (this.chipFamily !== CHIP_FAMILY_ESP32C3) {
|
|
1298
|
+
return 0;
|
|
1299
|
+
}
|
|
1300
|
+
// Read EFUSE_RD_MAC_SPI_SYS_3_REG (bits [20:18] = lower 3 bits of revision)
|
|
1301
|
+
const word3 = await this.readRegister(ESP32C3_EFUSE_RD_MAC_SPI_SYS_3_REG);
|
|
1302
|
+
const low = (word3 >> 18) & 0x07;
|
|
1303
|
+
// Read EFUSE_RD_MAC_SPI_SYS_5_REG (bits [25:23] = upper 3 bits of revision)
|
|
1304
|
+
const word5 = await this.readRegister(ESP32C3_EFUSE_RD_MAC_SPI_SYS_5_REG);
|
|
1305
|
+
const hi = (word5 >> 23) & 0x07;
|
|
1306
|
+
// Combine: upper 3 bits from word5, lower 3 bits from word3
|
|
1307
|
+
const revision = (hi << 3) | low;
|
|
1308
|
+
this.logger.debug(`ESP32-C3 revision: ${revision}`);
|
|
1309
|
+
return revision;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* RTC watchdog timer reset for ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C5, ESP32-C6, and ESP32-P4
|
|
1313
|
+
* Uses specific registers for each chip family
|
|
1314
|
+
* Note: ESP32-H2 does NOT support WDT reset
|
|
1315
|
+
*/
|
|
1316
|
+
async rtcWdtResetChipSpecific() {
|
|
1317
|
+
this.logger.debug("Hard resetting with watchdog timer...");
|
|
1318
|
+
let WDTWPROTECT_REG;
|
|
1319
|
+
let WDTCONFIG0_REG;
|
|
1320
|
+
let WDTCONFIG1_REG;
|
|
1321
|
+
let WDT_WKEY;
|
|
1322
|
+
if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
|
|
1323
|
+
WDTWPROTECT_REG = ESP32S2_RTC_CNTL_WDTWPROTECT_REG;
|
|
1324
|
+
WDTCONFIG0_REG = ESP32S2_RTC_CNTL_WDTCONFIG0_REG;
|
|
1325
|
+
WDTCONFIG1_REG = ESP32S2_RTC_CNTL_WDTCONFIG1_REG;
|
|
1326
|
+
WDT_WKEY = ESP32S2_RTC_CNTL_WDT_WKEY;
|
|
1327
|
+
}
|
|
1328
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
|
|
1329
|
+
WDTWPROTECT_REG = ESP32S3_RTC_CNTL_WDTWPROTECT_REG;
|
|
1330
|
+
WDTCONFIG0_REG = ESP32S3_RTC_CNTL_WDTCONFIG0_REG;
|
|
1331
|
+
WDTCONFIG1_REG = ESP32S3_RTC_CNTL_WDTCONFIG1_REG;
|
|
1332
|
+
WDT_WKEY = ESP32S3_RTC_CNTL_WDT_WKEY;
|
|
1333
|
+
}
|
|
1334
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
|
|
1335
|
+
WDTWPROTECT_REG = ESP32C3_RTC_CNTL_WDTWPROTECT_REG;
|
|
1336
|
+
WDTCONFIG0_REG = ESP32C3_RTC_CNTL_WDTCONFIG0_REG;
|
|
1337
|
+
WDTCONFIG1_REG = ESP32C3_RTC_CNTL_WDTCONFIG1_REG;
|
|
1338
|
+
WDT_WKEY = ESP32C3_RTC_CNTL_WDT_WKEY;
|
|
1339
|
+
}
|
|
1340
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32C5 ||
|
|
1341
|
+
this.chipFamily === CHIP_FAMILY_ESP32C6) {
|
|
1342
|
+
// C5 and C6 use LP_WDT (Low Power Watchdog Timer)
|
|
1343
|
+
WDTWPROTECT_REG = ESP32C5_C6_RTC_CNTL_WDTWPROTECT_REG;
|
|
1344
|
+
WDTCONFIG0_REG = ESP32C5_C6_RTC_CNTL_WDTCONFIG0_REG;
|
|
1345
|
+
WDTCONFIG1_REG = ESP32C5_C6_RTC_CNTL_WDTCONFIG1_REG;
|
|
1346
|
+
WDT_WKEY = ESP32C5_C6_RTC_CNTL_WDT_WKEY;
|
|
1347
|
+
}
|
|
1348
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
1349
|
+
// P4 uses LP_WDT (Low Power Watchdog Timer)
|
|
1350
|
+
WDTWPROTECT_REG = ESP32P4_RTC_CNTL_WDTWPROTECT_REG;
|
|
1351
|
+
WDTCONFIG0_REG = ESP32P4_RTC_CNTL_WDTCONFIG0_REG;
|
|
1352
|
+
WDTCONFIG1_REG = ESP32P4_RTC_CNTL_WDTCONFIG1_REG;
|
|
1353
|
+
WDT_WKEY = ESP32P4_RTC_CNTL_WDT_WKEY;
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
throw new Error(`rtcWdtResetChipSpecific() is not supported for ${this.chipFamily}`);
|
|
1357
|
+
}
|
|
1358
|
+
// Unlock watchdog registers
|
|
1359
|
+
await this.writeRegister(WDTWPROTECT_REG, WDT_WKEY, undefined, 0);
|
|
1360
|
+
// Clear force download boot register (if applicable) BEFORE triggering WDT reset
|
|
1361
|
+
// This ensures the chip boots into firmware mode after reset
|
|
1362
|
+
if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
|
|
1363
|
+
try {
|
|
1364
|
+
await this.writeRegister(ESP32S2_RTC_CNTL_OPTION1_REG, 0, ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, 0);
|
|
1365
|
+
this.logger.debug("Cleared force download boot mask");
|
|
1366
|
+
}
|
|
1367
|
+
catch (err) {
|
|
1368
|
+
this.logger.debug(`Expected error clearing force download boot mask: ${err}`);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
|
|
1372
|
+
try {
|
|
1373
|
+
await this.writeRegister(ESP32S3_RTC_CNTL_OPTION1_REG, 0, ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, 0);
|
|
1374
|
+
this.logger.debug("Cleared force download boot mask");
|
|
1375
|
+
}
|
|
1376
|
+
catch (err) {
|
|
1377
|
+
this.logger.debug(`Expected error clearing force download boot mask: ${err}`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
|
|
1381
|
+
try {
|
|
1382
|
+
await this.writeRegister(ESP32P4_RTC_CNTL_OPTION1_REG, 0, ESP32P4_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK, 0);
|
|
1383
|
+
this.logger.debug("Cleared force download boot mask");
|
|
1384
|
+
}
|
|
1385
|
+
catch (err) {
|
|
1386
|
+
this.logger.debug(`Expected error clearing force download boot mask: ${err}`);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
// Set WDT timeout to 2000ms (matches Python esptool)
|
|
1390
|
+
await this.writeRegister(WDTCONFIG1_REG, 2000, undefined, 0);
|
|
1391
|
+
// Enable WDT: bit 31 = enable, bits 28-30 = stage, bit 8 = sys reset, bits 0-2 = prescaler
|
|
1392
|
+
const wdtConfig = (1 << 31) | (5 << 28) | (1 << 8) | 2;
|
|
1393
|
+
await this.writeRegister(WDTCONFIG0_REG, wdtConfig, undefined, 0);
|
|
1394
|
+
// Lock watchdog registers
|
|
1395
|
+
await this.writeRegister(WDTWPROTECT_REG, 0, undefined, 0);
|
|
1396
|
+
// Wait for reset to take effect
|
|
1397
|
+
await this.sleep(500);
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Helper: Check if USB-based WDT reset should be used for S2/S3
|
|
1401
|
+
* Returns true if WDT reset was performed, false otherwise
|
|
1402
|
+
*/
|
|
1403
|
+
async tryUsbWdtReset(chipName, GPIO_STRAP_REG, GPIO_STRAP_SPI_BOOT_MASK, RTC_CNTL_OPTION1_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK) {
|
|
1404
|
+
const isUsingUsbOtg = await this.usingUsbOtg();
|
|
1405
|
+
const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
|
|
1406
|
+
if (isUsingUsbOtg || isUsingUsbJtagSerial) {
|
|
1407
|
+
const strapReg = await this.readRegister(GPIO_STRAP_REG);
|
|
1408
|
+
const forceDlReg = await this.readRegister(RTC_CNTL_OPTION1_REG);
|
|
1409
|
+
// Only use watchdog reset if GPIO0 is low AND force download boot mode is not set
|
|
1410
|
+
if ((strapReg & GPIO_STRAP_SPI_BOOT_MASK) === 0 &&
|
|
1411
|
+
(forceDlReg & RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK) === 0) {
|
|
1412
|
+
await this.rtcWdtResetChipSpecific();
|
|
1413
|
+
this.logger.debug(`${chipName}: RTC WDT reset (USB detected, GPIO0 low)`);
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return false;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Chip-specific hard reset for ESP32-S2
|
|
1421
|
+
* Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
|
|
1422
|
+
*/
|
|
1423
|
+
async hardResetS2() {
|
|
1424
|
+
const isUsingUsbOtg = await this.usingUsbOtg();
|
|
1425
|
+
if (isUsingUsbOtg) {
|
|
1426
|
+
await this.rtcWdtResetChipSpecific();
|
|
1427
|
+
this.logger.debug("ESP32-S2: RTC WDT reset (USB-OTG detected)");
|
|
1428
|
+
}
|
|
1429
|
+
else {
|
|
1430
|
+
// Use standard hardware reset
|
|
1431
|
+
await this.hardResetClassic();
|
|
1432
|
+
this.logger.debug("ESP32-S2: Classic reset");
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Chip-specific hard reset for ESP32-S3
|
|
1437
|
+
* Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
|
|
1438
|
+
*/
|
|
1439
|
+
async hardResetS3() {
|
|
1440
|
+
const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
|
|
1441
|
+
if (isUsingUsbJtagSerial) {
|
|
1442
|
+
await this.rtcWdtResetChipSpecific();
|
|
1443
|
+
this.logger.debug("ESP32-S3: RTC WDT reset (USB-JTAG/Serial detected)");
|
|
1444
|
+
}
|
|
1445
|
+
else {
|
|
1446
|
+
// Use standard hardware reset
|
|
1447
|
+
await this.hardResetClassic();
|
|
1448
|
+
this.logger.debug("ESP32-S3: Classic reset");
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Chip-specific hard reset for ESP32-C3
|
|
1453
|
+
* Checks if using USB-JTAG/Serial and uses watchdog reset if necessary
|
|
1454
|
+
*/
|
|
1455
|
+
async hardResetC3() {
|
|
1456
|
+
const isUsingUsbJtagSerial = await this.usingUsbJtagSerial();
|
|
1457
|
+
if (isUsingUsbJtagSerial) {
|
|
1458
|
+
await this.rtcWdtResetChipSpecific();
|
|
1459
|
+
this.logger.debug("ESP32-C3: RTC WDT reset (USB-JTAG/Serial detected)");
|
|
1460
|
+
}
|
|
1461
|
+
else {
|
|
1462
|
+
// Use standard hardware reset
|
|
1463
|
+
await this.hardResetClassic();
|
|
1464
|
+
this.logger.debug("ESP32-C3: Classic reset");
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
325
1467
|
async hardReset(bootloader = false) {
|
|
1468
|
+
// In console mode, only allow simple hardware reset (no bootloader entry)
|
|
1469
|
+
if (this._consoleMode) {
|
|
1470
|
+
if (bootloader) {
|
|
1471
|
+
this.logger.debug("Skipping bootloader reset - device is in console mode");
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
// Simple hardware reset to restart firmware (IO0=HIGH)
|
|
1475
|
+
this.logger.debug("Performing hardware reset (console mode)...");
|
|
1476
|
+
if (this.isWebUSB()) {
|
|
1477
|
+
await this.hardResetToFirmwareWebUSB();
|
|
1478
|
+
}
|
|
1479
|
+
else {
|
|
1480
|
+
await this.hardResetToFirmware();
|
|
1481
|
+
}
|
|
1482
|
+
this.logger.debug("Hardware reset complete");
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
326
1485
|
if (bootloader) {
|
|
327
1486
|
// enter flash mode
|
|
328
1487
|
if (this.port.getInfo().usbProductId === USB_JTAG_SERIAL_PID) {
|
|
329
1488
|
await this.hardResetUSBJTAGSerial();
|
|
330
|
-
this.logger.
|
|
1489
|
+
this.logger.debug("USB-JTAG/Serial reset.");
|
|
331
1490
|
}
|
|
332
1491
|
else {
|
|
333
|
-
|
|
334
|
-
this.
|
|
1492
|
+
// Use different reset strategy for WebUSB (Android) vs Web Serial (Desktop)
|
|
1493
|
+
if (this.isWebUSB()) {
|
|
1494
|
+
await this.hardResetClassicWebUSB();
|
|
1495
|
+
this.logger.debug("Classic reset (WebUSB/Android).");
|
|
1496
|
+
}
|
|
1497
|
+
else {
|
|
1498
|
+
await this.hardResetClassic();
|
|
1499
|
+
this.logger.debug("Classic reset.");
|
|
1500
|
+
}
|
|
335
1501
|
}
|
|
336
1502
|
}
|
|
337
1503
|
else {
|
|
338
|
-
// just reset
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
1504
|
+
// just reset (no bootloader mode)
|
|
1505
|
+
// For ESP32-S2/S3 with USB-OTG or USB-JTAG/Serial, check if watchdog reset is needed
|
|
1506
|
+
if (this.chipFamily === CHIP_FAMILY_ESP32S2 && !this._consoleMode) {
|
|
1507
|
+
const wdtResetUsed = await this.tryUsbWdtReset("ESP32-S2", ESP32S2_GPIO_STRAP_REG, ESP32S2_GPIO_STRAP_SPI_BOOT_MASK, ESP32S2_RTC_CNTL_OPTION1_REG, ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK);
|
|
1508
|
+
if (wdtResetUsed)
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
else if (this.chipFamily === CHIP_FAMILY_ESP32S3 &&
|
|
1512
|
+
!this._consoleMode) {
|
|
1513
|
+
const wdtResetUsed = await this.tryUsbWdtReset("ESP32-S3", ESP32S3_GPIO_STRAP_REG, ESP32S3_GPIO_STRAP_SPI_BOOT_MASK, ESP32S3_RTC_CNTL_OPTION1_REG, ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK);
|
|
1514
|
+
if (wdtResetUsed)
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
// Standard reset for all other cases
|
|
1518
|
+
if (this.isWebUSB()) {
|
|
1519
|
+
// WebUSB: Use longer delays for better compatibility
|
|
1520
|
+
await this.setRTSWebUSB(true); // EN->LOW
|
|
1521
|
+
await this.sleep(200);
|
|
1522
|
+
await this.setRTSWebUSB(false);
|
|
1523
|
+
await this.sleep(200);
|
|
1524
|
+
this.logger.debug("Hard reset (WebUSB).");
|
|
1525
|
+
}
|
|
1526
|
+
else {
|
|
1527
|
+
// Web Serial: Standard reset
|
|
1528
|
+
await this.setRTS(true); // EN->LOW
|
|
1529
|
+
await this.sleep(100);
|
|
1530
|
+
await this.setRTS(false);
|
|
1531
|
+
this.logger.debug("Hard reset.");
|
|
1532
|
+
}
|
|
343
1533
|
}
|
|
344
1534
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
345
1535
|
}
|
|
@@ -464,6 +1654,12 @@ export class ESPLoader extends EventTarget {
|
|
|
464
1654
|
else if ([2, 4].includes(data.length)) {
|
|
465
1655
|
statusLen = data.length;
|
|
466
1656
|
}
|
|
1657
|
+
else {
|
|
1658
|
+
// Default to 2-byte status if we can't determine
|
|
1659
|
+
// This prevents silent data corruption when statusLen would be 0
|
|
1660
|
+
statusLen = 2;
|
|
1661
|
+
this.logger.debug(`Unknown chip family, defaulting to 2-byte status (opcode: ${toHex(opcode)}, data.length: ${data.length})`);
|
|
1662
|
+
}
|
|
467
1663
|
}
|
|
468
1664
|
if (data.length < statusLen) {
|
|
469
1665
|
throw new Error("Didn't get enough status bytes");
|
|
@@ -512,76 +1708,169 @@ export class ESPLoader extends EventTarget {
|
|
|
512
1708
|
* @name readPacket
|
|
513
1709
|
* Generator to read SLIP packets from a serial port.
|
|
514
1710
|
* Yields one full SLIP packet at a time, raises exception on timeout or invalid data.
|
|
1711
|
+
*
|
|
1712
|
+
* Two implementations:
|
|
1713
|
+
* - Burst: CDC devices (Native USB) and CH343 - very fast processing
|
|
1714
|
+
* - Byte-by-byte: CH340, CP2102, and other USB-Serial adapters - stable fast processing
|
|
515
1715
|
*/
|
|
516
1716
|
async readPacket(timeout) {
|
|
517
1717
|
let partialPacket = null;
|
|
518
1718
|
let inEscape = false;
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1719
|
+
// CDC devices use burst processing, non-CDC use byte-by-byte
|
|
1720
|
+
if (this._isCDCDevice) {
|
|
1721
|
+
// Burst version: Process all available bytes in one pass for ultra-high-speed transfers
|
|
1722
|
+
// Used for: CDC devices (all platforms) and CH343
|
|
1723
|
+
const startTime = Date.now();
|
|
1724
|
+
while (true) {
|
|
1725
|
+
// Check abandon flag (for reset strategy timeout)
|
|
1726
|
+
if (this._abandonCurrentOperation) {
|
|
1727
|
+
throw new SlipReadError("Operation abandoned (reset strategy timeout)");
|
|
527
1728
|
}
|
|
528
|
-
|
|
529
|
-
|
|
1729
|
+
// Check timeout
|
|
1730
|
+
if (Date.now() - startTime > timeout) {
|
|
1731
|
+
const waitingFor = partialPacket === null ? "header" : "content";
|
|
1732
|
+
throw new SlipReadError("Timed out waiting for packet " + waitingFor);
|
|
1733
|
+
}
|
|
1734
|
+
// If no data available, wait a bit
|
|
1735
|
+
if (this._inputBufferAvailable === 0) {
|
|
530
1736
|
await sleep(1);
|
|
1737
|
+
continue;
|
|
531
1738
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
for (const b of readBytes) {
|
|
540
|
-
if (partialPacket === null) {
|
|
541
|
-
// waiting for packet header
|
|
542
|
-
if (b == 0xc0) {
|
|
543
|
-
partialPacket = [];
|
|
1739
|
+
// Process all available bytes without going back to outer loop
|
|
1740
|
+
// This is critical for handling high-speed burst transfers
|
|
1741
|
+
while (this._inputBufferAvailable > 0) {
|
|
1742
|
+
// Periodic timeout check to prevent hang on slow data
|
|
1743
|
+
if (Date.now() - startTime > timeout) {
|
|
1744
|
+
const waitingFor = partialPacket === null ? "header" : "content";
|
|
1745
|
+
throw new SlipReadError("Timed out waiting for packet " + waitingFor);
|
|
544
1746
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
1747
|
+
const b = this._readByte();
|
|
1748
|
+
if (partialPacket === null) {
|
|
1749
|
+
// waiting for packet header
|
|
1750
|
+
if (b == 0xc0) {
|
|
1751
|
+
partialPacket = [];
|
|
1752
|
+
}
|
|
1753
|
+
else {
|
|
1754
|
+
if (this.debug) {
|
|
1755
|
+
this.logger.debug("Read invalid data: " + toHex(b));
|
|
1756
|
+
this.logger.debug("Remaining data in serial buffer: " +
|
|
1757
|
+
hexFormatter(this._inputBuffer));
|
|
1758
|
+
}
|
|
1759
|
+
throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
|
|
550
1760
|
}
|
|
551
|
-
throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
|
|
552
1761
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
1762
|
+
else if (inEscape) {
|
|
1763
|
+
// part-way through escape sequence
|
|
1764
|
+
inEscape = false;
|
|
1765
|
+
if (b == 0xdc) {
|
|
1766
|
+
partialPacket.push(0xc0);
|
|
1767
|
+
}
|
|
1768
|
+
else if (b == 0xdd) {
|
|
1769
|
+
partialPacket.push(0xdb);
|
|
1770
|
+
}
|
|
1771
|
+
else {
|
|
1772
|
+
if (this.debug) {
|
|
1773
|
+
this.logger.debug("Read invalid data: " + toHex(b));
|
|
1774
|
+
this.logger.debug("Remaining data in serial buffer: " +
|
|
1775
|
+
hexFormatter(this._inputBuffer));
|
|
1776
|
+
}
|
|
1777
|
+
throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
|
|
1778
|
+
}
|
|
559
1779
|
}
|
|
560
|
-
else if (b ==
|
|
561
|
-
|
|
1780
|
+
else if (b == 0xdb) {
|
|
1781
|
+
// start of escape sequence
|
|
1782
|
+
inEscape = true;
|
|
1783
|
+
}
|
|
1784
|
+
else if (b == 0xc0) {
|
|
1785
|
+
// end of packet
|
|
1786
|
+
if (this.debug)
|
|
1787
|
+
this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
|
|
1788
|
+
// Compact buffer periodically to prevent memory growth
|
|
1789
|
+
this._compactInputBuffer();
|
|
1790
|
+
return partialPacket;
|
|
562
1791
|
}
|
|
563
1792
|
else {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
this.logger.debug("Remaining data in serial buffer: " +
|
|
567
|
-
hexFormatter(this._inputBuffer));
|
|
568
|
-
}
|
|
569
|
-
throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
|
|
1793
|
+
// normal byte in packet
|
|
1794
|
+
partialPacket.push(b);
|
|
570
1795
|
}
|
|
571
1796
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
else {
|
|
1800
|
+
// Byte-by-byte version: Stable for non CDC USB-Serial adapters (CH340, CP2102, etc.)
|
|
1801
|
+
let readBytes = [];
|
|
1802
|
+
while (true) {
|
|
1803
|
+
// Check abandon flag (for reset strategy timeout)
|
|
1804
|
+
if (this._abandonCurrentOperation) {
|
|
1805
|
+
throw new SlipReadError("Operation abandoned (reset strategy timeout)");
|
|
575
1806
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1807
|
+
const stamp = Date.now();
|
|
1808
|
+
readBytes = [];
|
|
1809
|
+
while (Date.now() - stamp < timeout) {
|
|
1810
|
+
if (this._inputBufferAvailable > 0) {
|
|
1811
|
+
readBytes.push(this._readByte());
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1814
|
+
else {
|
|
1815
|
+
// Reduced sleep time for faster response during high-speed transfers
|
|
1816
|
+
await sleep(1);
|
|
1817
|
+
}
|
|
581
1818
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1819
|
+
if (readBytes.length == 0) {
|
|
1820
|
+
const waitingFor = partialPacket === null ? "header" : "content";
|
|
1821
|
+
throw new SlipReadError("Timed out waiting for packet " + waitingFor);
|
|
1822
|
+
}
|
|
1823
|
+
if (this.debug)
|
|
1824
|
+
this.logger.debug("Read " + readBytes.length + " bytes: " + hexFormatter(readBytes));
|
|
1825
|
+
for (const b of readBytes) {
|
|
1826
|
+
if (partialPacket === null) {
|
|
1827
|
+
// waiting for packet header
|
|
1828
|
+
if (b == 0xc0) {
|
|
1829
|
+
partialPacket = [];
|
|
1830
|
+
}
|
|
1831
|
+
else {
|
|
1832
|
+
if (this.debug) {
|
|
1833
|
+
this.logger.debug("Read invalid data: " + toHex(b));
|
|
1834
|
+
this.logger.debug("Remaining data in serial buffer: " +
|
|
1835
|
+
hexFormatter(this._inputBuffer));
|
|
1836
|
+
}
|
|
1837
|
+
throw new SlipReadError("Invalid head of packet (" + toHex(b) + ")");
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
else if (inEscape) {
|
|
1841
|
+
// part-way through escape sequence
|
|
1842
|
+
inEscape = false;
|
|
1843
|
+
if (b == 0xdc) {
|
|
1844
|
+
partialPacket.push(0xc0);
|
|
1845
|
+
}
|
|
1846
|
+
else if (b == 0xdd) {
|
|
1847
|
+
partialPacket.push(0xdb);
|
|
1848
|
+
}
|
|
1849
|
+
else {
|
|
1850
|
+
if (this.debug) {
|
|
1851
|
+
this.logger.debug("Read invalid data: " + toHex(b));
|
|
1852
|
+
this.logger.debug("Remaining data in serial buffer: " +
|
|
1853
|
+
hexFormatter(this._inputBuffer));
|
|
1854
|
+
}
|
|
1855
|
+
throw new SlipReadError("Invalid SLIP escape (0xdb, " + toHex(b) + ")");
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
else if (b == 0xdb) {
|
|
1859
|
+
// start of escape sequence
|
|
1860
|
+
inEscape = true;
|
|
1861
|
+
}
|
|
1862
|
+
else if (b == 0xc0) {
|
|
1863
|
+
// end of packet
|
|
1864
|
+
if (this.debug)
|
|
1865
|
+
this.logger.debug("Received full packet: " + hexFormatter(partialPacket));
|
|
1866
|
+
// Compact buffer periodically to prevent memory growth
|
|
1867
|
+
this._compactInputBuffer();
|
|
1868
|
+
return partialPacket;
|
|
1869
|
+
}
|
|
1870
|
+
else {
|
|
1871
|
+
// normal byte in packet
|
|
1872
|
+
partialPacket.push(b);
|
|
1873
|
+
}
|
|
585
1874
|
}
|
|
586
1875
|
}
|
|
587
1876
|
}
|
|
@@ -613,7 +1902,7 @@ export class ESPLoader extends EventTarget {
|
|
|
613
1902
|
throw new Error(`Invalid (unsupported) command ${toHex(opcode)}`);
|
|
614
1903
|
}
|
|
615
1904
|
}
|
|
616
|
-
throw "Response doesn't match request";
|
|
1905
|
+
throw new Error("Response doesn't match request");
|
|
617
1906
|
}
|
|
618
1907
|
/**
|
|
619
1908
|
* @name checksum
|
|
@@ -626,9 +1915,6 @@ export class ESPLoader extends EventTarget {
|
|
|
626
1915
|
return state;
|
|
627
1916
|
}
|
|
628
1917
|
async setBaudrate(baud) {
|
|
629
|
-
if (this.chipFamily == CHIP_FAMILY_ESP8266) {
|
|
630
|
-
throw new Error("Changing baud rate is not supported on the ESP8266");
|
|
631
|
-
}
|
|
632
1918
|
try {
|
|
633
1919
|
// Send ESP_ROM_BAUD(115200) as the old one if running STUB otherwise 0
|
|
634
1920
|
const buffer = pack("<II", baud, this.IS_STUB ? ESP_ROM_BAUD : 0);
|
|
@@ -648,10 +1934,10 @@ export class ESPLoader extends EventTarget {
|
|
|
648
1934
|
await sleep(SYNC_TIMEOUT);
|
|
649
1935
|
// Track current baudrate for reconnect
|
|
650
1936
|
if (this._parent) {
|
|
651
|
-
this._parent.
|
|
1937
|
+
this._parent.currentBaudRate = baud;
|
|
652
1938
|
}
|
|
653
1939
|
else {
|
|
654
|
-
this.
|
|
1940
|
+
this.currentBaudRate = baud;
|
|
655
1941
|
}
|
|
656
1942
|
// Warn if baudrate exceeds USB-Serial chip capability
|
|
657
1943
|
const maxBaud = this._parent
|
|
@@ -665,6 +1951,8 @@ export class ESPLoader extends EventTarget {
|
|
|
665
1951
|
}
|
|
666
1952
|
async reconfigurePort(baud) {
|
|
667
1953
|
var _a;
|
|
1954
|
+
// Block new writes during the entire reconfiguration (all paths)
|
|
1955
|
+
this._isReconfiguring = true;
|
|
668
1956
|
try {
|
|
669
1957
|
// Wait for pending writes to complete
|
|
670
1958
|
try {
|
|
@@ -673,8 +1961,30 @@ export class ESPLoader extends EventTarget {
|
|
|
673
1961
|
catch (err) {
|
|
674
1962
|
this.logger.debug(`Pending write error during reconfigure: ${err}`);
|
|
675
1963
|
}
|
|
676
|
-
//
|
|
677
|
-
this.
|
|
1964
|
+
// WebUSB: Check if we should use setBaudRate() or close/reopen
|
|
1965
|
+
if (this.isWebUSB()) {
|
|
1966
|
+
const portInfo = this.port.getInfo();
|
|
1967
|
+
const isCH343 = portInfo.usbVendorId === 0x1a86 && portInfo.usbProductId === 0x55d3;
|
|
1968
|
+
// CH343 is a CDC device and MUST use close/reopen
|
|
1969
|
+
// Other chips (CH340, CP2102, FTDI) MUST use setBaudRate()
|
|
1970
|
+
if (!isCH343 &&
|
|
1971
|
+
typeof this.port.setBaudRate === "function") {
|
|
1972
|
+
// this.logger.log(
|
|
1973
|
+
// `[WebUSB] Changing baudrate to ${baud} using setBaudRate()...`,
|
|
1974
|
+
// );
|
|
1975
|
+
await this.port.setBaudRate(baud);
|
|
1976
|
+
// this.logger.log(`[WebUSB] Baudrate changed to ${baud}`);
|
|
1977
|
+
// Give the chip time to adjust to new baudrate
|
|
1978
|
+
await sleep(100);
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
else if (isCH343) {
|
|
1982
|
+
// this.logger.log(
|
|
1983
|
+
// `[WebUSB] CH343 detected - using close/reopen for baudrate change`,
|
|
1984
|
+
// );
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
// Web Serial or CH343: Close and reopen port
|
|
678
1988
|
// Release persistent writer before closing
|
|
679
1989
|
if (this._writer) {
|
|
680
1990
|
try {
|
|
@@ -693,120 +2003,54 @@ export class ESPLoader extends EventTarget {
|
|
|
693
2003
|
await this.port.close();
|
|
694
2004
|
// Reopen Port
|
|
695
2005
|
await this.port.open({ baudRate: baud });
|
|
696
|
-
// Port is now open - allow writes again
|
|
697
|
-
this._isReconfiguring = false;
|
|
698
2006
|
// Clear buffer again
|
|
699
2007
|
await this.flushSerialBuffers();
|
|
700
2008
|
// Restart Readloop
|
|
701
2009
|
this.readLoop();
|
|
702
2010
|
}
|
|
703
2011
|
catch (e) {
|
|
704
|
-
this._isReconfiguring = false;
|
|
705
2012
|
this.logger.error(`Reconfigure port error: ${e}`);
|
|
706
2013
|
throw new Error(`Unable to change the baud rate to ${baud}: ${e}`);
|
|
707
2014
|
}
|
|
2015
|
+
finally {
|
|
2016
|
+
// Always reset flag, even on error or early return
|
|
2017
|
+
this._isReconfiguring = false;
|
|
2018
|
+
}
|
|
708
2019
|
}
|
|
709
2020
|
/**
|
|
710
|
-
* @name
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*/
|
|
714
|
-
async
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
name: "USB-JTAG/Serial",
|
|
727
|
-
fn: async () => await this.hardResetUSBJTAGSerial(),
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
// Strategy 2: Classic reset (for USB-to-Serial bridges)
|
|
731
|
-
resetStrategies.push({
|
|
732
|
-
name: "Classic",
|
|
733
|
-
fn: async () => await this.hardResetClassic(),
|
|
734
|
-
});
|
|
735
|
-
// Strategy 3: If USB-JTAG/Serial was not tried yet, try it as fallback
|
|
736
|
-
if (!isUSBJTAGSerial && !isEspressifUSB) {
|
|
737
|
-
resetStrategies.push({
|
|
738
|
-
name: "USB-JTAG/Serial (fallback)",
|
|
739
|
-
fn: async () => await this.hardResetUSBJTAGSerial(),
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
let lastError = null;
|
|
743
|
-
// Try each reset strategy
|
|
744
|
-
for (const strategy of resetStrategies) {
|
|
2021
|
+
* @name syncWithTimeout
|
|
2022
|
+
* Sync with timeout that can be abandoned (for reset strategy loop)
|
|
2023
|
+
* This is internally time-bounded and checks the abandon flag
|
|
2024
|
+
*/
|
|
2025
|
+
async syncWithTimeout(timeoutMs) {
|
|
2026
|
+
const startTime = Date.now();
|
|
2027
|
+
for (let i = 0; i < 5; i++) {
|
|
2028
|
+
// Check if we've exceeded the timeout
|
|
2029
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
2030
|
+
return false;
|
|
2031
|
+
}
|
|
2032
|
+
// Check abandon flag
|
|
2033
|
+
if (this._abandonCurrentOperation) {
|
|
2034
|
+
return false;
|
|
2035
|
+
}
|
|
2036
|
+
this._clearInputBuffer();
|
|
745
2037
|
try {
|
|
746
|
-
this.
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
continue;
|
|
2038
|
+
const response = await this._sync();
|
|
2039
|
+
if (response) {
|
|
2040
|
+
await sleep(SYNC_TIMEOUT);
|
|
2041
|
+
return true;
|
|
751
2042
|
}
|
|
752
|
-
|
|
753
|
-
// Try to sync after reset
|
|
754
|
-
await this.sync();
|
|
755
|
-
// If we get here, sync succeeded
|
|
756
|
-
this.logger.log(`Connected successfully with ${strategy.name} reset.`);
|
|
757
|
-
return;
|
|
2043
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
758
2044
|
}
|
|
759
|
-
catch (
|
|
760
|
-
|
|
761
|
-
this.
|
|
762
|
-
|
|
763
|
-
if (!this.connected || !this.port.writable) {
|
|
764
|
-
this.logger.log(`Port disconnected during reset attempt`);
|
|
765
|
-
break;
|
|
2045
|
+
catch (e) {
|
|
2046
|
+
// Check abandon flag after error
|
|
2047
|
+
if (this._abandonCurrentOperation) {
|
|
2048
|
+
return false;
|
|
766
2049
|
}
|
|
767
|
-
// Clear buffers before trying next strategy
|
|
768
|
-
this._inputBuffer.length = 0;
|
|
769
|
-
await this.drainInputBuffer(200);
|
|
770
|
-
await this.flushSerialBuffers();
|
|
771
2050
|
}
|
|
2051
|
+
await sleep(SYNC_TIMEOUT);
|
|
772
2052
|
}
|
|
773
|
-
|
|
774
|
-
throw new Error(`Couldn't sync to ESP. Try resetting manually. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
|
|
775
|
-
}
|
|
776
|
-
/**
|
|
777
|
-
* @name hardResetUSBJTAGSerial
|
|
778
|
-
* USB-JTAG/Serial reset sequence for ESP32-C3, ESP32-S3, ESP32-C6, etc.
|
|
779
|
-
*/
|
|
780
|
-
async hardResetUSBJTAGSerial() {
|
|
781
|
-
await this.setRTS(false);
|
|
782
|
-
await this.setDTR(false); // Idle
|
|
783
|
-
await this.sleep(100);
|
|
784
|
-
await this.setDTR(true); // Set IO0
|
|
785
|
-
await this.setRTS(false);
|
|
786
|
-
await this.sleep(100);
|
|
787
|
-
await this.setRTS(true); // Reset. Calls inverted to go through (1,1) instead of (0,0)
|
|
788
|
-
await this.setDTR(false);
|
|
789
|
-
await this.setRTS(true); // RTS set as Windows only propagates DTR on RTS setting
|
|
790
|
-
await this.sleep(100);
|
|
791
|
-
await this.setDTR(false);
|
|
792
|
-
await this.setRTS(false); // Chip out of reset
|
|
793
|
-
// Wait for chip to boot into bootloader
|
|
794
|
-
await this.sleep(200);
|
|
795
|
-
}
|
|
796
|
-
/**
|
|
797
|
-
* @name hardResetClassic
|
|
798
|
-
* Classic reset sequence for USB-to-Serial bridge chips (CH340, CP2102, etc.)
|
|
799
|
-
*/
|
|
800
|
-
async hardResetClassic() {
|
|
801
|
-
await this.setDTR(false); // IO0=HIGH
|
|
802
|
-
await this.setRTS(true); // EN=LOW, chip in reset
|
|
803
|
-
await this.sleep(100);
|
|
804
|
-
await this.setDTR(true); // IO0=LOW
|
|
805
|
-
await this.setRTS(false); // EN=HIGH, chip out of reset
|
|
806
|
-
await this.sleep(50);
|
|
807
|
-
await this.setDTR(false); // IO0=HIGH, done
|
|
808
|
-
// Wait for chip to boot into bootloader
|
|
809
|
-
await this.sleep(200);
|
|
2053
|
+
return false;
|
|
810
2054
|
}
|
|
811
2055
|
/**
|
|
812
2056
|
* @name sync
|
|
@@ -815,7 +2059,7 @@ export class ESPLoader extends EventTarget {
|
|
|
815
2059
|
*/
|
|
816
2060
|
async sync() {
|
|
817
2061
|
for (let i = 0; i < 5; i++) {
|
|
818
|
-
this.
|
|
2062
|
+
this._clearInputBuffer();
|
|
819
2063
|
const response = await this._sync();
|
|
820
2064
|
if (response) {
|
|
821
2065
|
await sleep(SYNC_TIMEOUT);
|
|
@@ -839,8 +2083,10 @@ export class ESPLoader extends EventTarget {
|
|
|
839
2083
|
return true;
|
|
840
2084
|
}
|
|
841
2085
|
}
|
|
842
|
-
catch {
|
|
843
|
-
|
|
2086
|
+
catch (e) {
|
|
2087
|
+
if (this.debug) {
|
|
2088
|
+
this.logger.debug(`Sync attempt ${i + 1} failed: ${e}`);
|
|
2089
|
+
}
|
|
844
2090
|
}
|
|
845
2091
|
}
|
|
846
2092
|
return false;
|
|
@@ -975,7 +2221,7 @@ export class ESPLoader extends EventTarget {
|
|
|
975
2221
|
await this.checkCommand(ESP_SPI_ATTACH, new Array(8).fill(0));
|
|
976
2222
|
}
|
|
977
2223
|
const numBlocks = Math.floor((size + flashWriteSize - 1) / flashWriteSize);
|
|
978
|
-
if (this.chipFamily == CHIP_FAMILY_ESP8266) {
|
|
2224
|
+
if (this.chipFamily == CHIP_FAMILY_ESP8266 && !this.IS_STUB) {
|
|
979
2225
|
eraseSize = this.getEraseSize(offset, size);
|
|
980
2226
|
}
|
|
981
2227
|
else {
|
|
@@ -1315,12 +2561,19 @@ export class ESPLoader extends EventTarget {
|
|
|
1315
2561
|
await this._writer.write(new Uint8Array(data));
|
|
1316
2562
|
}, async () => {
|
|
1317
2563
|
// Previous write failed, but still attempt this write
|
|
2564
|
+
this.logger.debug("Previous write failed, attempting recovery for current write");
|
|
1318
2565
|
if (!this.port.writable) {
|
|
1319
2566
|
throw new Error("Port became unavailable during write");
|
|
1320
2567
|
}
|
|
1321
2568
|
// Writer was likely cleaned up by previous error, create new one
|
|
1322
2569
|
if (!this._writer) {
|
|
1323
|
-
|
|
2570
|
+
try {
|
|
2571
|
+
this._writer = this.port.writable.getWriter();
|
|
2572
|
+
}
|
|
2573
|
+
catch (err) {
|
|
2574
|
+
this.logger.debug(`Failed to get writer in recovery: ${err}`);
|
|
2575
|
+
throw new Error("Cannot acquire writer lock");
|
|
2576
|
+
}
|
|
1324
2577
|
}
|
|
1325
2578
|
await this._writer.write(new Uint8Array(data));
|
|
1326
2579
|
})
|
|
@@ -1331,7 +2584,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1331
2584
|
try {
|
|
1332
2585
|
this._writer.releaseLock();
|
|
1333
2586
|
}
|
|
1334
|
-
catch
|
|
2587
|
+
catch {
|
|
1335
2588
|
// Ignore release errors
|
|
1336
2589
|
}
|
|
1337
2590
|
this._writer = undefined;
|
|
@@ -1351,51 +2604,239 @@ export class ESPLoader extends EventTarget {
|
|
|
1351
2604
|
this.logger.debug("Port already closed, skipping disconnect");
|
|
1352
2605
|
return;
|
|
1353
2606
|
}
|
|
2607
|
+
// Wait for pending writes to complete
|
|
1354
2608
|
try {
|
|
1355
|
-
|
|
2609
|
+
await this._writeChain;
|
|
2610
|
+
}
|
|
2611
|
+
catch (err) {
|
|
2612
|
+
this.logger.debug(`Pending write error during disconnect: ${err}`);
|
|
2613
|
+
}
|
|
2614
|
+
// Release persistent writer before closing
|
|
2615
|
+
if (this._writer) {
|
|
1356
2616
|
try {
|
|
1357
|
-
await this.
|
|
2617
|
+
await this._writer.close();
|
|
2618
|
+
this._writer.releaseLock();
|
|
1358
2619
|
}
|
|
1359
2620
|
catch (err) {
|
|
1360
|
-
this.logger.debug(`
|
|
2621
|
+
this.logger.debug(`Writer close/release error: ${err}`);
|
|
1361
2622
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
2623
|
+
this._writer = undefined;
|
|
2624
|
+
}
|
|
2625
|
+
else {
|
|
2626
|
+
// No persistent writer exists, close stream directly
|
|
2627
|
+
// This path is taken when no writes have been queued
|
|
2628
|
+
try {
|
|
2629
|
+
const writer = this.port.writable.getWriter();
|
|
2630
|
+
await writer.close();
|
|
2631
|
+
writer.releaseLock();
|
|
2632
|
+
}
|
|
2633
|
+
catch (err) {
|
|
2634
|
+
this.logger.debug(`Direct writer close error: ${err}`);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
await new Promise((resolve) => {
|
|
2638
|
+
if (!this._reader) {
|
|
2639
|
+
resolve(undefined);
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
// Set a timeout to prevent hanging (important for node-usb)
|
|
2643
|
+
const timeout = setTimeout(() => {
|
|
2644
|
+
this.logger.debug("Disconnect timeout - forcing resolution");
|
|
2645
|
+
resolve(undefined);
|
|
2646
|
+
}, 1000);
|
|
2647
|
+
this.addEventListener("disconnect", () => {
|
|
2648
|
+
clearTimeout(timeout);
|
|
2649
|
+
resolve(undefined);
|
|
2650
|
+
}, { once: true });
|
|
2651
|
+
// Only cancel if reader is still active
|
|
2652
|
+
try {
|
|
2653
|
+
this._reader.cancel();
|
|
2654
|
+
}
|
|
2655
|
+
catch (err) {
|
|
2656
|
+
this.logger.debug(`Reader cancel error: ${err}`);
|
|
2657
|
+
// Reader already released, resolve immediately
|
|
2658
|
+
clearTimeout(timeout);
|
|
2659
|
+
resolve(undefined);
|
|
2660
|
+
}
|
|
2661
|
+
});
|
|
2662
|
+
this.connected = false;
|
|
2663
|
+
// Close the port (important for node-usb adapter)
|
|
2664
|
+
try {
|
|
2665
|
+
await this.port.close();
|
|
2666
|
+
this.logger.debug("Port closed successfully");
|
|
2667
|
+
}
|
|
2668
|
+
catch (err) {
|
|
2669
|
+
this.logger.debug(`Port close error: ${err}`);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* @name releaseReaderWriter
|
|
2674
|
+
* Release reader and writer locks without closing the port
|
|
2675
|
+
* Used when switching to console mode
|
|
2676
|
+
*/
|
|
2677
|
+
async releaseReaderWriter() {
|
|
2678
|
+
if (this._parent) {
|
|
2679
|
+
await this._parent.releaseReaderWriter();
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
// Check if device is in JTAG mode and needs reset to boot into firmware
|
|
2683
|
+
const didReconnect = await this._resetToFirmwareIfNeeded();
|
|
2684
|
+
// If we reconnected for console, the reader/writer are already released and restarted
|
|
2685
|
+
if (didReconnect) {
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
// Wait for pending writes to complete
|
|
2689
|
+
try {
|
|
2690
|
+
await this._writeChain;
|
|
2691
|
+
}
|
|
2692
|
+
catch (err) {
|
|
2693
|
+
this.logger.debug(`Pending write error during release: ${err}`);
|
|
2694
|
+
}
|
|
2695
|
+
// Release writer
|
|
2696
|
+
if (this._writer) {
|
|
2697
|
+
try {
|
|
2698
|
+
this._writer.releaseLock();
|
|
2699
|
+
this.logger.debug("Writer released");
|
|
2700
|
+
}
|
|
2701
|
+
catch (err) {
|
|
2702
|
+
this.logger.debug(`Writer release error: ${err}`);
|
|
2703
|
+
}
|
|
2704
|
+
this._writer = undefined;
|
|
2705
|
+
}
|
|
2706
|
+
// Cancel and release reader
|
|
2707
|
+
if (this._reader) {
|
|
2708
|
+
const reader = this._reader;
|
|
2709
|
+
try {
|
|
2710
|
+
// Suppress disconnect event during console mode switching
|
|
2711
|
+
this._suppressDisconnect = true;
|
|
2712
|
+
await reader.cancel();
|
|
2713
|
+
this.logger.debug("Reader cancelled");
|
|
2714
|
+
}
|
|
2715
|
+
catch (err) {
|
|
2716
|
+
this.logger.debug(`Reader cancel error: ${err}`);
|
|
2717
|
+
}
|
|
2718
|
+
finally {
|
|
1366
2719
|
try {
|
|
1367
|
-
|
|
1368
|
-
this._writer.releaseLock();
|
|
2720
|
+
reader.releaseLock();
|
|
1369
2721
|
}
|
|
1370
2722
|
catch (err) {
|
|
1371
|
-
this.logger.debug(`
|
|
2723
|
+
this.logger.debug(`Reader release error: ${err}`);
|
|
1372
2724
|
}
|
|
1373
|
-
this._writer = undefined;
|
|
1374
2725
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
2726
|
+
if (this._reader === reader) {
|
|
2727
|
+
this._reader = undefined;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
/**
|
|
2732
|
+
* @name resetToFirmware
|
|
2733
|
+
* Public method to reset device from bootloader to firmware for console mode
|
|
2734
|
+
* Automatically detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
|
|
2735
|
+
* @returns true if reset was performed, false if not needed
|
|
2736
|
+
*/
|
|
2737
|
+
async resetToFirmware() {
|
|
2738
|
+
return await this._resetToFirmwareIfNeeded();
|
|
2739
|
+
}
|
|
2740
|
+
/**
|
|
2741
|
+
* @name enterConsoleMode
|
|
2742
|
+
* Prepare device for console mode by resetting to firmware
|
|
2743
|
+
* Handles both USB-JTAG/OTG devices (closes port) and external serial chips (keeps port open)
|
|
2744
|
+
* @returns true if port was closed (USB-JTAG), false if port stays open (serial chip)
|
|
2745
|
+
*/
|
|
2746
|
+
async enterConsoleMode() {
|
|
2747
|
+
// Set console mode flag
|
|
2748
|
+
this._consoleMode = true;
|
|
2749
|
+
// Check device type
|
|
2750
|
+
const isUsbJtag = this.isUsbJtagOrOtg === true;
|
|
2751
|
+
if (isUsbJtag) {
|
|
2752
|
+
// USB-JTAG/OTG devices: Use watchdog reset which closes port
|
|
2753
|
+
const wasReset = await this._resetToFirmwareIfNeeded();
|
|
2754
|
+
return wasReset; // true = port closed, caller must reopen
|
|
2755
|
+
}
|
|
2756
|
+
else {
|
|
2757
|
+
// External serial chip devices: Release locks and do simple reset
|
|
2758
|
+
try {
|
|
2759
|
+
await this.releaseReaderWriter();
|
|
2760
|
+
await this.sleep(100);
|
|
2761
|
+
}
|
|
2762
|
+
catch (err) {
|
|
2763
|
+
this.logger.debug(`Failed to release locks: ${err}`);
|
|
2764
|
+
}
|
|
2765
|
+
// Hardware reset to firmware mode (IO0=HIGH)
|
|
2766
|
+
try {
|
|
2767
|
+
await this.hardReset(false);
|
|
2768
|
+
this.logger.log("Device reset to firmware mode");
|
|
2769
|
+
}
|
|
2770
|
+
catch (err) {
|
|
2771
|
+
this.logger.debug(`Could not reset device: ${err}`);
|
|
2772
|
+
}
|
|
2773
|
+
return false; // Port stays open
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* @name _resetToFirmwareIfNeeded
|
|
2778
|
+
* Reset device from bootloader to firmware when switching to console mode
|
|
2779
|
+
* Detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
|
|
2780
|
+
* @returns true if reconnect was performed, false otherwise
|
|
2781
|
+
*/
|
|
2782
|
+
async _resetToFirmwareIfNeeded() {
|
|
2783
|
+
try {
|
|
2784
|
+
// Check if device is using USB-JTAG/Serial or USB-OTG
|
|
2785
|
+
// Value should already be set during main() connection
|
|
2786
|
+
// Use getter to access parent's value if this is a stub
|
|
2787
|
+
const needsReset = this.isUsbJtagOrOtg === true;
|
|
2788
|
+
if (needsReset) {
|
|
2789
|
+
const resetMethod = this.chipFamily === CHIP_FAMILY_ESP32S2 ||
|
|
2790
|
+
this.chipFamily === CHIP_FAMILY_ESP32S3
|
|
2791
|
+
? "USB-JTAG/Serial or USB-OTG"
|
|
2792
|
+
: "USB-JTAG/Serial";
|
|
2793
|
+
this.logger.log(`Resetting ${this.chipFamily} (${resetMethod}) to boot into firmware...`);
|
|
2794
|
+
// Set console mode flag before reset to prevent subsequent hardReset calls
|
|
2795
|
+
this._consoleMode = true;
|
|
2796
|
+
// For S2/S3: Clear force download boot mask before WDT reset
|
|
2797
|
+
if (this.chipFamily === CHIP_FAMILY_ESP32S2 ||
|
|
2798
|
+
this.chipFamily === CHIP_FAMILY_ESP32S3) {
|
|
2799
|
+
const OPTION1_REG = this.chipFamily === CHIP_FAMILY_ESP32S2
|
|
2800
|
+
? ESP32S2_RTC_CNTL_OPTION1_REG
|
|
2801
|
+
: ESP32S3_RTC_CNTL_OPTION1_REG;
|
|
2802
|
+
const FORCE_DOWNLOAD_BOOT_MASK = this.chipFamily === CHIP_FAMILY_ESP32S2
|
|
2803
|
+
? ESP32S2_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK
|
|
2804
|
+
: ESP32S3_RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK;
|
|
2805
|
+
try {
|
|
2806
|
+
// Clear force download boot mode to avoid chip being stuck in download mode
|
|
2807
|
+
await this.writeRegister(OPTION1_REG, 0, FORCE_DOWNLOAD_BOOT_MASK, 0);
|
|
2808
|
+
this.logger.debug("Cleared force download boot mask");
|
|
2809
|
+
}
|
|
2810
|
+
catch (err) {
|
|
2811
|
+
this.logger.debug(`Expected error clearing force download boot mask: ${err}`);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
// Perform watchdog reset to reboot into firmware
|
|
1378
2815
|
try {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
writer.releaseLock();
|
|
2816
|
+
await this.rtcWdtResetChipSpecific();
|
|
2817
|
+
this.logger.debug("Watchdog reset triggered successfully");
|
|
1382
2818
|
}
|
|
1383
2819
|
catch (err) {
|
|
1384
|
-
|
|
2820
|
+
// Error is expected - device resets before responding
|
|
2821
|
+
this.logger.debug(`Watchdog reset initiated (connection lost as expected: ${err})`);
|
|
1385
2822
|
}
|
|
2823
|
+
// Wait for device to fully boot into firmware
|
|
2824
|
+
this.logger.log("Waiting for device to boot into firmware...");
|
|
2825
|
+
await this.sleep(1000);
|
|
2826
|
+
// After WDT reset, streams are dead/locked - don't try to manipulate them
|
|
2827
|
+
// Just mark everything as disconnected and let browser clean up
|
|
2828
|
+
this.connected = false;
|
|
2829
|
+
this._writer = undefined;
|
|
2830
|
+
this._reader = undefined;
|
|
2831
|
+
this.logger.debug("Device reset to firmware mode (port closed)");
|
|
2832
|
+
return true;
|
|
1386
2833
|
}
|
|
1387
|
-
await new Promise((resolve) => {
|
|
1388
|
-
if (!this._reader) {
|
|
1389
|
-
resolve(undefined);
|
|
1390
|
-
}
|
|
1391
|
-
this.addEventListener("disconnect", resolve, { once: true });
|
|
1392
|
-
this._reader.cancel();
|
|
1393
|
-
});
|
|
1394
|
-
this.connected = false;
|
|
1395
2834
|
}
|
|
1396
|
-
|
|
1397
|
-
this.
|
|
2835
|
+
catch (err) {
|
|
2836
|
+
this.logger.debug(`Could not reset device to firmware mode: ${err}`);
|
|
2837
|
+
// Continue anyway - console mode might still work
|
|
1398
2838
|
}
|
|
2839
|
+
return false;
|
|
1399
2840
|
}
|
|
1400
2841
|
/**
|
|
1401
2842
|
* @name reconnectAndResume
|
|
@@ -1408,8 +2849,10 @@ export class ESPLoader extends EventTarget {
|
|
|
1408
2849
|
}
|
|
1409
2850
|
try {
|
|
1410
2851
|
this.logger.log("Reconnecting serial port...");
|
|
2852
|
+
const savedBaudRate = this.currentBaudRate;
|
|
1411
2853
|
this.connected = false;
|
|
1412
2854
|
this.__inputBuffer = [];
|
|
2855
|
+
this.__inputBufferReadIndex = 0;
|
|
1413
2856
|
// Wait for pending writes to complete
|
|
1414
2857
|
try {
|
|
1415
2858
|
await this._writeChain;
|
|
@@ -1442,7 +2885,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1442
2885
|
// Close port
|
|
1443
2886
|
try {
|
|
1444
2887
|
await this.port.close();
|
|
1445
|
-
this.logger.
|
|
2888
|
+
this.logger.debug("Port closed");
|
|
1446
2889
|
}
|
|
1447
2890
|
catch (err) {
|
|
1448
2891
|
this.logger.debug(`Port close error: ${err}`);
|
|
@@ -1452,6 +2895,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1452
2895
|
try {
|
|
1453
2896
|
await this.port.open({ baudRate: ESP_ROM_BAUD });
|
|
1454
2897
|
this.connected = true;
|
|
2898
|
+
this.currentBaudRate = ESP_ROM_BAUD;
|
|
1455
2899
|
}
|
|
1456
2900
|
catch (err) {
|
|
1457
2901
|
throw new Error(`Failed to open port: ${err}`);
|
|
@@ -1472,6 +2916,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1472
2916
|
await this.hardReset(true);
|
|
1473
2917
|
if (!this._parent) {
|
|
1474
2918
|
this.__inputBuffer = [];
|
|
2919
|
+
this.__inputBufferReadIndex = 0;
|
|
1475
2920
|
this.__totalBytesRead = 0;
|
|
1476
2921
|
this.readLoop();
|
|
1477
2922
|
}
|
|
@@ -1492,17 +2937,17 @@ export class ESPLoader extends EventTarget {
|
|
|
1492
2937
|
const stubLoader = await this.runStub(true);
|
|
1493
2938
|
this.logger.debug("Stub loaded");
|
|
1494
2939
|
// Restore baudrate if it was changed
|
|
1495
|
-
if (
|
|
1496
|
-
await stubLoader.setBaudrate(
|
|
2940
|
+
if (savedBaudRate !== ESP_ROM_BAUD) {
|
|
2941
|
+
await stubLoader.setBaudrate(savedBaudRate);
|
|
1497
2942
|
// Verify port is still ready after baudrate change
|
|
1498
2943
|
if (!this.port.writable || !this.port.readable) {
|
|
1499
2944
|
throw new Error(`Port not ready after baudrate change (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
|
|
1500
2945
|
}
|
|
1501
2946
|
}
|
|
1502
|
-
//
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
2947
|
+
// The stub is now running on the chip
|
|
2948
|
+
// stubLoader has this instance as _parent, so all operations go through this
|
|
2949
|
+
// We just need to mark this instance as running stub code
|
|
2950
|
+
this.IS_STUB = true;
|
|
1506
2951
|
this.logger.debug("Reconnection successful");
|
|
1507
2952
|
}
|
|
1508
2953
|
catch (err) {
|
|
@@ -1511,6 +2956,103 @@ export class ESPLoader extends EventTarget {
|
|
|
1511
2956
|
throw err;
|
|
1512
2957
|
}
|
|
1513
2958
|
}
|
|
2959
|
+
/**
|
|
2960
|
+
* @name reconnectToBootloader
|
|
2961
|
+
* Close and reopen the port, then reset ESP to bootloader mode
|
|
2962
|
+
* This is needed after Improv or other operations that leave ESP in firmware mode
|
|
2963
|
+
*/
|
|
2964
|
+
async reconnectToBootloader() {
|
|
2965
|
+
if (this._parent) {
|
|
2966
|
+
await this._parent.reconnectToBootloader();
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
try {
|
|
2970
|
+
this.logger.log("Reconnecting to bootloader mode...");
|
|
2971
|
+
// Clear console mode flag when reconnecting to bootloader
|
|
2972
|
+
this._consoleMode = false;
|
|
2973
|
+
this.connected = false;
|
|
2974
|
+
this.__inputBuffer = [];
|
|
2975
|
+
this.__inputBufferReadIndex = 0;
|
|
2976
|
+
// Wait for pending writes to complete
|
|
2977
|
+
try {
|
|
2978
|
+
await this._writeChain;
|
|
2979
|
+
}
|
|
2980
|
+
catch (err) {
|
|
2981
|
+
this.logger.debug(`Pending write error during reconnect: ${err}`);
|
|
2982
|
+
}
|
|
2983
|
+
// Block new writes during port close/open
|
|
2984
|
+
this._isReconfiguring = true;
|
|
2985
|
+
// Release persistent writer
|
|
2986
|
+
if (this._writer) {
|
|
2987
|
+
try {
|
|
2988
|
+
this._writer.releaseLock();
|
|
2989
|
+
}
|
|
2990
|
+
catch (err) {
|
|
2991
|
+
this.logger.debug(`Writer release error during reconnect: ${err}`);
|
|
2992
|
+
}
|
|
2993
|
+
this._writer = undefined;
|
|
2994
|
+
}
|
|
2995
|
+
// Cancel reader
|
|
2996
|
+
if (this._reader) {
|
|
2997
|
+
try {
|
|
2998
|
+
await this._reader.cancel();
|
|
2999
|
+
}
|
|
3000
|
+
catch (err) {
|
|
3001
|
+
this.logger.debug(`Reader cancel error: ${err}`);
|
|
3002
|
+
}
|
|
3003
|
+
this._reader = undefined;
|
|
3004
|
+
}
|
|
3005
|
+
// Close port
|
|
3006
|
+
try {
|
|
3007
|
+
await this.port.close();
|
|
3008
|
+
this.logger.debug("Port closed");
|
|
3009
|
+
}
|
|
3010
|
+
catch (err) {
|
|
3011
|
+
this.logger.debug(`Port close error: ${err}`);
|
|
3012
|
+
}
|
|
3013
|
+
// Open the port
|
|
3014
|
+
this.logger.debug("Opening port...");
|
|
3015
|
+
try {
|
|
3016
|
+
await this.port.open({ baudRate: ESP_ROM_BAUD });
|
|
3017
|
+
this.connected = true;
|
|
3018
|
+
this.currentBaudRate = ESP_ROM_BAUD;
|
|
3019
|
+
}
|
|
3020
|
+
catch (err) {
|
|
3021
|
+
throw new Error(`Failed to open port: ${err}`);
|
|
3022
|
+
}
|
|
3023
|
+
// Verify port streams are available
|
|
3024
|
+
if (!this.port.readable || !this.port.writable) {
|
|
3025
|
+
throw new Error(`Port streams not available after open (readable: ${!!this.port.readable}, writable: ${!!this.port.writable})`);
|
|
3026
|
+
}
|
|
3027
|
+
// Port is now open and ready - allow writes for initialization
|
|
3028
|
+
this._isReconfiguring = false;
|
|
3029
|
+
// Reset chip info and stub state
|
|
3030
|
+
this.__chipFamily = undefined;
|
|
3031
|
+
this.chipName = "Unknown Chip";
|
|
3032
|
+
this.chipRevision = null;
|
|
3033
|
+
this.chipVariant = null;
|
|
3034
|
+
this.IS_STUB = false;
|
|
3035
|
+
// Start read loop
|
|
3036
|
+
if (!this._parent) {
|
|
3037
|
+
this.__inputBuffer = [];
|
|
3038
|
+
this.__inputBufferReadIndex = 0;
|
|
3039
|
+
this.__totalBytesRead = 0;
|
|
3040
|
+
this.readLoop();
|
|
3041
|
+
}
|
|
3042
|
+
// Wait for readLoop to start
|
|
3043
|
+
await sleep(100);
|
|
3044
|
+
// Reset to bootloader mode using multiple strategies
|
|
3045
|
+
await this.connectWithResetStrategies();
|
|
3046
|
+
// Detect chip type
|
|
3047
|
+
await this.detectChip();
|
|
3048
|
+
this.logger.log(`Reconnected to bootloader: ${this.chipName}`);
|
|
3049
|
+
}
|
|
3050
|
+
catch (err) {
|
|
3051
|
+
// Ensure flag is reset on error
|
|
3052
|
+
this._isReconfiguring = false;
|
|
3053
|
+
throw err;
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
1514
3056
|
/**
|
|
1515
3057
|
* @name drainInputBuffer
|
|
1516
3058
|
* Actively drain the input buffer by reading data for a specified time.
|
|
@@ -1534,8 +3076,8 @@ export class ESPLoader extends EventTarget {
|
|
|
1534
3076
|
const drainStart = Date.now();
|
|
1535
3077
|
const drainTimeout = 100; // Short timeout for draining
|
|
1536
3078
|
while (drained < bytesToDrain && Date.now() - drainStart < drainTimeout) {
|
|
1537
|
-
if (this.
|
|
1538
|
-
const byte = this.
|
|
3079
|
+
if (this._inputBufferAvailable > 0) {
|
|
3080
|
+
const byte = this._readByte();
|
|
1539
3081
|
if (byte !== undefined) {
|
|
1540
3082
|
drained++;
|
|
1541
3083
|
}
|
|
@@ -1551,6 +3093,7 @@ export class ESPLoader extends EventTarget {
|
|
|
1551
3093
|
// Final clear of application buffer
|
|
1552
3094
|
if (!this._parent) {
|
|
1553
3095
|
this.__inputBuffer = [];
|
|
3096
|
+
this.__inputBufferReadIndex = 0;
|
|
1554
3097
|
}
|
|
1555
3098
|
}
|
|
1556
3099
|
/**
|
|
@@ -1562,12 +3105,14 @@ export class ESPLoader extends EventTarget {
|
|
|
1562
3105
|
// Clear application buffer
|
|
1563
3106
|
if (!this._parent) {
|
|
1564
3107
|
this.__inputBuffer = [];
|
|
3108
|
+
this.__inputBufferReadIndex = 0;
|
|
1565
3109
|
}
|
|
1566
3110
|
// Wait for any pending data
|
|
1567
3111
|
await sleep(SYNC_TIMEOUT);
|
|
1568
3112
|
// Final clear
|
|
1569
3113
|
if (!this._parent) {
|
|
1570
3114
|
this.__inputBuffer = [];
|
|
3115
|
+
this.__inputBufferReadIndex = 0;
|
|
1571
3116
|
}
|
|
1572
3117
|
this.logger.debug("Serial buffers flushed");
|
|
1573
3118
|
}
|
|
@@ -1577,16 +3122,53 @@ export class ESPLoader extends EventTarget {
|
|
|
1577
3122
|
* @param addr - Address to read from
|
|
1578
3123
|
* @param size - Number of bytes to read
|
|
1579
3124
|
* @param onPacketReceived - Optional callback function called when packet is received
|
|
3125
|
+
* @param options - Optional parameters for advanced control
|
|
3126
|
+
* - chunkSize: Amount of data to request from ESP in one command (bytes)
|
|
3127
|
+
* - blockSize: Size of each data block sent by ESP (bytes)
|
|
3128
|
+
* - maxInFlight: Maximum unacknowledged bytes (bytes)
|
|
1580
3129
|
* @returns Uint8Array containing the flash data
|
|
1581
3130
|
*/
|
|
1582
|
-
async readFlash(addr, size, onPacketReceived) {
|
|
3131
|
+
async readFlash(addr, size, onPacketReceived, options) {
|
|
1583
3132
|
if (!this.IS_STUB) {
|
|
1584
3133
|
throw new Error("Reading flash is only supported in stub mode. Please run runStub() first.");
|
|
1585
3134
|
}
|
|
1586
3135
|
// Flush serial buffers before flash read operation
|
|
1587
3136
|
await this.flushSerialBuffers();
|
|
1588
3137
|
this.logger.log(`Reading ${size} bytes from flash at address 0x${addr.toString(16)}...`);
|
|
1589
|
-
|
|
3138
|
+
// Initialize adaptive speed multipliers for WebUSB devices
|
|
3139
|
+
if (this.isWebUSB()) {
|
|
3140
|
+
if (this._isCDCDevice) {
|
|
3141
|
+
// CDC devices (CH343): Start with maximum, adaptive adjustment enabled
|
|
3142
|
+
this._adaptiveBlockMultiplier = 8; // blockSize = 248 bytes
|
|
3143
|
+
this._adaptiveMaxInFlightMultiplier = 8; // maxInFlight = 248 bytes
|
|
3144
|
+
this._consecutiveSuccessfulChunks = 0;
|
|
3145
|
+
this.logger.debug(`CDC device - Initialized: blockMultiplier=${this._adaptiveBlockMultiplier}, maxInFlightMultiplier=${this._adaptiveMaxInFlightMultiplier}`);
|
|
3146
|
+
}
|
|
3147
|
+
else {
|
|
3148
|
+
// Non-CDC devices (CH340, CP2102): Fixed values, no adaptive adjustment
|
|
3149
|
+
this._adaptiveBlockMultiplier = 1; // blockSize = 31 bytes (fixed)
|
|
3150
|
+
this._adaptiveMaxInFlightMultiplier = 1; // maxInFlight = 31 bytes (fixed)
|
|
3151
|
+
this._consecutiveSuccessfulChunks = 0;
|
|
3152
|
+
this.logger.debug(`Non-CDC device - Fixed values: blockSize=31, maxInFlight=31`);
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
// Chunk size: Amount of data to request from ESP in one command
|
|
3156
|
+
// For WebUSB (Android), use smaller chunks to avoid timeouts and buffer issues
|
|
3157
|
+
// For Web Serial (Desktop), use larger chunks for better performance
|
|
3158
|
+
let CHUNK_SIZE;
|
|
3159
|
+
if ((options === null || options === void 0 ? void 0 : options.chunkSize) !== undefined) {
|
|
3160
|
+
// Use user-provided chunkSize if in advanced mode
|
|
3161
|
+
CHUNK_SIZE = options.chunkSize;
|
|
3162
|
+
this.logger.log(`Using custom chunk size: 0x${CHUNK_SIZE.toString(16)} bytes`);
|
|
3163
|
+
}
|
|
3164
|
+
else if (this.isWebUSB()) {
|
|
3165
|
+
// WebUSB: Use smaller chunks to avoid SLIP timeout issues
|
|
3166
|
+
CHUNK_SIZE = 0x4 * 0x1000; // 4KB = 16384 bytes
|
|
3167
|
+
}
|
|
3168
|
+
else {
|
|
3169
|
+
// Web Serial: Use larger chunks for better performance
|
|
3170
|
+
CHUNK_SIZE = 0x40 * 0x1000;
|
|
3171
|
+
}
|
|
1590
3172
|
let allData = new Uint8Array(0);
|
|
1591
3173
|
let currentAddr = addr;
|
|
1592
3174
|
let remainingSize = size;
|
|
@@ -1599,14 +3181,39 @@ export class ESPLoader extends EventTarget {
|
|
|
1599
3181
|
// Retry loop for this chunk
|
|
1600
3182
|
while (!chunkSuccess && retryCount <= MAX_RETRIES) {
|
|
1601
3183
|
let resp = new Uint8Array(0);
|
|
3184
|
+
let lastAckedLength = 0; // Track last acknowledged length
|
|
1602
3185
|
try {
|
|
1603
3186
|
// Only log on first attempt or retries
|
|
1604
3187
|
if (retryCount === 0) {
|
|
1605
3188
|
this.logger.debug(`Reading chunk at 0x${currentAddr.toString(16)}, size: 0x${chunkSize.toString(16)}`);
|
|
1606
3189
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
3190
|
+
let blockSize;
|
|
3191
|
+
let maxInFlight;
|
|
3192
|
+
if ((options === null || options === void 0 ? void 0 : options.blockSize) !== undefined &&
|
|
3193
|
+
(options === null || options === void 0 ? void 0 : options.maxInFlight) !== undefined) {
|
|
3194
|
+
// Use user-provided values if in advanced mode
|
|
3195
|
+
blockSize = options.blockSize;
|
|
3196
|
+
maxInFlight = options.maxInFlight;
|
|
3197
|
+
if (retryCount === 0) {
|
|
3198
|
+
this.logger.debug(`Using custom parameters: blockSize=${blockSize}, maxInFlight=${maxInFlight}`);
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
else if (this.isWebUSB()) {
|
|
3202
|
+
// WebUSB (Android): All devices use adaptive speed
|
|
3203
|
+
// All have maxTransferSize=64, baseBlockSize=31
|
|
3204
|
+
const maxTransferSize = this.port.maxTransferSize || 64;
|
|
3205
|
+
const baseBlockSize = Math.floor((maxTransferSize - 2) / 2); // 31 bytes
|
|
3206
|
+
// Use current adaptive multipliers (initialized at start of readFlash)
|
|
3207
|
+
blockSize = baseBlockSize * this._adaptiveBlockMultiplier;
|
|
3208
|
+
maxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
|
|
3209
|
+
}
|
|
3210
|
+
else {
|
|
3211
|
+
// Web Serial (Desktop): Use multiples of 63 for consistency
|
|
3212
|
+
const base = 63;
|
|
3213
|
+
blockSize = base * 65; // 63 * 65 = 4095 (close to 0x1000)
|
|
3214
|
+
maxInFlight = base * 130; // 63 * 130 = 8190 (close to blockSize * 2)
|
|
3215
|
+
}
|
|
3216
|
+
const pkt = pack("<IIII", currentAddr, chunkSize, blockSize, maxInFlight);
|
|
1610
3217
|
const [res] = await this.checkCommand(ESP_READ_FLASH, pkt);
|
|
1611
3218
|
if (res != 0) {
|
|
1612
3219
|
throw new Error("Failed to read memory: " + res);
|
|
@@ -1649,10 +3256,19 @@ export class ESPLoader extends EventTarget {
|
|
|
1649
3256
|
newResp.set(resp);
|
|
1650
3257
|
newResp.set(packetData, resp.length);
|
|
1651
3258
|
resp = newResp;
|
|
1652
|
-
// Send acknowledgment
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
3259
|
+
// Send acknowledgment when we've received maxInFlight bytes
|
|
3260
|
+
// The stub sends packets until (num_sent - num_acked) >= max_in_flight
|
|
3261
|
+
// We MUST wait for all packets before sending ACK
|
|
3262
|
+
const shouldAck = resp.length >= chunkSize || // End of chunk
|
|
3263
|
+
resp.length >= lastAckedLength + maxInFlight; // Received all packets
|
|
3264
|
+
if (shouldAck) {
|
|
3265
|
+
const ackData = pack("<I", resp.length);
|
|
3266
|
+
const slipEncodedAck = slipEncode(ackData);
|
|
3267
|
+
await this.writeToStream(slipEncodedAck);
|
|
3268
|
+
// Update lastAckedLength to current response length
|
|
3269
|
+
// This ensures next ACK is sent at the right time
|
|
3270
|
+
lastAckedLength = resp.length;
|
|
3271
|
+
}
|
|
1656
3272
|
}
|
|
1657
3273
|
}
|
|
1658
3274
|
// Chunk read successfully - append to all data
|
|
@@ -1661,13 +3277,66 @@ export class ESPLoader extends EventTarget {
|
|
|
1661
3277
|
newAllData.set(resp, allData.length);
|
|
1662
3278
|
allData = newAllData;
|
|
1663
3279
|
chunkSuccess = true;
|
|
3280
|
+
// ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
|
|
3281
|
+
// Non-CDC devices (CH340, CP2102) stay at fixed blockSize=31, maxInFlight=31
|
|
3282
|
+
if (this.isWebUSB() && this._isCDCDevice && retryCount === 0) {
|
|
3283
|
+
this._consecutiveSuccessfulChunks++;
|
|
3284
|
+
// After 2 consecutive successful chunks, increase speed gradually
|
|
3285
|
+
if (this._consecutiveSuccessfulChunks >= 2) {
|
|
3286
|
+
const maxTransferSize = this.port.maxTransferSize || 64;
|
|
3287
|
+
const baseBlockSize = Math.floor((maxTransferSize - 2) / 2); // 31 bytes
|
|
3288
|
+
// Maximum: blockSize=248 (8 * 31), maxInFlight=248 (8 * 31)
|
|
3289
|
+
const MAX_BLOCK_MULTIPLIER = 8; // 248 bytes - tested stable
|
|
3290
|
+
const MAX_INFLIGHT_MULTIPLIER = 8; // 248 bytes - tested stable
|
|
3291
|
+
let adjusted = false;
|
|
3292
|
+
// Increase blockSize first (up to 248), then maxInFlight
|
|
3293
|
+
if (this._adaptiveBlockMultiplier < MAX_BLOCK_MULTIPLIER) {
|
|
3294
|
+
this._adaptiveBlockMultiplier = Math.min(this._adaptiveBlockMultiplier * 2, MAX_BLOCK_MULTIPLIER);
|
|
3295
|
+
adjusted = true;
|
|
3296
|
+
}
|
|
3297
|
+
// Once blockSize is at maximum, increase maxInFlight
|
|
3298
|
+
else if (this._adaptiveMaxInFlightMultiplier < MAX_INFLIGHT_MULTIPLIER) {
|
|
3299
|
+
this._adaptiveMaxInFlightMultiplier = Math.min(this._adaptiveMaxInFlightMultiplier * 2, MAX_INFLIGHT_MULTIPLIER);
|
|
3300
|
+
adjusted = true;
|
|
3301
|
+
}
|
|
3302
|
+
if (adjusted) {
|
|
3303
|
+
const newBlockSize = baseBlockSize * this._adaptiveBlockMultiplier;
|
|
3304
|
+
const newMaxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
|
|
3305
|
+
this.logger.debug(`Speed increased: blockSize=${newBlockSize}, maxInFlight=${newMaxInFlight}`);
|
|
3306
|
+
this._lastAdaptiveAdjustment = Date.now();
|
|
3307
|
+
}
|
|
3308
|
+
// Reset counter
|
|
3309
|
+
this._consecutiveSuccessfulChunks = 0;
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
1664
3312
|
}
|
|
1665
3313
|
catch (err) {
|
|
1666
3314
|
retryCount++;
|
|
3315
|
+
// ADAPTIVE SPEED ADJUSTMENT: Only for CDC devices
|
|
3316
|
+
// Non-CDC devices stay at fixed values
|
|
3317
|
+
if (this.isWebUSB() && this._isCDCDevice && retryCount === 1) {
|
|
3318
|
+
// Only reduce if we're above minimum
|
|
3319
|
+
if (this._adaptiveBlockMultiplier > 1 ||
|
|
3320
|
+
this._adaptiveMaxInFlightMultiplier > 1) {
|
|
3321
|
+
// Reduce to minimum on error
|
|
3322
|
+
this._adaptiveBlockMultiplier = 1; // 31 bytes (for CH343)
|
|
3323
|
+
this._adaptiveMaxInFlightMultiplier = 1; // 31 bytes
|
|
3324
|
+
this._consecutiveSuccessfulChunks = 0; // Reset success counter
|
|
3325
|
+
const maxTransferSize = this.port.maxTransferSize || 64;
|
|
3326
|
+
const baseBlockSize = Math.floor((maxTransferSize - 2) / 2);
|
|
3327
|
+
const newBlockSize = baseBlockSize * this._adaptiveBlockMultiplier;
|
|
3328
|
+
const newMaxInFlight = baseBlockSize * this._adaptiveMaxInFlightMultiplier;
|
|
3329
|
+
this.logger.debug(`Error at higher speed - reduced to minimum: blockSize=${newBlockSize}, maxInFlight=${newMaxInFlight}`);
|
|
3330
|
+
}
|
|
3331
|
+
else {
|
|
3332
|
+
// Already at minimum and still failing - this is a real error
|
|
3333
|
+
this.logger.debug(`Error at minimum speed (blockSize=31, maxInFlight=31) - not a speed issue`);
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
1667
3336
|
// Check if it's a timeout error or SLIP error
|
|
1668
3337
|
if (err instanceof SlipReadError) {
|
|
1669
3338
|
if (retryCount <= MAX_RETRIES) {
|
|
1670
|
-
this.logger.
|
|
3339
|
+
this.logger.debug(`${err.message} at 0x${currentAddr.toString(16)}. Draining buffer and retrying (attempt ${retryCount}/${MAX_RETRIES})...`);
|
|
1671
3340
|
try {
|
|
1672
3341
|
await this.drainInputBuffer(200);
|
|
1673
3342
|
// Clear application buffer
|
|
@@ -1681,10 +3350,11 @@ export class ESPLoader extends EventTarget {
|
|
|
1681
3350
|
}
|
|
1682
3351
|
}
|
|
1683
3352
|
else {
|
|
1684
|
-
// All retries exhausted - attempt
|
|
3353
|
+
// All retries exhausted - attempt recovery by reloading stub
|
|
3354
|
+
// IMPORTANT: Do NOT close port to keep ESP32 in bootloader mode
|
|
1685
3355
|
if (!deepRecoveryAttempted) {
|
|
1686
3356
|
deepRecoveryAttempted = true;
|
|
1687
|
-
this.logger.log(`All retries exhausted at 0x${currentAddr.toString(16)}. Attempting
|
|
3357
|
+
this.logger.log(`All retries exhausted at 0x${currentAddr.toString(16)}. Attempting recovery (close and reopen port)...`);
|
|
1688
3358
|
try {
|
|
1689
3359
|
// Reconnect will close port, reopen, and reload stub
|
|
1690
3360
|
await this.reconnect();
|
|
@@ -1693,13 +3363,13 @@ export class ESPLoader extends EventTarget {
|
|
|
1693
3363
|
retryCount = 0;
|
|
1694
3364
|
continue;
|
|
1695
3365
|
}
|
|
1696
|
-
catch (
|
|
1697
|
-
throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and
|
|
3366
|
+
catch (recoveryErr) {
|
|
3367
|
+
throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery failed: ${recoveryErr}`);
|
|
1698
3368
|
}
|
|
1699
3369
|
}
|
|
1700
3370
|
else {
|
|
1701
|
-
//
|
|
1702
|
-
throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and
|
|
3371
|
+
// Recovery already attempted, give up
|
|
3372
|
+
throw new Error(`Failed to read chunk at 0x${currentAddr.toString(16)} after ${MAX_RETRIES} retries and recovery attempt`);
|
|
1703
3373
|
}
|
|
1704
3374
|
}
|
|
1705
3375
|
}
|
|
@@ -1717,7 +3387,6 @@ export class ESPLoader extends EventTarget {
|
|
|
1717
3387
|
remainingSize -= chunkSize;
|
|
1718
3388
|
this.logger.debug(`Total progress: 0x${allData.length.toString(16)} from 0x${size.toString(16)} bytes`);
|
|
1719
3389
|
}
|
|
1720
|
-
this.logger.debug(`Successfully read ${allData.length} bytes from flash`);
|
|
1721
3390
|
return allData;
|
|
1722
3391
|
}
|
|
1723
3392
|
}
|
|
@@ -1765,10 +3434,50 @@ class EspStubLoader extends ESPLoader {
|
|
|
1765
3434
|
return [0, []];
|
|
1766
3435
|
}
|
|
1767
3436
|
/**
|
|
1768
|
-
* @name
|
|
1769
|
-
*
|
|
3437
|
+
* @name eraseFlash
|
|
3438
|
+
* Erase entire flash chip
|
|
1770
3439
|
*/
|
|
1771
3440
|
async eraseFlash() {
|
|
1772
3441
|
await this.checkCommand(ESP_ERASE_FLASH, [], 0, CHIP_ERASE_TIMEOUT);
|
|
1773
3442
|
}
|
|
3443
|
+
/**
|
|
3444
|
+
* @name eraseRegion
|
|
3445
|
+
* Erase a specific region of flash
|
|
3446
|
+
*/
|
|
3447
|
+
async eraseRegion(offset, size) {
|
|
3448
|
+
// Validate inputs
|
|
3449
|
+
if (offset < 0) {
|
|
3450
|
+
throw new Error(`Invalid offset: ${offset} (must be non-negative)`);
|
|
3451
|
+
}
|
|
3452
|
+
if (size < 0) {
|
|
3453
|
+
throw new Error(`Invalid size: ${size} (must be non-negative)`);
|
|
3454
|
+
}
|
|
3455
|
+
// No-op for zero size
|
|
3456
|
+
if (size === 0) {
|
|
3457
|
+
this.logger.log("eraseRegion: size is 0, skipping erase");
|
|
3458
|
+
return;
|
|
3459
|
+
}
|
|
3460
|
+
// Check for sector alignment
|
|
3461
|
+
if (offset % FLASH_SECTOR_SIZE !== 0) {
|
|
3462
|
+
throw new Error(`Offset ${offset} (0x${offset.toString(16)}) is not aligned to flash sector size ${FLASH_SECTOR_SIZE} (0x${FLASH_SECTOR_SIZE.toString(16)})`);
|
|
3463
|
+
}
|
|
3464
|
+
if (size % FLASH_SECTOR_SIZE !== 0) {
|
|
3465
|
+
throw new Error(`Size ${size} (0x${size.toString(16)}) is not aligned to flash sector size ${FLASH_SECTOR_SIZE} (0x${FLASH_SECTOR_SIZE.toString(16)})`);
|
|
3466
|
+
}
|
|
3467
|
+
// Check for reasonable bounds (prevent wrapping in pack)
|
|
3468
|
+
const maxValue = 0xffffffff; // 32-bit unsigned max
|
|
3469
|
+
if (offset > maxValue) {
|
|
3470
|
+
throw new Error(`Offset ${offset} exceeds maximum value ${maxValue}`);
|
|
3471
|
+
}
|
|
3472
|
+
if (size > maxValue) {
|
|
3473
|
+
throw new Error(`Size ${size} exceeds maximum value ${maxValue}`);
|
|
3474
|
+
}
|
|
3475
|
+
// Check for wrap-around
|
|
3476
|
+
if (offset + size > maxValue) {
|
|
3477
|
+
throw new Error(`Region end (offset + size = ${offset + size}) exceeds maximum addressable range ${maxValue}`);
|
|
3478
|
+
}
|
|
3479
|
+
const timeout = timeoutPerMb(ERASE_REGION_TIMEOUT_PER_MB, size);
|
|
3480
|
+
const buffer = pack("<II", offset, size);
|
|
3481
|
+
await this.checkCommand(ESP_ERASE_REGION, buffer, 0, timeout);
|
|
3482
|
+
}
|
|
1774
3483
|
}
|