esp32tool 1.3.7 → 1.4.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/src/esp_loader.ts CHANGED
@@ -109,6 +109,29 @@ import {
109
109
  ESP32P4_PMU_0P1A_TARGET0_0,
110
110
  ESP32P4_PMU_0P1A_FORCE_TIEH_SEL_0,
111
111
  ESP32P4_PMU_DATE_REG,
112
+ ESP32S2_UARTDEV_BUF_NO,
113
+ ESP32S2_UARTDEV_BUF_NO_USB_OTG,
114
+ ESP32S3_UARTDEV_BUF_NO,
115
+ ESP32S3_UARTDEV_BUF_NO_USB_OTG,
116
+ ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
117
+ ESP32C3_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
118
+ ESP32C3_BUF_UART_NO_OFFSET,
119
+ ESP32C5_UARTDEV_BUF_NO,
120
+ ESP32C5_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
121
+ ESP32C6_UARTDEV_BUF_NO,
122
+ ESP32C6_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
123
+ ESP32C61_UARTDEV_BUF_NO_REV_LE2,
124
+ ESP32C61_UARTDEV_BUF_NO_REV_GT2,
125
+ ESP32C61_UARTDEV_BUF_NO_USB_JTAG_SERIAL_REV_LE2,
126
+ ESP32C61_UARTDEV_BUF_NO_USB_JTAG_SERIAL_REV_GT2,
127
+ ESP32H2_UARTDEV_BUF_NO,
128
+ ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
129
+ ESP32H4_UARTDEV_BUF_NO,
130
+ ESP32H4_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
131
+ ESP32P4_UARTDEV_BUF_NO_REV0,
132
+ ESP32P4_UARTDEV_BUF_NO_REV300,
133
+ ESP32P4_UARTDEV_BUF_NO_USB_OTG,
134
+ ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL,
112
135
  } from "./const";
113
136
  import { getStubCode } from "./stubs";
114
137
  import { hexFormatter, sleep, slipEncode, toHex } from "./util";
@@ -536,7 +559,15 @@ export class ESPLoader extends EventTarget {
536
559
  );
537
560
  } catch (err) {
538
561
  this.logger.debug(`Could not detect USB connection type: ${err}`);
539
- // Leave as undefined if detection fails
562
+ }
563
+
564
+ try {
565
+ const usbMode = await this.getUsbMode();
566
+ this.logger.debug(
567
+ `USB mode (register): ${usbMode.mode} (uartNo=${usbMode.uartNo})`,
568
+ );
569
+ } catch (err) {
570
+ this.logger.debug(`Could not detect USB mode: ${err}`);
540
571
  }
541
572
 
542
573
  // Read the OTP data for this chip and store into this.efuses array
@@ -952,10 +983,6 @@ export class ESPLoader extends EventTarget {
952
983
  this.logger.debug("Finished read loop");
953
984
  }
954
985
 
955
- sleep(ms = 100) {
956
- return new Promise((resolve) => setTimeout(resolve, ms));
957
- }
958
-
959
986
  state_DTR = false;
960
987
  state_RTS = false;
961
988
 
@@ -986,28 +1013,46 @@ export class ESPLoader extends EventTarget {
986
1013
  });
987
1014
  }
988
1015
 
1016
+ private async runSignalSequence(
1017
+ steps: Array<{ dtr?: boolean; rts?: boolean; delayMs?: number }>,
1018
+ ): Promise<void> {
1019
+ const webusb = (this.port as any).isWebUSB === true;
1020
+ for (const step of steps) {
1021
+ if (step.dtr !== undefined && step.rts !== undefined) {
1022
+ if (webusb) {
1023
+ await this.setDTRandRTSWebUSB(step.dtr, step.rts);
1024
+ } else {
1025
+ await this.setDTRandRTS(step.dtr, step.rts);
1026
+ }
1027
+ } else {
1028
+ if (step.dtr !== undefined) {
1029
+ webusb
1030
+ ? await this.setDTRWebUSB(step.dtr)
1031
+ : await this.setDTR(step.dtr);
1032
+ }
1033
+ if (step.rts !== undefined) {
1034
+ webusb
1035
+ ? await this.setRTSWebUSB(step.rts)
1036
+ : await this.setRTS(step.rts);
1037
+ }
1038
+ }
1039
+ if (step.delayMs) await sleep(step.delayMs);
1040
+ }
1041
+ }
1042
+
989
1043
  /**
990
1044
  * @name hardResetUSBJTAGSerial
991
1045
  * USB-JTAG/Serial reset for Web Serial (Desktop)
992
1046
  */
993
1047
  async hardResetUSBJTAGSerial() {
994
- await this.setRTS(false);
995
- await this.setDTR(false); // Idle
996
- await this.sleep(100);
997
-
998
- await this.setDTR(true); // Set IO0
999
- await this.setRTS(false);
1000
- await this.sleep(100);
1001
-
1002
- await this.setRTS(true); // Reset
1003
- await this.setDTR(false);
1004
- await this.setRTS(true);
1005
- await this.sleep(100);
1006
-
1007
- await this.setDTR(false);
1008
- await this.setRTS(false); // Chip out of reset
1009
-
1010
- await this.sleep(200);
1048
+ await this.runSignalSequence([
1049
+ { rts: false },
1050
+ { dtr: false, delayMs: 100 },
1051
+ { dtr: true, rts: false, delayMs: 100 },
1052
+ { rts: true },
1053
+ { dtr: false, rts: true, delayMs: 100 },
1054
+ { dtr: false, rts: false, delayMs: 200 },
1055
+ ]);
1011
1056
  }
1012
1057
 
1013
1058
  /**
@@ -1015,15 +1060,11 @@ export class ESPLoader extends EventTarget {
1015
1060
  * Classic reset for Web Serial (Desktop) DTR = IO0, RTS = EN
1016
1061
  */
1017
1062
  async hardResetClassic() {
1018
- await this.setDTR(false); // IO0=HIGH
1019
- await this.setRTS(true); // EN=LOW, chip in reset
1020
- await this.sleep(100);
1021
- await this.setDTR(true); // IO0=LOW
1022
- await this.setRTS(false); // EN=HIGH, chip out of reset
1023
- await this.sleep(50);
1024
- await this.setDTR(false); // IO0=HIGH, done
1025
-
1026
- await this.sleep(200);
1063
+ await this.runSignalSequence([
1064
+ { dtr: false, rts: true, delayMs: 100 },
1065
+ { dtr: true, rts: false, delayMs: 50 },
1066
+ { dtr: false, delayMs: 200 },
1067
+ ]);
1027
1068
  }
1028
1069
 
1029
1070
  /**
@@ -1031,27 +1072,11 @@ export class ESPLoader extends EventTarget {
1031
1072
  * Keeps IO0=HIGH during reset so chip boots into firmware
1032
1073
  */
1033
1074
  async hardResetToFirmware() {
1034
- await this.setDTR(false); // IO0=HIGH
1035
- await this.setRTS(true); // EN=LOW, chip in reset
1036
- await this.sleep(100);
1037
- await this.setRTS(false); // EN=HIGH, chip out of reset (IO0 stays HIGH)
1038
- await this.sleep(50);
1039
-
1040
- await this.sleep(200);
1041
- }
1042
-
1043
- /**
1044
- * Reset to firmware mode (not bootloader) for WebUSB
1045
- * Keeps IO0=HIGH during reset so chip boots into firmware
1046
- */
1047
- async hardResetToFirmwareWebUSB() {
1048
- await this.setDTRWebUSB(false); // IO0=HIGH
1049
- await this.setRTSWebUSB(true); // EN=LOW, chip in reset
1050
- await this.sleep(100);
1051
- await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset (IO0 stays HIGH)
1052
- await this.sleep(50);
1053
-
1054
- await this.sleep(200);
1075
+ await this.runSignalSequence([
1076
+ { dtr: false, rts: true, delayMs: 100 },
1077
+ { rts: false, delayMs: 50 },
1078
+ { delayMs: 200 },
1079
+ ]);
1055
1080
  }
