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/js/script.js CHANGED
@@ -9,13 +9,14 @@ if (!globalThis.requestSerialPort) {
9
9
  }
10
10
 
11
11
  // Utility functions imported from esptool module
12
- let toHex, formatMacAddr, sleep;
12
+ let toHex, formatMacAddr, sleep, CHIP_FAMILY_ESP32S2;
13
13
 
14
14
  // Load utilities from esptool package
15
15
  window.esptoolPackage.then((esptoolMod) => {
16
16
  toHex = esptoolMod.toHex;
17
17
  formatMacAddr = esptoolMod.formatMacAddr;
18
18
  sleep = esptoolMod.sleep;
19
+ CHIP_FAMILY_ESP32S2 = esptoolMod.CHIP_FAMILY_ESP32S2;
19
20
  });
20
21
 
21
22
  let espStub;
@@ -36,6 +37,7 @@ let espLoaderBeforeConsole = null; // Store original ESPLoader before console
36
37
  let chipFamilyBeforeConsole = null; // Store chipFamily before opening console
37
38
  let consoleResetHandler = null;
38
39
  let consoleCloseHandler = null;
40
+ let consoleBootloaderHandlerModule = null;
39
41
 
40
42
  /**
41
43
  * Get display name for current filesystem type
@@ -368,16 +370,16 @@ document.addEventListener("DOMContentLoaded", () => {
368
370
  // Initialize upload rows visibility - only show first row
369
371
  updateUploadRowsVisibility();
370
372
 
371
- autoscroll.addEventListener("click", clickAutoscroll);
373
+ bindCheckboxSetting(autoscroll, "autoscroll");
372
374
  consoleSwitch.addEventListener("click", clickConsole);
373
375
  baudRateSelect.addEventListener("change", changeBaudRate);
374
376
  advancedMode.addEventListener("change", clickAdvancedMode);
375
377
  chunkSizeSelect.addEventListener("change", changeAdvancedParam);
376
378
  blockSizeSelect.addEventListener("change", changeAdvancedParam);
377
379
  maxInFlightSelect.addEventListener("change", changeAdvancedParam);
378
- darkMode.addEventListener("click", clickDarkMode);
379
- debugMode.addEventListener("click", clickDebugMode);
380
- showLog.addEventListener("click", clickShowLog);
380
+ bindCheckboxSetting(darkMode, "darkmode", () => updateTheme());
381
+ bindCheckboxSetting(debugMode, "debugmode", (v) => logMsg("Debug mode " + (v ? "enabled" : "disabled")));
382
+ bindCheckboxSetting(showLog, "showlog", () => updateLogVisibility());
381
383
  window.addEventListener("error", function (event) {
382
384
  console.log("Got an uncaught error: ", event.error);
383
385
  });
@@ -811,75 +813,95 @@ async function changeBaudRate() {
811
813
  }
812
814
  }
813
815
 
814
- /**
815
- * @name clickAutoscroll
816
- * Change handler for the Autoscroll checkbox.
817
- */
818
- async function clickAutoscroll() {
819
- saveSetting("autoscroll", autoscroll.checked);
820
- }
821
-
822
- /**
823
- * @name clickDarkMode
824
- * Change handler for the Dark Mode checkbox.
825
- */
826
- async function clickDarkMode() {
827
- updateTheme();
828
- saveSetting("darkmode", darkMode.checked);
816
+ function bindCheckboxSetting(checkbox, key, onChange) {
817
+ checkbox.addEventListener("click", () => {
818
+ saveSetting(key, checkbox.checked);
819
+ if (onChange) onChange(checkbox.checked);
820
+ });
829
821
  }
830
822
 
831
823
  /**
832
- * @name clickDebugMode
833
- * Change handler for the Debug Mode checkbox.
824
+ * @name requestPort
825
+ * Request a serial port from the user (WebUSB or Web Serial)
834
826
  */
835
- async function clickDebugMode() {
836
- saveSetting("debugmode", debugMode.checked);
837
- logMsg("Debug mode " + (debugMode.checked ? "enabled" : "disabled"));
827
+ async function requestPort() {
828
+ return isUsingWebUSB()
829
+ ? await WebUSBSerial.requestPort((...args) => logMsg(...args))
830
+ : await navigator.serial.requestPort();
838
831
  }
839
832
 
840
833
  /**
841
- * @name clickShowLog
842
- * Change handler for the Show Log checkbox.
834
+ * @name showS2Modal
835
+ * Show the ESP32-S2 modal with given title/text and return a Promise
836
+ * that resolves when the user clicks the reconnect button.
843
837
  */
844
- async function clickShowLog() {
845
- saveSetting("showlog", showLog.checked);
846
- updateLogVisibility();
838
+ function showS2Modal(title, text) {
839
+ return new Promise((resolve) => {
840
+ const modal = document.getElementById("esp32s2Modal");
841
+ const reconnectBtn = document.getElementById("butReconnectS2");
842
+ const modalTitle = modal.querySelector("h2");
843
+ const modalText = modal.querySelector("p");
844
+ if (modalTitle) modalTitle.textContent = title;
845
+ if (modalText) modalText.textContent = text;
846
+ modal.classList.remove("hidden");
847
+ reconnectBtn.addEventListener("click", () => {
848
+ modal.classList.add("hidden");
849
+ resolve();
850
+ }, { once: true });
851
+ });
847
852
  }
848
853
 
849
854
  /**
850
- * @name openConsolePortAndInit
851
- * Helper to open port for console and initialize console UI
852
- * Avoids code duplication across different console init flows
855
+ * @name assignPort
856
+ * Assign a new port to espStub, its parent, and espLoaderBeforeConsole
853
857
  */
854
- async function openConsolePortAndInit(newPort) {
855
- // Open the port at 115200 for console
856
- await newPort.open({ baudRate: 115200 });
858
+ function assignPort(newPort) {
857
859
  espStub.port = newPort;
858
860
  espStub.connected = true;
859
-
860
- // Keep parent/loader in sync (used by closeConsole)
861
+ espStub._writer = undefined;
862
+ espStub._reader = undefined;
861
863
  if (espStub._parent) {
862
864
  espStub._parent.port = newPort;
863
865
  }
864
866
  if (espLoaderBeforeConsole) {
865
867
  espLoaderBeforeConsole.port = newPort;
868
+ espLoaderBeforeConsole.connected = true;
866
869
  }
870
+ }
871
+
872
+ /**
873
+ * @name openConsolePortAndInit
874
+ * Helper to open port for console and initialize console UI
875
+ * @param {SerialPort} newPort - The port to open
876
+ */
877
+ async function openConsolePortAndInit(newPort) {
878
+ await newPort.open({ baudRate: 115200 });
879
+ assignPort(newPort);
867
880
 
868
881
  debugMsg("Port opened for console at 115200 baud");
869
882
 
870
- // Device is already in firmware mode, port is open at 115200
871
- // Initialize console directly
883
+ // WebUSB needs extra time for USB interface and streams to stabilize
884
+ if (isUsingWebUSB()) {
885
+ let retries = 30;
886
+ while (retries > 0 && !newPort.readable) {
887
+ await sleep(100);
888
+ retries--;
889
+ }
890
+ if (!newPort.readable) {
891
+ throw new Error("Port readable stream not available after open");
892
+ }
893
+ debugMsg("WebUSB port streams ready for console");
894
+ }
895
+
872
896
  consoleSwitch.checked = true;
873
897
  saveSetting("console", true);
874
898
 
875
- // Initialize console UI and handlers
876
899
  await initConsoleUI();
877
900
  }
878
901
 
879
902
  /**
880
903
  * @name initConsoleUI
881
904
  * Initialize console UI, event handlers, and start console instance
882
- * Extracted helper to avoid duplication across different console init flows
883
905
  */
884
906
  async function initConsoleUI() {
885
907
  // Wait for port to be ready
@@ -897,20 +919,6 @@ async function initConsoleUI() {
897
919
  consoleInstance = new ESP32ToolConsole(espStub.port, consoleContainer, true);
898
920
  await consoleInstance.init();
899
921
 
900
- // Check if console reset is supported and hide button if not
901
- if (espLoaderBeforeConsole && typeof espLoaderBeforeConsole.isConsoleResetSupported === 'function') {
902
- const resetSupported = espLoaderBeforeConsole.isConsoleResetSupported();
903
- const resetBtn = consoleContainer.querySelector("#console-reset-btn");
904
- if (resetBtn) {
905
- if (resetSupported) {
906
- resetBtn.style.display = "";
907
- } else {
908
- resetBtn.style.display = "none";
909
- debugMsg("Console reset disabled for ESP32-S2 USB-JTAG/CDC (hardware limitation)");
910
- }
911
- }
912
- }
913
-
914
922
  // Listen for console reset events
915
923
  if (consoleResetHandler) {
916
924
  consoleContainer.removeEventListener('console-reset', consoleResetHandler);
@@ -918,9 +926,45 @@ async function initConsoleUI() {
918
926
  consoleResetHandler = async () => {
919
927
  if (espLoaderBeforeConsole && typeof espLoaderBeforeConsole.resetInConsoleMode === 'function') {
920
928
  try {
921
- debugMsg("Resetting device from console...");
922
- await espLoaderBeforeConsole.resetInConsoleMode();
923
- debugMsg("Device reset successful");
929
+ const isS2 = chipFamilyBeforeConsole === CHIP_FAMILY_ESP32S2;
930
+
931
+ if (isS2) {
932
+ debugMsg("ESP32-S2 console reset: entering bootloader, then WDT reset to firmware...");
933
+
934
+ if (consoleInstance) {
935
+ await consoleInstance.disconnect();
936
+ }
937
+
938
+ await espLoaderBeforeConsole.resetInConsoleMode();
939
+
940
+ const waitTime = isUsingWebUSB() ? 1000 : 500;
941
+ await sleep(waitTime);
942
+
943
+ // Step 1: Select bootloader port for WDT reset
944
+ const portLabel = isUsingWebUSB() ? "USB device" : "serial port";
945
+ await showS2Modal("Select bootloader port", `Select the ${portLabel} (bootloader) to reset the device.`);
946
+ const bootloaderPort = await requestPort();
947
+
948
+ debugMsg("Syncing with bootloader and performing WDT reset...");
949
+ await espLoaderBeforeConsole.syncAndWdtReset(bootloaderPort);
950
+ try { await bootloaderPort.close(); } catch (e) { /* port may already be gone */ }
951
+
952
+ debugMsg("Waiting for device to boot into firmware...");
953
+ await sleep(waitTime);
954
+
955
+ // Step 2: Select firmware port for console
956
+ await showS2Modal("Select console port", `Select the ${portLabel} (firmware) for console.`);
957
+ const consolePort = await requestPort();
958
+
959
+ await consolePort.open({ baudRate: 115200 });
960
+ assignPort(consolePort);
961
+ await consoleInstance.reconnect(consolePort);
962
+ debugMsg("Console reconnected after S2 reset");
963
+ } else {
964
+ debugMsg("Resetting device from console...");
965
+ await espLoaderBeforeConsole.resetInConsoleMode();
966
+ debugMsg("Device reset successful");
967
+ }
924
968
  } catch (err) {
925
969
  errorMsg("Failed to reset device: " + err.message);
926
970
  }
@@ -941,6 +985,29 @@ async function initConsoleUI() {
941
985
  };
942
986
  consoleContainer.addEventListener('console-close', consoleCloseHandler);
943
987
 
988
+ // Listen for console bootloader detection events
989
+ // The console detects bootloader patterns in real-time as data arrives
990
+ // and dispatches this event when bootloader is detected
991
+ if (consoleBootloaderHandlerModule) {
992
+ consoleContainer.removeEventListener('console-bootloader', consoleBootloaderHandlerModule);
993
+ }
994
+ consoleBootloaderHandlerModule = async () => {
995
+ logMsg("Console detected bootloader mode - resetting to firmware...");
996
+ if (espLoaderBeforeConsole && typeof espLoaderBeforeConsole.resetInConsoleMode === 'function') {
997
+ try {
998
+ await espLoaderBeforeConsole.resetInConsoleMode();
999
+ logMsg("✅ Device reset to firmware mode");
1000
+ // Clear console to see new output after reset
1001
+ if (consoleInstance && typeof consoleInstance.clear === 'function') {
1002
+ consoleInstance.clear();
1003
+ }
1004
+ } catch (err) {
1005
+ errorMsg("Failed to reset device: " + err.message);
1006
+ }
1007
+ }
1008
+ };
1009
+ consoleContainer.addEventListener('console-bootloader', consoleBootloaderHandlerModule);
1010
+
944
1011
  logMsg("Console initialized");
945
1012
  }
946
1013
 
@@ -994,7 +1061,7 @@ async function clickConsole() {
994
1061
 
995
1062
  // Check if this is ESP32-S2 or if we're on Android (WebUSB)
996
1063
  // Both need modal for user gesture
997
- const isS2 = chipFamilyBeforeConsole === 0x3252; // CHIP_FAMILY_ESP32S2 = 0x3252
1064
+ const isS2 = chipFamilyBeforeConsole === CHIP_FAMILY_ESP32S2;
998
1065
  const needsModal = isS2 || isWebUSB;
999
1066
 
1000
1067
  if (needsModal) {
@@ -1022,58 +1089,20 @@ async function clickConsole() {
1022
1089
  // Wait for browser to process port closure and USB re-enumeration
1023
1090
  await sleep(300);
1024
1091
 
1025
- // Show modal for port selection (requires user gesture)
1026
- const modal = document.getElementById("esp32s2Modal");
1027
- const reconnectBtn = document.getElementById("butReconnectS2");
1028
-
1029
- // Update modal text for console mode
1030
- const modalTitle = modal.querySelector("h2");
1031
- const modalText = modal.querySelector("p");
1032
- if (modalTitle) modalTitle.textContent = "Device has been reset to firmware mode";
1033
- if (modalText) {
1034
- modalText.textContent = isWebUSB
1035
- ? "Please click the button below to select the USB device for console."
1036
- : "Please click the button below to select the serial port for console.";
1037
- }
1038
-
1039
- modal.classList.remove("hidden");
1040
-
1041
- // Handle reconnect button click (single-fire to prevent multiple prompts)
1042
- const handleReconnect = async () => {
1043
- modal.classList.add("hidden");
1044
-
1045
- try {
1046
- // Request the NEW port (user gesture from button click)
1047
- debugMsg("Please select the port for console mode...");
1048
- const newPort = isWebUSB
1049
- ? await WebUSBSerial.requestPort((...args) => logMsg(...args))
1050
- : await navigator.serial.requestPort();
1051
-
1052
- // Use helper to open port and initialize console
1053
- await openConsolePortAndInit(newPort);
1054
- } catch (err) {
1055
- errorMsg(`Failed to open port for console: ${err.message}`);
1056
- consoleSwitch.checked = false;
1057
- saveSetting("console", false);
1058
- }
1059
- };
1060
-
1061
- // Use { once: true } to ensure single-fire and automatic cleanup
1062
- reconnectBtn.addEventListener("click", handleReconnect, { once: true });
1063
- } else {
1064
- // Desktop (Web Serial) with ESP32-S3/C3/C5/C6/H2/P4: Direct requestPort
1065
- try {
1066
- // Request port selection from user (direct)
1067
- debugMsg("Please select the serial port again for console mode...");
1068
- const newPort = await navigator.serial.requestPort();
1069
-
1070
- // Use helper to open port and initialize console
1071
- await openConsolePortAndInit(newPort);
1072
- } catch (err) {
1073
- errorMsg(`Failed to open port for console: ${err.message}`);
1074
- consoleSwitch.checked = false;
1075
- saveSetting("console", false);
1076
- }
1092
+ const portLabel = isWebUSB ? "USB device" : "serial port";
1093
+ await showS2Modal(
1094
+ "Device has been reset to firmware mode",
1095
+ `Please click the button below to select the ${portLabel} for console.`
1096
+ );
1097
+ }
1098
+
1099
+ try {
1100
+ const newPort = await requestPort();
1101
+ await openConsolePortAndInit(newPort);
1102
+ } catch (err) {
1103
+ errorMsg(`Failed to open port for console: ${err.message}`);
1104
+ consoleSwitch.checked = false;
1105
+ saveSetting("console", false);
1077
1106
  }
1078
1107
 
1079
1108
  return;
@@ -1091,7 +1120,7 @@ async function clickConsole() {
1091
1120
  // Wait for:
1092
1121
  // - Firmware to start after reset
1093
1122
  // - Port to be ready for new reader
1094
- await sleep(200);
1123
+ await sleep(500);
1095
1124
 
1096
1125
  // Initialize console UI and handlers
1097
1126
  await initConsoleUI();
@@ -1144,6 +1173,20 @@ async function closeConsole() {
1144
1173
  consoleInstance = null;
1145
1174
  }
1146
1175
 
1176
+ // Remove console event handlers
1177
+ if (consoleResetHandler) {
1178
+ consoleContainer.removeEventListener('console-reset', consoleResetHandler);
1179
+ consoleResetHandler = null;
1180
+ }
1181
+ if (consoleCloseHandler) {
1182
+ consoleContainer.removeEventListener('console-close', consoleCloseHandler);
1183
+ consoleCloseHandler = null;
1184
+ }
1185
+ if (consoleBootloaderHandlerModule) {
1186
+ consoleContainer.removeEventListener('console-bootloader', consoleBootloaderHandlerModule);
1187
+ consoleBootloaderHandlerModule = null;
1188
+ }
1189
+
1147
1190
  // Use esp_loader's exitConsoleMode function
1148
1191
  try {
1149
1192
  const needsManualReconnect = await espLoaderBeforeConsole.exitConsoleMode();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esp32tool",
3
- "version": "1.3.7",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "description": "Flash & Read ESP devices using WebSerial, Electron, and also Android mobile via WebUSB",
6
6
  "main": "electron/main.cjs",
Binary file
Binary file
package/src/console.ts CHANGED
@@ -179,19 +179,36 @@ export class ESP32ToolConsole {
179
179
  private async _connect(abortSignal: AbortSignal) {
180
180
  console.log("Starting console read loop");
181
181
 
182
- // Check if port.readable is available
183
- if (!this.port.readable) {
184
- this.console!.addLine("");
185
- this.console!.addLine("");
186
- this.console!.addLine(
187
- `Terminal disconnected: Port readable stream not available`,
188
- );
189
- console.error(
190
- "Port readable stream not available - port may need to be reopened at correct baudrate",
191
- );
192
- return;
182
+ // Wait for readable stream to be available with timeout
183
+ const maxWaitTime = 3000; // 3 seconds
184
+ const startTime = Date.now();
185
+
186
+ while (!this.port.readable) {
187
+ const elapsed = Date.now() - startTime;
188
+ if (elapsed > maxWaitTime) {
189
+ this.console!.addLine("");
190
+ this.console!.addLine("");
191
+ this.console!.addLine(
192
+ `Terminal disconnected: Port readable stream not available after ${maxWaitTime}ms`,
193
+ );
194
+ this.console!.addLine(`This can happen if:`);
195
+ this.console!.addLine(
196
+ `1. Port was just opened and streams are not ready yet`,
197
+ );
198
+ this.console!.addLine(
199
+ `2. Device was reset and port needs to be reopened`,
200
+ );
201
+ this.console!.addLine(`3. USB device re-enumerated after reset`);
202
+ console.error(
203
+ "Port readable stream not available - port may need to be reopened at correct baudrate",
204
+ );
205
+ return;
206
+ }
207
+ await new Promise((resolve) => setTimeout(resolve, 50));
193
208
  }
194
209
 
210
+ console.log("Port readable stream is ready - starting console");
211
+
195
212
  try {
196
213
  await this.port
197
214
  .readable!.pipeThrough(
package/src/const.ts CHANGED
@@ -286,6 +286,11 @@ export const ESP32C61_SPI_MISO_DLEN_OFFS = 0x28;
286
286
  export const ESP32C61_SPI_W0_OFFS = 0x58;
287
287
  export const ESP32C61_UART_DATE_REG_ADDR = 0x6000007c;
288
288
  export const ESP32C61_BOOTLOADER_FLASH_OFFSET = 0x0000;
289
+ // ESP32-C61 USB-JTAG/Serial detection (dynamic based on chip revision)
290
+ export const ESP32C61_UARTDEV_BUF_NO_REV_LE2 = 0x4084f5ec; // revision <= 2
291
+ export const ESP32C61_UARTDEV_BUF_NO_REV_GT2 = 0x4084f5e4; // revision > 2
292
+ export const ESP32C61_UARTDEV_BUF_NO_USB_JTAG_SERIAL_REV_LE2 = 3; // revision <= 2
293
+ export const ESP32C61_UARTDEV_BUF_NO_USB_JTAG_SERIAL_REV_GT2 = 4; // revision > 2
289
294
 
290
295
  export const ESP32H2_SPI_REG_BASE = 0x60003000;
291
296
  export const ESP32H2_BASEFUSEADDR = 0x600b0800;
@@ -335,6 +340,9 @@ export const ESP32H4_RTC_CNTL_WDTCONFIG1_REG =
335
340
  ESP32H4_DR_REG_LP_WDT_BASE + 0x0004; // LP_WDT_RWDT_CONFIG1_REG
336
341
  export const ESP32H4_RTC_CNTL_WDT_WKEY = 0x50d83aa1; // LP_WDT_SWD_WKEY, same as WDT key in this case
337
342
  export const ESP32H4_RTC_CNTL_SWD_WKEY = 0x50d83aa1; // LP_WDT_SWD_WKEY, same as WDT key in this case
343
+ // ESP32-H4 USB-JTAG/Serial detection
344
+ export const ESP32H4_UARTDEV_BUF_NO = 0x4087f580; // Variable in ROM .bss which indicates the port in use
345
+ export const ESP32H4_UARTDEV_BUF_NO_USB_JTAG_SERIAL = 3; // The above var when USB-JTAG/Serial is used
338
346
 
339
347
  export const ESP32H21_SPI_REG_BASE = 0x60003000;
340
348
  export const ESP32H21_BASEFUSEADDR = 0x600b4000;