1056
1081
 
1057
1082
  /**
@@ -1059,16 +1084,14 @@ export class ESPLoader extends EventTarget {
1059
1084
  * Unix Tight reset for Web Serial (Desktop) - sets DTR and RTS simultaneously
1060
1085
  */
1061
1086
  async hardResetUnixTight() {
1062
- await this.setDTRandRTS(true, true);
1063
- await this.setDTRandRTS(false, false);
1064
- await this.setDTRandRTS(false, true); // IO0=HIGH & EN=LOW, chip in reset
1065
- await this.sleep(100);
1066
- await this.setDTRandRTS(true, false); // IO0=LOW & EN=HIGH, chip out of reset
1067
- await this.sleep(50);
1068
- await this.setDTRandRTS(false, false); // IO0=HIGH, done
1069
- await this.setDTR(false); // Needed in some environments to ensure IO0=HIGH
1070
-
1071
- await this.sleep(200);
1087
+ await this.runSignalSequence([
1088
+ { dtr: true, rts: true },
1089
+ { dtr: false, rts: false },
1090
+ { dtr: false, rts: true, delayMs: 100 },
1091
+ { dtr: true, rts: false, delayMs: 50 },
1092
+ { dtr: false, rts: false },
1093
+ { dtr: false, delayMs: 200 },
1094
+ ]);
1072
1095
  }
1073
1096
 
1074
1097
  // ============================================================================
@@ -1103,83 +1126,17 @@ export class ESPLoader extends EventTarget {
1103
1126
  });
1104
1127
  }
1105
1128
 
1106
- /**
1107
- * @name hardResetUSBJTAGSerialWebUSB
1108
- * USB-JTAG/Serial reset for WebUSB (Android)
1109
- */
1110
- async hardResetUSBJTAGSerialWebUSB() {
1111
- await this.setRTSWebUSB(false);
1112
- await this.setDTRWebUSB(false); // Idle
1113
- await this.sleep(100);
1114
-
1115
- await this.setDTRWebUSB(true); // Set IO0
1116
- await this.setRTSWebUSB(false);
1117
- await this.sleep(100);
1118
-
1119
- await this.setRTSWebUSB(true); // Reset
1120
- await this.setDTRWebUSB(false);
1121
- await this.setRTSWebUSB(true);
1122
- await this.sleep(100);
1123
-
1124
- await this.setDTRWebUSB(false);
1125
- await this.setRTSWebUSB(false); // Chip out of reset
1126
-
1127
- await this.sleep(200);
1128
- }
1129
-
1130
1129
  /**
1131
1130
  * @name hardResetUSBJTAGSerialInvertedDTRWebUSB
1132
1131
  * USB-JTAG/Serial reset with inverted DTR for WebUSB (Android)
1133
1132
  */
1134
1133
  async hardResetUSBJTAGSerialInvertedDTRWebUSB() {
1135
- await this.setRTSWebUSB(false);
1136
- await this.setDTRWebUSB(true); // Idle (DTR inverted)
1137
- await this.sleep(100);
1138
-
1139
- await this.setDTRWebUSB(false); // Set IO0 (DTR inverted)
1140
- await this.setRTSWebUSB(false);
1141
- await this.sleep(100);
1142
-
1143
- await this.setRTSWebUSB(true); // Reset
1144
- await this.setDTRWebUSB(true); // (DTR inverted)
1145
- await this.setRTSWebUSB(true);
1146
- await this.sleep(100);
1147
-
1148
- await this.setDTRWebUSB(true); // (DTR inverted)
1149
- await this.setRTSWebUSB(false); // Chip out of reset
1150
-
1151
- await this.sleep(200);
1152
- }
1153
-
1154
- /**
1155
- * @name hardResetClassicWebUSB
1156
- * Classic reset for WebUSB (Android)
1157
- */
1158
- async hardResetClassicWebUSB() {
1159
- await this.setDTRWebUSB(false); // IO0=HIGH
1160
- await this.setRTSWebUSB(true); // EN=LOW, chip in reset
1161
- await this.sleep(100);
1162
- await this.setDTRWebUSB(true); // IO0=LOW
1163
- await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
1164
- await this.sleep(50);
1165
- await this.setDTRWebUSB(false); // IO0=HIGH, done
1166
- await this.sleep(200);
1167
- }
1168
-
1169
- /**
1170
- * @name hardResetUnixTightWebUSB
1171
- * Unix Tight reset for WebUSB (Android) - sets DTR and RTS simultaneously
1172
- */
1173
- async hardResetUnixTightWebUSB() {
1174
- await this.setDTRandRTSWebUSB(false, false);
1175
- await this.setDTRandRTSWebUSB(true, true);
1176
- await this.setDTRandRTSWebUSB(false, true); // IO0=HIGH & EN=LOW, chip in reset
1177
- await this.sleep(100);
1178
- await this.setDTRandRTSWebUSB(true, false); // IO0=LOW & EN=HIGH, chip out of reset
1179
- await this.sleep(50);
1180
- await this.setDTRandRTSWebUSB(false, false); // IO0=HIGH, done
1181
- await this.setDTRWebUSB(false); // Ensure IO0=HIGH
1182
- await this.sleep(200);
1134
+ await this.runSignalSequence([
1135
+ { rts: false, dtr: true, delayMs: 100 },
1136
+ { dtr: false, rts: false, delayMs: 100 },
1137
+ { rts: true, dtr: true, delayMs: 100 },
1138
+ { dtr: true, rts: false, delayMs: 200 },
1139
+ ]);
1183
1140
  }
1184
1141
 
1185
1142
  /**
@@ -1188,14 +1145,11 @@ export class ESPLoader extends EventTarget {
1188
1145
  * Specifically for CP2102/CH340 which may need more time
1189
1146
  */
1190
1147
  async hardResetClassicLongDelayWebUSB() {
1191
- await this.setDTRWebUSB(false); // IO0=HIGH
1192
- await this.setRTSWebUSB(true); // EN=LOW, chip in reset
1193
- await this.sleep(500); // Extra long delay
1194
- await this.setDTRWebUSB(true); // IO0=LOW
1195
- await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
1196
- await this.sleep(200);
1197
- await this.setDTRWebUSB(false); // IO0=HIGH, done
1198
- await this.sleep(500); // Extra long delay
1148
+ await this.runSignalSequence([
1149
+ { dtr: false, rts: true, delayMs: 500 },
1150
+ { dtr: true, rts: false, delayMs: 200 },
1151
+ { dtr: false, delayMs: 500 },
1152
+ ]);
1199
1153
  }
1200
1154
 
1201
1155
  /**
@@ -1205,12 +1159,12 @@ export class ESPLoader extends EventTarget {
1205
1159
  async hardResetClassicShortDelayWebUSB() {
1206
1160
  await this.setDTRWebUSB(false); // IO0=HIGH
1207
1161
  await this.setRTSWebUSB(true); // EN=LOW, chip in reset
1208
- await this.sleep(50);
1162
+ await sleep(50);
1209
1163
  await this.setDTRWebUSB(true); // IO0=LOW
1210
1164
  await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset
1211
- await this.sleep(25);
1165
+ await sleep(25);
1212
1166
  await this.setDTRWebUSB(false); // IO0=HIGH, done
1213
- await this.sleep(100);
1167
+ await sleep(100);
1214
1168
  }
1215
1169
 
1216
1170
  /**
@@ -1220,12 +1174,12 @@ export class ESPLoader extends EventTarget {
1220
1174
  async hardResetInvertedWebUSB() {
1221
1175
  await this.setDTRWebUSB(true); // IO0=HIGH (inverted)
1222
1176
  await this.setRTSWebUSB(false); // EN=LOW, chip in reset (inverted)
1223
- await this.sleep(100);
1177
+ await sleep(100);
1224
1178
  await this.setDTRWebUSB(false); // IO0=LOW (inverted)
1225
1179
  await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (inverted)
1226
- await this.sleep(50);
1180
+ await sleep(50);
1227
1181
  await this.setDTRWebUSB(true); // IO0=HIGH, done (inverted)
1228
- await this.sleep(200);
1182
+ await sleep(200);
1229
1183
  }
1230
1184
 
1231
1185
  /**
@@ -1235,12 +1189,12 @@ export class ESPLoader extends EventTarget {
1235
1189
  async hardResetInvertedDTRWebUSB() {
1236
1190
  await this.setDTRWebUSB(true); // IO0=HIGH (DTR inverted)
1237
1191
  await this.setRTSWebUSB(true); // EN=LOW, chip in reset (RTS normal)
1238
- await this.sleep(100);
1192
+ await sleep(100);
1239
1193
  await this.setDTRWebUSB(false); // IO0=LOW (DTR inverted)
1240
1194
  await this.setRTSWebUSB(false); // EN=HIGH, chip out of reset (RTS normal)
1241
- await this.sleep(50);
1195
+ await sleep(50);
1242
1196
  await this.setDTRWebUSB(true); // IO0=HIGH, done (DTR inverted)
1243
- await this.sleep(200);
1197
+ await sleep(200);
1244
1198
  }
1245
1199
 
1246
1200
  /**
@@ -1250,12 +1204,12 @@ export class ESPLoader extends EventTarget {
1250
1204
  async hardResetInvertedRTSWebUSB() {
1251
1205
  await this.setDTRWebUSB(false); // IO0=HIGH (DTR normal)
1252
1206
  await this.setRTSWebUSB(false); // EN=LOW, chip in reset (RTS inverted)
1253
- await this.sleep(100);
1207
+ await sleep(100);
1254
1208
  await this.setDTRWebUSB(true); // IO0=LOW (DTR normal)
1255
1209
  await this.setRTSWebUSB(true); // EN=HIGH, chip out of reset (RTS inverted)
1256
- await this.sleep(50);
1210
+ await sleep(50);
1257
1211
  await this.setDTRWebUSB(false); // IO0=HIGH, done (DTR normal)
1258
- await this.sleep(200);
1212
+ await sleep(200);
1259
1213
  }
1260
1214
 
1261
1215
  /**
@@ -1312,7 +1266,7 @@ export class ESPLoader extends EventTarget {
1312
1266
  resetStrategies.push({
1313
1267
  name: "USB-JTAG/Serial (WebUSB) - ESP32-S2",
1314
1268
  fn: async () => {
1315
- return await self.hardResetUSBJTAGSerialWebUSB();
1269
+ return await self.hardResetUSBJTAGSerial();
1316
1270
  },
1317
1271
  });
1318
1272
 
@@ -1328,7 +1282,7 @@ export class ESPLoader extends EventTarget {
1328
1282
  resetStrategies.push({
1329
1283
  name: "UnixTight (WebUSB) - ESP32-S2 CDC",
1330
1284
  fn: async () => {
1331
- return await self.hardResetUnixTightWebUSB();
1285
+ return await self.hardResetUnixTight();
1332
1286
  },
1333
1287
  });
1334
1288
 
@@ -1336,7 +1290,7 @@ export class ESPLoader extends EventTarget {
1336
1290
  resetStrategies.push({
1337
1291
  name: "Classic (WebUSB) - ESP32-S2 CDC",
1338
1292
  fn: async () => {
1339
- return await self.hardResetClassicWebUSB();
1293
+ return await self.hardResetClassic();
1340
1294
  },
1341
1295
  });
1342
1296
  } else {
@@ -1350,7 +1304,7 @@ export class ESPLoader extends EventTarget {
1350
1304
  resetStrategies.push({
1351
1305
  name: "USB-JTAG/Serial (WebUSB)",
1352
1306
  fn: async () => {
1353
- return await self.hardResetUSBJTAGSerialWebUSB();
1307
+ return await self.hardResetUSBJTAGSerial();
1354
1308
  },
1355
1309
  });
1356
1310
  resetStrategies.push({
@@ -1369,13 +1323,13 @@ export class ESPLoader extends EventTarget {
1369
1323
  resetStrategies.push({
1370
1324
  name: "UnixTight (WebUSB) - CH34x",
1371
1325
  fn: async () => {
1372
- return await self.hardResetUnixTightWebUSB();
1326
+ return await self.hardResetUnixTight();
1373
1327
  },
1374
1328
  });
1375
1329
  resetStrategies.push({
1376
1330
  name: "Classic (WebUSB) - CH34x",
1377
1331
  fn: async () => {
1378
- return await self.hardResetClassicWebUSB();
1332
+ return await self.hardResetClassic();
1379
1333
  },
1380
1334
  });
1381
1335
  resetStrategies.push({
@@ -1403,14 +1357,14 @@ export class ESPLoader extends EventTarget {
1403
1357
  resetStrategies.push({
1404
1358
  name: "UnixTight (WebUSB) - CP2102",
1405
1359
  fn: async () => {
1406
- return await self.hardResetUnixTightWebUSB();
1360
+ return await self.hardResetUnixTight();
1407
1361
  },
1408
1362
  });
1409
1363
 
1410
1364
  resetStrategies.push({
1411
1365
  name: "Classic (WebUSB) - CP2102",
1412
1366
  fn: async () => {
1413
- return await self.hardResetClassicWebUSB();
1367
+ return await self.hardResetClassic();
1414
1368
  },
1415
1369
  });
1416
1370
 
@@ -1439,13 +1393,13 @@ export class ESPLoader extends EventTarget {
1439
1393
  resetStrategies.push({
1440
1394
  name: "UnixTight (WebUSB)",
1441
1395
  fn: async () => {
1442
- return await self.hardResetUnixTightWebUSB();
1396
+ return await self.hardResetUnixTight();
1443
1397
  },
1444
1398
  });
1445
1399
  resetStrategies.push({
1446
1400
  name: "Classic (WebUSB)",
1447
1401
  fn: async function () {
1448
- return await self.hardResetClassicWebUSB();
1402
+ return await self.hardResetClassic();
1449
1403
  },
1450
1404
  });
1451
1405
  resetStrategies.push({
@@ -1476,7 +1430,7 @@ export class ESPLoader extends EventTarget {
1476
1430
  resetStrategies.push({
1477
1431
  name: "Classic (WebUSB)",
1478
1432
  fn: async function () {
1479
- return await self.hardResetClassicWebUSB();
1433
+ return await self.hardResetClassic();
1480
1434
  },
1481
1435
  });
1482
1436
  }
@@ -1485,7 +1439,7 @@ export class ESPLoader extends EventTarget {
1485
1439
  resetStrategies.push({
1486
1440
  name: "UnixTight (WebUSB)",
1487
1441
  fn: async function () {
1488
- return await self.hardResetUnixTightWebUSB();
1442
+ return await self.hardResetUnixTight();
1489
1443
  },
1490
1444
  });
1491
1445
 
@@ -1510,7 +1464,7 @@ export class ESPLoader extends EventTarget {
1510
1464
  resetStrategies.push({
1511
1465
  name: "USB-JTAG/Serial fallback (WebUSB)",
1512
1466
  fn: async function () {
1513
- return await self.hardResetUSBJTAGSerialWebUSB();
1467
+ return await self.hardResetUSBJTAGSerial();
1514
1468
  },
1515
1469
  });
1516
1470
  }
@@ -1708,34 +1662,152 @@ export class ESPLoader extends EventTarget {
1708
1662
  await this.writeRegister(WDTWPROTECT_REG, 0, undefined, 0);
1709
1663
 
1710
1664
  // Wait for reset to take effect
1711
- await this.sleep(500);
1665
+ await sleep(500);
1712
1666
  }
1713
1667
 
1714
1668
  /**
1715
- * Helper: USB-based WDT reset
1716
- * Returns true if WDT reset was performed, false otherwise
1669
+ * Reset device from bootloader mode to firmware mode
1670
+ * Automatically selects the correct reset strategy based on USB connection type
1671
+ * @param clearForceDownloadFlag - If true, clears the force download boot flag (USB-OTG only)
1672
+ * @returns true if port will change (USB-OTG), false otherwise
1717
1673
  */
1718
- private async tryUsbWdtReset(chipName: string): Promise<boolean> {
1719
- const isUsingUsbOtg = await this.detectUsbConnectionType();
1674
+ public async resetToFirmwareMode(
1675
+ clearForceDownloadFlag = true,
1676
+ ): Promise<boolean> {
1677
+ this.logger.debug("Resetting from bootloader to firmware mode...");
1720
1678
 
1721
- if (isUsingUsbOtg) {
1722
- // Use WDT reset for USB-OTG devices
1723
- await this.rtcWdtResetChipSpecific();
1724
- this.logger.debug(
1725
- `${chipName}: RTC WDT reset (USB-JTAG/Serial or USB-OTG detected)`,
1726
- );
1727
- return true;
1728
- } else {
1729
- // Use classic reset for non-USB devices
1730
- if (this.isWebUSB()) {
1731
- await this.hardResetClassicWebUSB();
1732
- this.logger.debug("Classic reset (WebUSB/Android).");
1679
+ try {
1680
+ // Detect USB connection type
1681
+ const isUsbJtagOrOtg = await this.detectUsbConnectionType();
1682
+
1683
+ if (isUsbJtagOrOtg) {
1684
+ // USB-JTAG/OTG devices need special handling
1685
+ this.logger.debug("USB-JTAG/OTG detected - checking WDT reset support");
1686
+
1687
+ // Get detailed USB mode information
1688
+ let usbMode: {
1689
+ mode: "uart" | "usb-jtag-serial" | "usb-otg";
1690
+ uartNo: number;
1691
+ };
1692
+ try {
1693
+ usbMode = await this.getUsbMode();
1694
+ this.logger.debug(
1695
+ `USB mode: ${usbMode.mode} (uartNo=${usbMode.uartNo})`,
1696
+ );
1697
+ } catch (err) {
1698
+ this.logger.debug(`Could not get USB mode: ${err}`);
1699
+ // Fall back to generic USB-JTAG/OTG handling
1700
+ usbMode = { mode: "usb-jtag-serial", uartNo: 0 };
1701
+ }
1702
+
1703
+ // Check if chip supports WDT reset
1704
+ // WDT reset is not needed for ESP32-C3
1705
+ // WDT reset is supported by: ESP32-S2, ESP32-S3, ESP32-P4
1706
+ // WDT reset is NOT supported by: ESP32-C5, ESP32-C6, ESP32-C61, ESP32-H2
1707
+ const supportsWdtReset =
1708
+ this.chipFamily === CHIP_FAMILY_ESP32S2 ||
1709
+ this.chipFamily === CHIP_FAMILY_ESP32S3 ||
1710
+ this.chipFamily === CHIP_FAMILY_ESP32P4;
1711
+
1712
+ if (!supportsWdtReset) {
1713
+ this.logger.debug(
1714
+ `${this.chipName} does not support WDT reset - using classic reset instead`,
1715
+ );
1716
+
1717
+ // Use classic reset for chips without WDT support
1718
+ await this.hardResetToFirmware();
1719
+ this.logger.debug("Classic reset to firmware complete");
1720
+ return false; // Port stays open
1721
+ }
1722
+
1723
+ // WDT reset is supported - proceed with WDT reset logic
1724
+ this.logger.debug(
1725
+ `${this.chipName} supports WDT reset - using WDT reset strategy`,
1726
+ );
1727
+
1728
+ // CRITICAL: WDT register writes require ROM (not stub) and baudrate 115200
1729
+
1730
+ // If on stub, need to return to ROM first
1731
+ if (this.IS_STUB) {
1732
+ this.logger.debug("On stub - returning to ROM before WDT reset");
1733
+
1734
+ // Change baudrate back to ROM baudrate if needed
1735
+ if (this.currentBaudRate !== ESP_ROM_BAUD) {
1736
+ this.logger.debug(
1737
+ `Changing baudrate from ${this.currentBaudRate} to ${ESP_ROM_BAUD}`,
1738
+ );
1739
+ await this.reconfigurePort(ESP_ROM_BAUD);
1740
+ this.currentBaudRate = ESP_ROM_BAUD;
1741
+ this.logger.debug("Baudrate changed to 115200");
1742
+ }
1743
+
1744
+ // CRITICAL: Temporarily clear console mode flag so hardReset(true) works
1745
+ const wasInConsoleMode = this._consoleMode;
1746
+ this._consoleMode = false;
1747
+
1748
+ // Reset to bootloader (ROM)
1749
+ await this.hardReset(true);
1750
+ await sleep(200);
1751
+
1752
+ // Restore console mode flag
1753
+ this._consoleMode = wasInConsoleMode;
1754
+
1755
+ // Sync with ROM
1756
+ await this.sync();
1757
+ this.IS_STUB = false;
1758
+ this.logger.debug("Now on ROM");
1759
+ } else {
1760
+ // Even if not on stub, ensure baudrate is 115200 for WDT register writes
1761
+ if (this.currentBaudRate !== ESP_ROM_BAUD) {
1762
+ this.logger.debug(
1763
+ `Not on stub, but baudrate is ${this.currentBaudRate} - changing to ${ESP_ROM_BAUD} for WDT reset`,
1764
+ );
1765
+ await this.reconfigurePort(ESP_ROM_BAUD);
1766
+ this.currentBaudRate = ESP_ROM_BAUD;
1767
+ this.logger.debug("Baudrate changed to 115200");
1768
+ }
1769
+ }
1770
+
1771
+ // Clear force download boot flag if requested (USB-OTG only)
1772
+ if (clearForceDownloadFlag && usbMode.mode === "usb-otg") {
1773
+ const flagCleared = await this._clearForceDownloadBootIfNeeded();
1774
+ if (flagCleared) {
1775
+ this.logger.debug("Force download boot flag cleared");
1776
+ }
1777
+ }
1778
+
1779
+ // Perform WDT reset to boot into firmware
1780
+ await this.rtcWdtResetChipSpecific();
1781
+ this.logger.debug("WDT reset performed - device will boot to firmware");
1782
+
1783
+ // Check if port will change after WDT reset
1784
+ // USB-OTG (ESP32-S2/P4): Port always changes
1785
+ // USB-JTAG/Serial (ESP32-S3/C3/C5/C6/C61/H2/P4): Port may change depending on platform
1786
+ const portWillChange =
1787
+ usbMode.mode === "usb-otg" || usbMode.mode === "usb-jtag-serial";
1788
+
1789
+ if (portWillChange) {
1790
+ this.logger.debug(
1791
+ `Port will change after WDT reset (${usbMode.mode}) - port reselection needed`,
1792
+ );
1793
+ return true;
1794
+ }
1795
+
1796
+ return false;
1733
1797
  } else {
1734
- await this.hardResetClassic();
1735
- this.logger.debug("Classic reset.");
1798
+ // External serial chip - use classic reset to firmware
1799
+ this.logger.debug(
1800
+ "External serial chip detected - using classic reset",
1801
+ );
1802
+
1803
+ await this.hardResetToFirmware();
1804
+ this.logger.debug("Classic reset to firmware complete");
1805
+ return false;
1736
1806
  }
1807
+ } catch (err) {
1808
+ this.logger.error(`Failed to reset to firmware mode: ${err}`);
1809
+ throw err;
1737
1810
  }
1738
- return false;
1739
1811
  }
1740
1812
 
1741
1813
  async hardReset(bootloader = false) {
@@ -1749,68 +1821,83 @@ export class ESPLoader extends EventTarget {
1749
1821
  }
1750
1822
  // Simple hardware reset to restart firmware (IO0=HIGH)
1751
1823
  this.logger.debug("Performing hardware reset (console mode)...");
1752
- if (this.isWebUSB()) {
1753
- await this.hardResetToFirmwareWebUSB();
1754
- } else {
1755
- await this.hardResetToFirmware();
1756
- }
1824
+ await this.resetInConsoleMode();
1757
1825
  this.logger.debug("Hardware reset complete");
1758
1826
  return;
1759
1827
  }
1760
1828
 
1761
1829
  if (bootloader) {
1762
- // enter flash mode
1830
+ // Enter bootloader/flash mode
1763
1831
  if (this.port.getInfo().usbProductId === USB_JTAG_SERIAL_PID) {
1764
1832
  await this.hardResetUSBJTAGSerial();
1765
- this.logger.debug("USB-JTAG/Serial reset.");
1833
+ this.logger.debug("USB-JTAG/Serial reset to bootloader.");
1766
1834
  } else {
1767
- // Use different reset strategy for WebUSB (Android) vs Web Serial (Desktop)
1768
- if (this.isWebUSB()) {
1769
- await this.hardResetClassicWebUSB();
1770
- this.logger.debug("Classic reset (WebUSB/Android).");
1771
- } else {
1772
- await this.hardResetClassic();
1773
- this.logger.debug("Classic reset.");
1774
- }
1835
+ await this.hardResetClassic();
1836
+ this.logger.debug("Classic reset to bootloader.");
1775
1837
  }
1776
1838
  } else {
1777
- // just reset (no bootloader mode)
1778
- // For ESP32-S2/S3/P4 with USB-OTG or USB-JTAG/Serial, check if watchdog reset is needed
1779
- this.logger.debug("*** Performing WDT reset strategy ***");
1780
- if (this.chipFamily === CHIP_FAMILY_ESP32S2) {
1781
- const wdtResetUsed = await this.tryUsbWdtReset("ESP32-S2");
1782
- if (wdtResetUsed) return;
1783
- // } else if (this.chipFamily === CHIP_FAMILY_ESP32S3) {
1784
- // const wdtResetUsed = await this.tryUsbWdtReset("ESP32-S3");
1785
- // if (wdtResetUsed) return;
1786
- } else if (this.chipFamily === CHIP_FAMILY_ESP32P4) {
1787
- const wdtResetUsed = await this.tryUsbWdtReset("ESP32-P4");
1788
- if (wdtResetUsed) return;
1789
- // } else if (this.chipFamily === CHIP_FAMILY_ESP32C3) {
1790
- // const wdtResetUsed = await this.tryUsbWdtReset("ESP32-C3");
1791
- // if (wdtResetUsed) return;
1792
- } else if (this.chipFamily === CHIP_FAMILY_ESP32C5) {
1793
- const wdtResetUsed = await this.tryUsbWdtReset("ESP32-C5");
1794
- if (wdtResetUsed) return;
1795
- } else if (this.chipFamily === CHIP_FAMILY_ESP32C6) {
1796
- const wdtResetUsed = await this.tryUsbWdtReset("ESP32-C6");
1797
- if (wdtResetUsed) return;
1798
- }
1839
+ // Reset to firmware mode (exit bootloader)
1840
+ // Use intelligent reset strategy based on USB connection type
1841
+ this.logger.debug("Resetting to firmware mode...");
1842
+
1843
+ // Detect USB connection type to choose correct reset method
1844
+ const isUsbJtagOrOtg = await this.detectUsbConnectionType();
1845
+
1846
+ if (isUsbJtagOrOtg) {
1847
+ // USB-JTAG/OTG devices: Use WDT reset
1848
+ this.logger.debug("USB-JTAG/OTG detected - using WDT reset");
1849
+
1850
+ // Get USB mode details
1851
+ let usbMode: {
1852
+ mode: "uart" | "usb-jtag-serial" | "usb-otg";
1853
+ uartNo: number;
1854
+ };
1855
+ try {
1856
+ usbMode = await this.getUsbMode();
1857
+ this.logger.debug(
1858
+ `USB mode: ${usbMode.mode} (uartNo=${usbMode.uartNo})`,
1859
+ );
1860
+ } catch (err) {
1861
+ this.logger.debug(`Could not get USB mode: ${err}`);
1862
+ usbMode = { mode: "usb-jtag-serial", uartNo: 0 };
1863
+ }
1799
1864
 
1800
- // Standard reset for all other cases
1801
- if (this.isWebUSB()) {
1802
- // WebUSB: Use longer delays for better compatibility
1803
- await this.setRTSWebUSB(true); // EN->LOW
1804
- await this.sleep(200);
1805
- await this.setRTSWebUSB(false);
1806
- await this.sleep(200);
1807
- this.logger.debug("Hard reset (WebUSB).");
1865
+ // Clear force download flag for USB-OTG devices
1866
+ if (usbMode.mode === "usb-otg") {
1867
+ try {
1868
+ const flagCleared = await this._clearForceDownloadBootIfNeeded();
1869
+ if (flagCleared) {
1870
+ this.logger.debug("Force download boot flag cleared");
1871
+ }
1872
+ } catch (err) {
1873
+ this.logger.debug(`Could not clear force download flag: ${err}`);
1874
+ }
1875
+ }
1876
+
1877
+ // Perform WDT reset
1878
+ await this.rtcWdtResetChipSpecific();
1879
+ this.logger.debug(`${this.chipName}: WDT reset to firmware complete`);
1880
+ return;
1808
1881
  } else {
1809
- // Web Serial: Standard reset
1810
- await this.setRTS(true); // EN->LOW
1811
- await this.sleep(100);
1812
- await this.setRTS(false);
1813
- this.logger.debug("Hard reset.");
1882
+ // External serial chip: Use classic reset
1883
+ this.logger.debug(
1884
+ "External serial chip detected - using classic reset",
1885
+ );
1886
+
1887
+ if (this.isWebUSB()) {
1888
+ // WebUSB: Use longer delays for better compatibility
1889
+ await this.setRTSWebUSB(true); // EN->LOW
1890
+ await sleep(200);
1891
+ await this.setRTSWebUSB(false);
1892
+ await sleep(200);
1893
+ this.logger.debug("Hard reset to firmware (WebUSB).");
1894
+ } else {
1895
+ // Web Serial: Standard reset
1896
+ await this.setRTS(true); // EN->LOW
1897
+ await sleep(100);
1898
+ await this.setRTS(false);
1899
+ this.logger.debug("Hard reset to firmware.");
1900
+ }
1814
1901
  }
1815
1902
  }
1816
1903
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -3284,26 +3371,27 @@ export class ESPLoader extends EventTarget {
3284
3371
  this._writer = undefined;
3285
3372
  }
3286
3373
 
3287
- // Cancel and release reader
3374
+ // Cancel reader - let readLoop's finally block handle releaseLock()
3288
3375
  if (this._reader) {
3289
- const reader = this._reader;
3290
3376
  try {
3291
3377
  // Suppress disconnect event during console mode switching
3292
3378
  this._suppressDisconnect = true;
3293
- await reader.cancel();
3294
- this.logger.debug("Reader cancelled");
3379
+
3380
+ // Cancel will cause readLoop to exit and call releaseLock() in its finally block
3381
+ await this._reader.cancel();
3382
+ this.logger.debug("Reader cancelled - waiting for readLoop to finish");
3383
+
3384
+ // CRITICAL: Wait a bit for readLoop's finally block to complete
3385
+ // The finally block needs time to call releaseLock() and set _reader = undefined
3386
+ // This is much faster than waiting for browser to unlock (just waiting for JS execution)
3387
+ await sleep(50);
3388
+
3389
+ this.logger.debug("ReadLoop cleanup should be complete");
3295
3390
  } catch (err) {
3296
3391
  this.logger.debug(`Reader cancel error: ${err}`);
3297
- } finally {
3298
- try {
3299
- reader.releaseLock();
3300
- } catch (err) {
3301
- this.logger.debug(`Reader release error: ${err}`);
3302
- }
3303
- }
3304
- if (this._reader === reader) {
3305
- this._reader = undefined;
3306
3392
  }
3393
+ // Don't call releaseLock() or set _reader to undefined here
3394
+ // Let readLoop's finally block handle it to avoid race conditions
3307
3395
  }
3308
3396
  }
3309
3397
 
@@ -3352,6 +3440,155 @@ export class ESPLoader extends EventTarget {
3352
3440
  return isUsbJtag;
3353
3441
  }
3354
3442
 
3443
+ public async getUsbMode(): Promise<{
3444
+ mode: "uart" | "usb-jtag-serial" | "usb-otg";
3445
+ uartNo: number;
3446
+ }> {
3447
+ const family = this._parent ? this._parent.chipFamily : this.chipFamily;
3448
+ const revision = this._parent
3449
+ ? (this._parent.chipRevision ?? 0)
3450
+ : (this.chipRevision ?? 0);
3451
+
3452
+ let bufNoAddr: number | null = null;
3453
+ let jtagSerialVal: number | null = null;
3454
+ let otgVal: number | null = null;
3455
+
3456
+ switch (family) {
3457
+ case CHIP_FAMILY_ESP32S2:
3458
+ bufNoAddr = ESP32S2_UARTDEV_BUF_NO;
3459
+ otgVal = ESP32S2_UARTDEV_BUF_NO_USB_OTG;
3460
+ break;
3461
+ case CHIP_FAMILY_ESP32S3:
3462
+ bufNoAddr = ESP32S3_UARTDEV_BUF_NO;
3463
+ jtagSerialVal = ESP32S3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3464
+ otgVal = ESP32S3_UARTDEV_BUF_NO_USB_OTG;
3465
+ break;
3466
+ case CHIP_FAMILY_ESP32C3: {
3467
+ const bssAddr = revision < 101 ? 0x3fcdf064 : 0x3fcdf060;
3468
+ bufNoAddr = bssAddr + ESP32C3_BUF_UART_NO_OFFSET;
3469
+ jtagSerialVal = ESP32C3_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3470
+ break;
3471
+ }
3472
+ case CHIP_FAMILY_ESP32C5:
3473
+ bufNoAddr = ESP32C5_UARTDEV_BUF_NO;
3474
+ jtagSerialVal = ESP32C5_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3475
+ break;
3476
+ case CHIP_FAMILY_ESP32C6:
3477
+ bufNoAddr = ESP32C6_UARTDEV_BUF_NO;
3478
+ jtagSerialVal = ESP32C6_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3479
+ break;
3480
+ case CHIP_FAMILY_ESP32C61:
3481
+ bufNoAddr =
3482
+ revision <= 200
3483
+ ? ESP32C61_UARTDEV_BUF_NO_REV_LE2
3484
+ : ESP32C61_UARTDEV_BUF_NO_REV_GT2;
3485
+ jtagSerialVal =
3486
+ revision <= 200
3487
+ ? ESP32C61_UARTDEV_BUF_NO_USB_JTAG_SERIAL_REV_LE2
3488
+ : ESP32C61_UARTDEV_BUF_NO_USB_JTAG_SERIAL_REV_GT2;
3489
+ break;
3490
+ case CHIP_FAMILY_ESP32H2:
3491
+ bufNoAddr = ESP32H2_UARTDEV_BUF_NO;
3492
+ jtagSerialVal = ESP32H2_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3493
+ break;
3494
+ case CHIP_FAMILY_ESP32H4:
3495
+ bufNoAddr = ESP32H4_UARTDEV_BUF_NO;
3496
+ jtagSerialVal = ESP32H4_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3497
+ break;
3498
+ case CHIP_FAMILY_ESP32P4:
3499
+ bufNoAddr =
3500
+ revision < 300
3501
+ ? ESP32P4_UARTDEV_BUF_NO_REV0
3502
+ : ESP32P4_UARTDEV_BUF_NO_REV300;
3503
+ jtagSerialVal = ESP32P4_UARTDEV_BUF_NO_USB_JTAG_SERIAL;
3504
+ otgVal = ESP32P4_UARTDEV_BUF_NO_USB_OTG;
3505
+ break;
3506
+ }
3507
+
3508
+ if (bufNoAddr === null) {
3509
+ return { mode: "uart", uartNo: 0 };
3510
+ }
3511
+
3512
+ const uartNo = (await this.readRegister(bufNoAddr)) & 0xff;
3513
+
3514
+ if (otgVal !== null && uartNo === otgVal) {
3515
+ this.logger.debug(`USB mode: USB-OTG (uartNo=${uartNo})`);
3516
+ return { mode: "usb-otg", uartNo };
3517
+ }
3518
+ if (jtagSerialVal !== null && uartNo === jtagSerialVal) {
3519
+ this.logger.debug(`USB mode: USB-JTAG/Serial (uartNo=${uartNo})`);
3520
+ return { mode: "usb-jtag-serial", uartNo };
3521
+ }
3522
+
3523
+ this.logger.debug(`USB mode: UART (uartNo=${uartNo})`);
3524
+ return { mode: "uart", uartNo };
3525
+ }
3526
+
3527
+ /**
3528
+ * Check if the current chip supports USB-JTAG or USB-OTG
3529
+ * @returns true if chip has native USB support (JTAG or OTG)
3530
+ */
3531
+ public supportsNativeUsb(): boolean {
3532
+ const family = this._parent ? this._parent.chipFamily : this.chipFamily;
3533
+
3534
+ // Chips with USB-JTAG/Serial or USB-OTG support
3535
+ const usbChips = [
3536
+ CHIP_FAMILY_ESP32S2, // USB-OTG
3537
+ CHIP_FAMILY_ESP32S3, // USB-OTG + USB-JTAG/Serial
3538
+ CHIP_FAMILY_ESP32C3, // USB-JTAG/Serial
3539
+ CHIP_FAMILY_ESP32C5, // USB-JTAG/Serial
3540
+ CHIP_FAMILY_ESP32C6, // USB-JTAG/Serial
3541
+ CHIP_FAMILY_ESP32C61, // USB-JTAG/Serial
3542
+ CHIP_FAMILY_ESP32H2, // USB-JTAG/Serial
3543
+ CHIP_FAMILY_ESP32H4, // USB-JTAG/Serial
3544
+ CHIP_FAMILY_ESP32P4, // USB-OTG + USB-JTAG/Serial
3545
+ ];
3546
+
3547
+ return usbChips.includes(family);
3548
+ }
3549
+
3550
+ /**
3551
+ * @name _ensureStreamsReady
3552
+ * After a hardware reset, ensure port streams are available.
3553
+ * On WebUSB, recreates streams since they break after reset.
3554
+ * On Web Serial, waits for streams to become available.
3555
+ */
3556
+ private async _ensureStreamsReady(): Promise<void> {
3557
+ if (this.isWebUSB()) {
3558
+ try {
3559
+ await (this.port as any).recreateStreams();
3560
+ this.logger.debug("WebUSB streams recreated");
3561
+
3562
+ let retries = 30;
3563
+ while (retries > 0 && !this.port.readable) {
3564
+ await sleep(100);
3565
+ retries--;
3566
+ }
3567
+ if (!this.port.readable) {
3568
+ throw new Error(
3569
+ "Readable stream not available after recreating streams",
3570
+ );
3571
+ }
3572
+ this.logger.debug("WebUSB streams are ready");
3573
+ } catch (err) {
3574
+ this.logger.error(`Failed to recreate WebUSB streams: ${err}`);
3575
+ this._consoleMode = false;
3576
+ throw err;
3577
+ }
3578
+ } else {
3579
+ let retries = 20;
3580
+ while (retries > 0 && !this.port.readable) {
3581
+ await sleep(100);
3582
+ retries--;
3583
+ }
3584
+ if (!this.port.readable) {
3585
+ this._consoleMode = false;
3586
+ throw new Error("Readable stream not available after reset");
3587
+ }
3588
+ this.logger.debug("Port streams are ready");
3589
+ }
3590
+ }
3591
+
3355
3592
  /**
3356
3593
  * @name enterConsoleMode
3357
3594
  * Prepare device for console mode by resetting to firmware
@@ -3384,8 +3621,6 @@ export class ESPLoader extends EventTarget {
3384
3621
  `Cannot enter console mode: USB connection type unknown and detection failed: ${err}`,
3385
3622
  );
3386
3623
  }
3387
- // Set console mode flag
3388
- this._consoleMode = false;
3389
3624
 
3390
3625
  this.logger.debug(
3391
3626
  `USB detection failed, using cached value: ${this.isUsbJtagOrOtg}`,
@@ -3393,56 +3628,40 @@ export class ESPLoader extends EventTarget {
3393
3628
  isUsbJtag = this.isUsbJtagOrOtg;
3394
3629
  }
3395
3630
 
3396
- // Release reader/writer so console can create new ones
3397
- // This is needed for Desktop (Web Serial) to unlock streams
3631
+ // Set console mode flag BEFORE any operations
3632
+ this._consoleMode = true;
3633
+
3398
3634
  if (isUsbJtag) {
3399
- // USB-JTAG/OTG devices: Use watchdog reset which closes port
3635
+ // USB-JTAG/OTG devices: Use reset which may close port
3400
3636
  const wasReset = await this._resetToFirmwareIfNeeded();
3401
- return wasReset; // true = port closed, caller must reopen
3637
+ if (wasReset) {
3638
+ return true; // port closed, caller must reopen
3639
+ }
3640
+
3641
+ // Port stayed open (e.g. C3/C5/C6/H2 classic reset)
3642
+ await this._ensureStreamsReady();
3643
+ return false;
3402
3644
  } else {
3403
3645
  // External serial chip devices: Release locks and do simple reset
3404
3646
  try {
3405
3647
  await this.releaseReaderWriter();
3406
- await this.sleep(100);
3648
+ await sleep(100);
3407
3649
  } catch (err) {
3408
3650
  this.logger.debug(`Failed to release locks: ${err}`);
3409
3651
  }
3410
3652
 
3411
- // Hardware reset to firmware mode (IO0=HIGH)
3412
3653
  try {
3413
- await this.hardReset(false);
3414
- this.logger.log("Device reset to firmware mode");
3654
+ await this.hardResetToFirmware();
3655
+ this.logger.debug("Device reset to firmware mode");
3415
3656
  } catch (err) {
3416
3657
  this.logger.debug(`Could not reset device: ${err}`);
3417
3658
  }
3418
3659
 
3419
- // For WebUSB (Android), recreate streams after hardware reset
3420
- if (this.isWebUSB()) {
3421
- try {
3422
- // Use the public recreateStreams() method to safely recreate streams
3423
- // without closing the port (important after hardware reset)
3424
- await (this.port as any).recreateStreams();
3425
- this.logger.debug("WebUSB streams recreated for console mode");
3426
- } catch (err) {
3427
- // Set console mode flag
3428
- this._consoleMode = false;
3429
- this.logger.debug(`Failed to recreate WebUSB streams: ${err}`);
3430
- }
3431
- }
3432
-
3433
- // Set console mode flag
3434
- this._consoleMode = true;
3435
-
3436
- return false; // Port stays open
3660
+ await this._ensureStreamsReady();
3661
+ return false;
3437
3662
  }
3438
3663
  }
3439
3664
 
3440
- /**
3441
- * @name _resetToFirmwareIfNeeded
3442
- * Reset device from bootloader to firmware when switching to console mode
3443
- * Detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
3444
- * @returns true if reconnect was performed, false otherwise
3445
- */
3446
3665
  /**
3447
3666
  * @name _clearForceDownloadBootIfNeeded
3448
3667
  * Read and clear the force download boot flag if it is set
@@ -3503,7 +3722,15 @@ export class ESPLoader extends EventTarget {
3503
3722
  }
3504
3723
  }
3505
3724
 
3725
+ /**
3726
+ * @name _resetToFirmwareIfNeeded
3727
+ * Reset device from bootloader to firmware when switching to console mode
3728
+ * Detects USB-JTAG/Serial and USB-OTG devices and performs appropriate reset
3729
+ * @returns true if reconnect was performed, false otherwise
3730
+ */
3506
3731
  private async _resetToFirmwareIfNeeded(): Promise<boolean> {
3732
+ // Detect if we need WDT reset (USB-JTAG/OTG) or classic reset
3733
+ const isUsbJtagOrOtg = await this.detectUsbConnectionType();
3507
3734
  try {
3508
3735
  // Check if port is open - if not, assume device is already in firmware mode
3509
3736
  if (!this.port.writable || !this.port.readable) {
@@ -3513,114 +3740,65 @@ export class ESPLoader extends EventTarget {
3513
3740
  return false;
3514
3741
  }
3515
3742
 
3516
- const isUsingUsbOtg = await this.detectUsbConnectionType();
3517
-
3518
- if (isUsingUsbOtg) {
3519
- // For USB-OTG devices, we need to check if force download flag is set
3520
- // Only if it's set, we need WDT reset (which causes port change)
3521
- // If it's clear, we can use normal reset (no port change)
3522
-
3523
- if (this.IS_STUB) {
3524
- this.logger.debug("On stub - need to get back to ROM to check flag");
3525
-
3526
- // If we're running at higher baudrate, we need to change back to ROM baudrate
3527
- if (this.currentBaudRate !== ESP_ROM_BAUD) {
3528
- this.logger.debug(
3529
- `Changing baudrate from ${this.currentBaudRate} to ${ESP_ROM_BAUD} for ROM`,
3530
- );
3531
- try {
3532
- await this.reconfigurePort(ESP_ROM_BAUD);
3533
- this.currentBaudRate = ESP_ROM_BAUD;
3534
- } catch (err) {
3535
- this.logger.debug(`Baudrate change failed: ${err}`);
3536
- // Continue anyway
3537
- }
3538
- }
3539
-
3540
- this.logger.debug("Resetting to bootloader (ROM)...");
3541
-
3542
- // Reset to bootloader - this will clear the stub from RAM
3543
- try {
3544
- await this.hardReset(true);
3545
-
3546
- // Wait for reset to complete
3547
- await sleep(200);
3548
-
3549
- // Sync with ROM
3550
- await this.sync();
3551
-
3552
- this.logger.debug("Now on ROM after reset");
3553
-
3554
- // Mark that we're no longer on stub
3555
- this.IS_STUB = false;
3556
- } catch (resetErr) {
3557
- this.logger.debug(`Reset to ROM failed: ${resetErr}`);
3558
- // If reset fails, we might already be in firmware mode
3559
- // In this case, we don't need to do anything - just use normal reset
3560
- this.logger.debug("Assuming device is already in firmware mode");
3561
-
3562
- // Release reader/writer before returning
3563
- await this.releaseReaderWriter();
3564
- return false; // No port change needed
3565
- }
3566
- } else {
3567
- this.logger.debug("Already on ROM - checking force download flag");
3568
- }
3569
-
3570
- // Now check if force download flag is set and clear it if needed
3571
- const flagWasCleared = await this._clearForceDownloadBootIfNeeded();
3572
-
3573
- if (flagWasCleared) {
3574
- this.logger.debug(
3575
- "Force download flag was cleared - device will boot to firmware after reset",
3576
- );
3577
- } else {
3578
- this.logger.debug(
3579
- "Force download flag already clear - device will boot to firmware after reset",
3580
- );
3581
- }
3582
-
3583
- // Perform WDT reset BEFORE releasing reader/writer (needs communication)
3584
- // After WDT reset, the device will reboot into firmware mode
3585
- await this.hardReset(false);
3586
-
3587
- // For USB-OTG devices (ESP32-S2, ESP32-P4), the port will change after WDT reset
3588
- const portWillChange =
3589
- (this.chipFamily === CHIP_FAMILY_ESP32S2 && isUsingUsbOtg) ||
3590
- (this.chipFamily === CHIP_FAMILY_ESP32P4 && isUsingUsbOtg);
3743
+ if (isUsbJtagOrOtg) {
3744
+ // USB-JTAG/OTG: DON'T release reader/writer before WDT reset
3745
+ // The WDT reset needs active communication to send register write commands
3746
+ // The port will close automatically after the WDT reset anyway
3747
+ this.logger.debug(
3748
+ "USB-JTAG/OTG: Keeping reader/writer active for WDT reset",
3749
+ );
3750
+ } else {
3751
+ // External serial chip: Release reader/writer before classic reset
3752
+ await this.releaseReaderWriter();
3753
+ this.logger.debug(
3754
+ "External serial: Reader/writer released before reset",
3755
+ );
3756
+ }
3591
3757
 
3592
- if (portWillChange) {
3593
- // Port will change - release reader/writer and let the port become invalid
3594
- await this.releaseReaderWriter();
3758
+ // Use the new resetToFirmwareMode method which handles all the logic
3759
+ const portWillChange = await this.resetToFirmwareMode(true);
3595
3760
 
3596
- this.logger.debug(
3597
- `${this.chipName} USB-OTG: Port will change after WDT reset`,
3598
- );
3761
+ if (portWillChange) {
3762
+ this.logger.debug(
3763
+ `${this.chipName}: Port will change after WDT reset - user must reselect port`,
3764
+ );
3599
3765
 
3600
- // Dispatch event to signal port change
3601
- this.dispatchEvent(
3602
- new CustomEvent("usb-otg-port-change", {
3603
- detail: {
3604
- chipName: this.chipName,
3605
- message: `${this.chipName} USB port changed after reset. Please select the new port.`,
3606
- reason: "wdt-reset-to-firmware",
3607
- },
3608
- }),
3609
- );
3766
+ // Dispatch event to signal port change
3767
+ this.dispatchEvent(
3768
+ new CustomEvent("usb-otg-port-change", {
3769
+ detail: {
3770
+ chipName: this.chipName,
3771
+ message: `${this.chipName} USB port changed after reset. Please select the new port.`,
3772
+ reason: "wdt-reset-to-firmware",
3773
+ },
3774
+ }),
3775
+ );
3610
3776
 
3611
- // Return true to indicate port selection is needed
3612
- return true;
3613
- } else {
3614
- // Port stays the same - release reader/writer so console can use the stream
3777
+ return true;
3778
+ } else {
3779
+ // Port stays the same - release reader/writer now if not already done
3780
+ if (isUsbJtagOrOtg) {
3615
3781
  await this.releaseReaderWriter();
3616
- return false;
3782
+ this.logger.debug("Reader/writer released after reset");
3617
3783
  }
3784
+ return false;
3618
3785
  }
3619
3786
  } catch (err) {
3620
- this.logger.debug(`Could not reset device to firmware mode: ${err}`);
3621
- // Continue anyway - console mode might still work
3787
+ this.logger.error(`Reset to firmware mode failed: ${err}`);
3788
+
3789
+ // For USB-JTAG/OTG, the port is likely dead after a failed reset
3790
+ // For external serial, the port is usually still fine
3791
+ if (isUsbJtagOrOtg) {
3792
+ this.logger.debug(
3793
+ "Forcing port reselection due to USB-JTAG/OTG reset failure",
3794
+ );
3795
+ return true;
3796
+ }
3797
+ this.logger.debug(
3798
+ "External serial reset failed, but port should still be usable",
3799
+ );
3800
+ return false;
3622
3801
  }
3623
- return false;
3624
3802
  }
3625
3803
 
3626
3804
  /**
@@ -3921,11 +4099,7 @@ export class ESPLoader extends EventTarget {
3921
4099
  // Perform hardware reset to bootloader (GPIO0=LOW)
3922
4100
  // This will cause the port to change from CDC (firmware) to JTAG (bootloader)
3923
4101
  try {
3924
- if (this.isWebUSB()) {
3925
- await this.hardResetClassicWebUSB();
3926
- } else {
3927
- await this.hardResetClassic();
3928
- }
4102
+ await this.hardResetClassic();
3929
4103
  this.logger.debug("Reset to bootloader initiated");
3930
4104
  } catch (err) {
3931
4105
  this.logger.debug(`Reset error: ${err}`);
@@ -3992,23 +4166,19 @@ export class ESPLoader extends EventTarget {
3992
4166
 
3993
4167
  if (!this.isConsoleResetSupported()) {
3994
4168
  this.logger.debug(
3995
- "Console reset not supported for ESP32-S2 USB-JTAG/CDC",
4169
+ "Simple Console reset not supported for ESP32-S2 USB-JTAG/CDC - using exitConsoleMode to enter bootloader",
4170
+ );
4171
+ await this.exitConsoleMode();
4172
+ this.logger.debug(
4173
+ "S2 now in bootloader mode - caller must do syncAndWdtReset on new port, then reconnect console",
3996
4174
  );
3997
- return; // Do nothing
4175
+ return;
3998
4176
  }
3999
4177
 
4000
4178
  // For other devices: Use standard firmware reset
4001
- const isWebUSB = (this.port as any).isWebUSB === true;
4002
-
4003
4179
  try {
4004
4180
  this.logger.debug("Resetting device in console mode");
4005
-
4006
- if (isWebUSB) {
4007
- await this.hardResetToFirmwareWebUSB();
4008
- } else {
4009
- await this.hardResetToFirmware();
4010
- }
4011
-
4181
+ await this.hardResetToFirmware();
4012
4182
  this.logger.debug("Device reset complete");
4013
4183
  } catch (err) {
4014
4184
  this.logger.error(`Reset failed: ${err}`);
@@ -4016,6 +4186,47 @@ export class ESPLoader extends EventTarget {
4016
4186
  }
4017
4187
  }
4018
4188
 
4189
+ /**
4190
+ * @name syncAndWdtReset
4191
+ * Open a new bootloader port, sync with ROM (no stub, no reset strategies), and fire WDT reset.
4192
+ * This is used for ESP32-S2 USB-OTG devices which require WDT reset to switch modes.
4193
+ * After WDT reset the port will re-enumerate again.
4194
+ * The user must select the new port after this method is called.
4195
+ * @param newPort - The bootloader port selected by the user
4196
+ */
4197
+ async syncAndWdtReset(newPort: SerialPort): Promise<void> {
4198
+ if (this._parent) {
4199
+ await this._parent.syncAndWdtReset(newPort);
4200
+ return;
4201
+ }
4202
+
4203
+ this.port = newPort;
4204
+ this.connected = false;
4205
+ this.IS_STUB = false;
4206
+ this.__inputBuffer = [];
4207
+ this.__inputBufferReadIndex = 0;
4208
+ this.__totalBytesRead = 0;
4209
+
4210
+ this.logger.debug("Opening bootloader port at 115200...");
4211
+ await this.port.open({ baudRate: ESP_ROM_BAUD });
4212
+ this.connected = true;
4213
+ this.currentBaudRate = ESP_ROM_BAUD;
4214
+
4215
+ // Start read loop
4216
+ this.readLoop();
4217
+ await sleep(100);
4218
+
4219
+ // Sync with ROM only - no reset strategies, device is already in bootloader
4220
+ this.logger.debug("Syncing with bootloader ROM...");
4221
+ await this.sync();
4222
+ this.logger.debug("Bootloader sync OK, no stub");
4223
+
4224
+ // Fire WDT reset → device boots into firmware
4225
+ this.logger.debug("Firing WDT reset...");
4226
+ await this.rtcWdtResetChipSpecific();
4227
+ this.logger.debug("WDT reset fired - device will boot to firmware");
4228
+ }
4229
+
4019
4230
  /**
4020
4231
  * @name drainInputBuffer
4021
4232
  * Actively drain the input buffer by reading data for a specified time.