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.
Files changed (64) hide show
  1. package/.nojekyll +0 -0
  2. package/README.md +100 -6
  3. package/apple-touch-icon.png +0 -0
  4. package/build-electron-cli.cjs +177 -0
  5. package/build-single-binary.cjs +295 -0
  6. package/css/light.css +11 -0
  7. package/css/style.css +261 -41
  8. package/dist/cli.d.ts +17 -0
  9. package/dist/cli.js +458 -0
  10. package/dist/console.d.ts +15 -0
  11. package/dist/console.js +237 -0
  12. package/dist/const.d.ts +99 -0
  13. package/dist/const.js +129 -8
  14. package/dist/esp_loader.d.ts +244 -22
  15. package/dist/esp_loader.js +1960 -251
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.js +37 -4
  18. package/dist/node-usb-adapter.d.ts +47 -0
  19. package/dist/node-usb-adapter.js +725 -0
  20. package/dist/stubs/index.d.ts +1 -2
  21. package/dist/stubs/index.js +4 -0
  22. package/dist/util/console-color.d.ts +19 -0
  23. package/dist/util/console-color.js +272 -0
  24. package/dist/util/line-break-transformer.d.ts +5 -0
  25. package/dist/util/line-break-transformer.js +17 -0
  26. package/dist/web/index.js +1 -1
  27. package/electron/cli-main.cjs +74 -0
  28. package/electron/main.cjs +338 -0
  29. package/electron/main.js +7 -2
  30. package/favicon.ico +0 -0
  31. package/fix-cli-imports.cjs +127 -0
  32. package/generate-icons.sh +89 -0
  33. package/icons/icon-128.png +0 -0
  34. package/icons/icon-144.png +0 -0
  35. package/icons/icon-152.png +0 -0
  36. package/icons/icon-192.png +0 -0
  37. package/icons/icon-384.png +0 -0
  38. package/icons/icon-512.png +0 -0
  39. package/icons/icon-72.png +0 -0
  40. package/icons/icon-96.png +0 -0
  41. package/index.html +143 -73
  42. package/install-android.html +411 -0
  43. package/js/console.js +269 -0
  44. package/js/modules/esptool.js +1 -1
  45. package/js/script.js +750 -175
  46. package/js/util/console-color.js +282 -0
  47. package/js/util/line-break-transformer.js +19 -0
  48. package/js/webusb-serial.js +1017 -0
  49. package/license.md +1 -1
  50. package/manifest.json +89 -0
  51. package/package.cli.json +29 -0
  52. package/package.json +35 -24
  53. package/screenshots/desktop.png +0 -0
  54. package/screenshots/mobile.png +0 -0
  55. package/src/cli.ts +618 -0
  56. package/src/console.ts +278 -0
  57. package/src/const.ts +165 -8
  58. package/src/esp_loader.ts +2354 -302
  59. package/src/index.ts +69 -3
  60. package/src/node-usb-adapter.ts +924 -0
  61. package/src/stubs/index.ts +4 -1
  62. package/src/util/console-color.ts +290 -0
  63. package/src/util/line-break-transformer.ts +20 -0
  64. package/sw.js +155 -0
package/js/script.js CHANGED
@@ -1,3 +1,13 @@
1
+ // Import WebUSB serial support for Android compatibility
2
+ import { WebUSBSerial, requestSerialPort } from './webusb-serial.js';
3
+ import { ESP32ToolConsole } from './console.js';
4
+
5
+ // Make requestSerialPort available globally for esptool.js
6
+ // Use defensive assignment to avoid accidental overwrites
7
+ if (!globalThis.requestSerialPort) {
8
+ globalThis.requestSerialPort = requestSerialPort;
9
+ }
10
+
1
11
  let espStub;
2
12
  let esp32s2ReconnectInProgress = false;
3
13
  let currentLittleFS = null;
@@ -8,6 +18,13 @@ let currentFilesystemType = null; // 'littlefs', 'fatfs', or 'spiffs'
8
18
  let littlefsModulePromise = null; // Cache for LittleFS WASM module
9
19
  let lastReadFlashData = null; // Store last read flash data for ESP8266
10
20
  let currentChipName = null; // Store chip name globally
21
+ let isConnected = false; // Track connection state
22
+ let consoleInstance = null; // ESP32ToolConsole instance
23
+ let baudRateBeforeConsole = null; // Store baudrate before opening console
24
+ let espLoaderBeforeConsole = null; // Store original ESPLoader before console
25
+ let chipFamilyBeforeConsole = null; // Store chipFamily before opening console
26
+ let consoleResetHandler = null;
27
+ let consoleCloseHandler = null;
11
28
 
12
29
  /**
13
30
  * Get display name for current filesystem type
@@ -34,7 +51,7 @@ function clearAllCachedData() {
34
51
  currentLittleFS.destroy();
35
52
  }
36
53
  } catch (e) {
37
- console.error('Error destroying filesystem:', e);
54
+ debugMsg('Error destroying filesystem: ' + e);
38
55
  }
39
56
  }
40
57
 
@@ -86,6 +103,55 @@ function clearAllCachedData() {
86
103
  }
87
104
 
88
105
  const baudRates = [2000000, 1500000, 921600, 500000, 460800, 230400, 153600, 128000, 115200];
106
+
107
+ // Advanced read flash parameters
108
+ // chunkSize: Amount of data to request from ESP in one command (in KB)
109
+ const chunkSizes = [
110
+ { label: "4 KB", value: 0x1000 },
111
+ { label: "8 KB", value: 0x2000 },
112
+ { label: "16 KB (WebUSB)", value: 0x4000 },
113
+ { label: "64 KB", value: 0x10000 },
114
+ { label: "128 KB", value: 0x20000 },
115
+ { label: "256 KB (Desktop)", value: 0x40000 }
116
+ ];
117
+
118
+ // blockSize: Size of each data block sent by ESP (in bytes)
119
+ const blockSizes = [
120
+ { label: "31 B (Android)", value: 31 },
121
+ { label: "62 B", value: 62 },
122
+ { label: "124 B", value: 124 },
123
+ { label: "248 B (CDC)", value: 248 },
124
+ { label: "256 B", value: 256 },
125
+ { label: "496 B", value: 496 },
126
+ { label: "512 B", value: 512 },
127
+ { label: "992 B", value: 992 },
128
+ { label: "1024 B", value: 1024 },
129
+ { label: "1984 B", value: 1984 },
130
+ { label: "2024 B", value: 2024 },
131
+ { label: "3968 B (Desktop)", value: 3968 },
132
+ { label: "4096 B (Maximum)", value: 4096 }
133
+ ];
134
+
135
+ // maxInFlight: Maximum unacknowledged bytes (in bytes)
136
+ const maxInFlights = [
137
+ { label: "31 B (Android)", value: 31 },
138
+ { label: "62 B", value: 62 },
139
+ { label: "124 B", value: 124 },
140
+ { label: "248 B (Android CDC)", value: 248 },
141
+ { label: "512 B", value: 512 },
142
+ { label: "992 B", value: 992 },
143
+ { label: "1024 B", value: 1024 },
144
+ { label: "1984 B", value: 1984 },
145
+ { label: "2024 B", value: 2024 },
146
+ { label: "3968 B", value: 3968 },
147
+ { label: "4096 B", value: 4096 },
148
+ { label: "7936 B", value: 7936 },
149
+ { label: "8192 B", value: 8192 },
150
+ { label: "15872 B", value: 15872 },
151
+ { label: "31744 B", value: 31744 },
152
+ { label: "63488 B", value: 63488 }
153
+ ];
154
+
89
155
  const bufferSize = 512;
90
156
  const colors = ["#00a7e9", "#f89521", "#be1e2d"];
91
157
  const measurementPeriodId = "0001";
@@ -96,7 +162,13 @@ const isElectron = window.electronAPI && window.electronAPI.isElectron;
96
162
  const maxLogLength = 100;
97
163
  const log = document.getElementById("log");
98
164
  const butConnect = document.getElementById("butConnect");
99
- const baudRate = document.getElementById("baudRate");
165
+ const baudRateSelect = document.getElementById("baudRate");
166
+ const advancedMode = document.getElementById("advanced");
167
+ const advancedRow = document.querySelector(".advanced-row");
168
+ const main = document.querySelector(".main");
169
+ const chunkSizeSelect = document.getElementById("chunkSize");
170
+ const blockSizeSelect = document.getElementById("blockSize");
171
+ const maxInFlightSelect = document.getElementById("maxInFlight");
100
172
  const butClear = document.getElementById("butClear");
101
173
  const butErase = document.getElementById("butErase");
102
174
  const butProgram = document.getElementById("butProgram");
@@ -125,6 +197,8 @@ const littlefsFileInput = document.getElementById("littlefsFileInput");
125
197
  const butLittlefsUpload = document.getElementById("butLittlefsUpload");
126
198
  const butLittlefsMkdir = document.getElementById("butLittlefsMkdir");
127
199
  const autoscroll = document.getElementById("autoscroll");
200
+ const consoleSwitch = document.getElementById("console");
201
+ const consoleContainer = document.getElementById("console-container");
128
202
  const lightSS = document.getElementById("light");
129
203
  const darkSS = document.getElementById("dark");
130
204
  const darkMode = document.getElementById("darkmode");
@@ -150,7 +224,7 @@ let currentViewedFileData = null;
150
224
  document.addEventListener("DOMContentLoaded", () => {
151
225
  butConnect.addEventListener("click", () => {
152
226
  clickConnect().catch(async (e) => {
153
- console.error(e);
227
+ debugMsg('Connection error: ' + e);
154
228
  errorMsg(e.message || e);
155
229
  if (espStub) {
156
230
  await espStub.disconnect();
@@ -190,7 +264,12 @@ document.addEventListener("DOMContentLoaded", () => {
190
264
  updateUploadRowsVisibility();
191
265
 
192
266
  autoscroll.addEventListener("click", clickAutoscroll);
193
- baudRate.addEventListener("change", changeBaudRate);
267
+ consoleSwitch.addEventListener("click", clickConsole);
268
+ baudRateSelect.addEventListener("change", changeBaudRate);
269
+ advancedMode.addEventListener("change", clickAdvancedMode);
270
+ chunkSizeSelect.addEventListener("change", changeAdvancedParam);
271
+ blockSizeSelect.addEventListener("change", changeAdvancedParam);
272
+ maxInFlightSelect.addEventListener("change", changeAdvancedParam);
194
273
  darkMode.addEventListener("click", clickDarkMode);
195
274
  debugMode.addEventListener("click", clickDebugMode);
196
275
  showLog.addEventListener("click", clickShowLog);
@@ -198,10 +277,11 @@ document.addEventListener("DOMContentLoaded", () => {
198
277
  console.log("Got an uncaught error: ", event.error);
199
278
  });
200
279
 
201
- // Header auto-hide functionality
280
+ // Header auto-hide functionality - DISABLED
202
281
  const header = document.querySelector(".header");
203
282
  const main = document.querySelector(".main");
204
283
 
284
+ /* DISABLED: Auto-hide header
205
285
  // Show header on mouse enter at top of page
206
286
  main.addEventListener("mousemove", (e) => {
207
287
  if (e.clientY < 5 && header.classList.contains("header-hidden")) {
@@ -227,13 +307,16 @@ document.addEventListener("DOMContentLoaded", () => {
227
307
  }, 1000);
228
308
  }
229
309
  });
310
+ */
230
311
 
231
- if ("serial" in navigator) {
312
+ // Check for Web Serial or WebUSB support
313
+ if ("serial" in navigator || "usb" in navigator) {
232
314
  const notSupported = document.getElementById("notSupported");
233
315
  notSupported.classList.add("hidden");
234
316
  }
235
317
 
236
318
  initBaudRate();
319
+ initAdvancedParams();
237
320
  loadAllSettings();
238
321
  updateTheme();
239
322
  logMsg("ESP32Tool loaded.");
@@ -244,10 +327,42 @@ function initBaudRate() {
244
327
  var option = document.createElement("option");
245
328
  option.text = rate + " Baud";
246
329
  option.value = rate;
247
- baudRate.add(option);
330
+ baudRateSelect.add(option);
248
331
  }
249
332
  }
250
333
 
334
+ function initAdvancedParams() {
335
+ // Initialize chunkSize dropdown
336
+ for (let item of chunkSizes) {
337
+ const option = document.createElement("option");
338
+ option.text = item.label;
339
+ option.value = item.value;
340
+ chunkSizeSelect.add(option);
341
+ }
342
+ // Set default: 16 KB for WebUSB, 256 KB for Desktop
343
+ chunkSizeSelect.value = 0x4000; // 16 KB default
344
+
345
+ // Initialize blockSize dropdown
346
+ for (let item of blockSizes) {
347
+ const option = document.createElement("option");
348
+ option.text = item.label;
349
+ option.value = item.value;
350
+ blockSizeSelect.add(option);
351
+ }
352
+ // Set default: 4095 B for Desktop
353
+ blockSizeSelect.value = 4095;
354
+
355
+ // Initialize maxInFlight dropdown
356
+ for (let item of maxInFlights) {
357
+ const option = document.createElement("option");
358
+ option.text = item.label;
359
+ option.value = item.value;
360
+ maxInFlightSelect.add(option);
361
+ }
362
+ // Set default: 8190 B for Desktop
363
+ maxInFlightSelect.value = 8190;
364
+ }
365
+
251
366
  function logMsg(text) {
252
367
  log.innerHTML += text + "<br>";
253
368
 
@@ -266,31 +381,8 @@ function debugMsg(...args) {
266
381
  if (!debugMode.checked) {
267
382
  return;
268
383
  }
269
-
270
- function getStackTrace() {
271
- let stack = new Error().stack;
272
- //console.log(stack);
273
- stack = stack.split("\n").map((v) => v.trim());
274
- stack.shift();
275
- stack.shift();
276
384
 
277
- let trace = [];
278
- for (let line of stack) {
279
- line = line.replace("at ", "");
280
- trace.push({
281
- func: line.substr(0, line.indexOf("(") - 1),
282
- pos: line.substring(line.indexOf(".js:") + 4, line.lastIndexOf(":")),
283
- });
284
- }
285
-
286
- return trace;
287
- }
288
-
289
- let stack = getStackTrace();
290
- stack.shift();
291
- let top = stack.shift();
292
- let prefix =
293
- '<span class="debug-function">[' + top.func + ":" + top.pos + "]</span> ";
385
+ let prefix = "";
294
386
  for (let arg of args) {
295
387
  if (arg === undefined) {
296
388
  logMsg(prefix + "undefined");
@@ -355,19 +447,60 @@ function formatMacAddr(macAddr) {
355
447
  .join(":");
356
448
  }
357
449
 
450
+ function toHex(value) {
451
+ return "0x" + value.toString(16).padStart(2, "0");
452
+ }
453
+
454
+ /**
455
+ * Parse flash size string (e.g., "256KB", "4MB") to bytes
456
+ * @param {string} sizeStr - Flash size string with unit (KB or MB)
457
+ * @returns {number} Size in bytes
458
+ */
459
+ function parseFlashSize(sizeStr) {
460
+ if (!sizeStr || typeof sizeStr !== 'string') {
461
+ return 0;
462
+ }
463
+
464
+ // Extract number and unit
465
+ const match = sizeStr.match(/^(\d+)(KB|MB)$/i);
466
+ if (!match) {
467
+ // If no unit, assume it's already in MB (legacy behavior)
468
+ const num = parseInt(sizeStr);
469
+ return isNaN(num) ? 0 : num * 1024 * 1024;
470
+ }
471
+
472
+ const value = parseInt(match[1]);
473
+ const unit = match[2].toUpperCase();
474
+
475
+ if (unit === 'KB') {
476
+ return value * 1024; // KB to bytes
477
+ } else if (unit === 'MB') {
478
+ return value * 1024 * 1024; // MB to bytes
479
+ }
480
+
481
+ return 0;
482
+ }
483
+
358
484
  /**
359
485
  * @name clickConnect
360
486
  * Click handler for the connect/disconnect button.
361
487
  */
362
488
  async function clickConnect() {
489
+ console.log('[clickConnect] Function called');
490
+
363
491
  if (espStub) {
492
+ console.log('[clickConnect] Already connected, disconnecting...');
364
493
  // Remove disconnect event listener to prevent it from firing during manual disconnect
365
494
  if (espStub.handleDisconnect) {
366
495
  espStub.removeEventListener("disconnect", espStub.handleDisconnect);
367
496
  }
368
497
 
369
498
  await espStub.disconnect();
370
- await espStub.port.close();
499
+ try {
500
+ await espStub.port?.close?.();
501
+ } catch (e) {
502
+ // ignore double-close
503
+ }
371
504
  toggleUIConnected(false);
372
505
  espStub = undefined;
373
506
 
@@ -377,13 +510,45 @@ async function clickConnect() {
377
510
  return;
378
511
  }
379
512
 
513
+ console.log('[clickConnect] Getting esploaderMod...');
380
514
  const esploaderMod = await window.esptoolPackage;
381
515
 
382
- let esploader = await esploaderMod.connect({
383
- log: (...args) => logMsg(...args),
384
- debug: (...args) => debugMsg(...args),
385
- error: (...args) => errorMsg(...args),
386
- });
516
+ // Platform detection: Android always uses WebUSB, Desktop uses Web Serial
517
+ const userAgent = navigator.userAgent || '';
518
+ const isAndroid = /Android/i.test(userAgent);
519
+
520
+ // Only log platform details to UI in debug mode (avoid fingerprinting surface)
521
+ if (debugMode.checked) {
522
+ const platformMsg = `Platform: ${isAndroid ? 'Android' : 'Desktop'} (UA: ${userAgent.substring(0, 50)}...)`;
523
+ logMsg(platformMsg);
524
+ }
525
+ logMsg(`Using: ${isAndroid ? 'WebUSB' : 'Web Serial'}`);
526
+
527
+ let esploader;
528
+
529
+ if (isAndroid) {
530
+ // Android: Use WebUSB directly
531
+ console.log('[Connect] Using WebUSB for Android');
532
+ try {
533
+ const port = await WebUSBSerial.requestPort((...args) => logMsg(...args));
534
+ esploader = await esploaderMod.connectWithPort(port, {
535
+ log: (...args) => logMsg(...args),
536
+ debug: (...args) => debugMsg(...args),
537
+ error: (...args) => errorMsg(...args),
538
+ });
539
+ } catch (err) {
540
+ logMsg(`WebUSB connection failed: ${err.message || err}`);
541
+ throw err;
542
+ }
543
+ } else {
544
+ // Desktop: Use Web Serial (standard esptool connect)
545
+ console.log('[Connect] Using Web Serial for Desktop');
546
+ esploader = await esploaderMod.connect({
547
+ log: (...args) => logMsg(...args),
548
+ debug: (...args) => debugMsg(...args),
549
+ error: (...args) => errorMsg(...args),
550
+ });
551
+ }
387
552
 
388
553
  // Store port info for ESP32-S2 detection
389
554
  let portInfo = esploader.port?.getInfo ? esploader.port.getInfo() : {};
@@ -404,119 +569,73 @@ async function clickConnect() {
404
569
  espStub = undefined;
405
570
 
406
571
  try {
572
+ // Close the port first
407
573
  await esploader.port.close();
408
574
 
409
- if (esploader.port.forget) {
575
+ // For Android WebUSB: ESP32-S2 automatic reconnection doesn't work
576
+ // Show message and let user reconnect manually with BOOT button
577
+ if (isAndroid) {
578
+ logMsg("ESP32-S2 has switched to CDC mode");
579
+ logMsg("Please press and HOLD the BOOT button on your ESP32-S2, then click Connect");
580
+ esp32s2ReconnectInProgress = false;
581
+ return;
582
+ }
583
+ // For Desktop Web Serial: Use the modal dialog approach
584
+ if (!isAndroid && esploader.port.forget) {
410
585
  await esploader.port.forget();
411
586
  }
412
587
  } catch (disconnectErr) {
413
588
  // Ignore disconnect errors
589
+ debugMsg("Error during disconnect: " + disconnectErr);
414
590
  }
415
591
 
416
- // Show modal dialog
417
- const modal = document.getElementById("esp32s2Modal");
418
- const reconnectBtn = document.getElementById("butReconnectS2");
419
-
420
- modal.classList.remove("hidden");
421
-
422
- // Handle reconnect button click
423
- const handleReconnect = async () => {
424
- modal.classList.add("hidden");
425
- reconnectBtn.removeEventListener("click", handleReconnect);
592
+ // Show modal dialog ONLY for Desktop
593
+ if (!isAndroid) {
594
+ const modal = document.getElementById("esp32s2Modal");
595
+ const reconnectBtn = document.getElementById("butReconnectS2");
426
596
 
427
- // Trigger port selection
428
- try {
429
- await clickConnect();
430
- // Reset flag on successful connection
431
- esp32s2ReconnectInProgress = false;
432
- } catch (err) {
433
- errorMsg("Failed to reconnect: " + err);
434
- // Reset flag on error so user can try again
435
- esp32s2ReconnectInProgress = false;
436
- }
437
- };
438
-
439
- reconnectBtn.addEventListener("click", handleReconnect);
597
+ modal.classList.remove("hidden");
598
+
599
+ // Handle reconnect button click
600
+ const handleReconnect = async () => {
601
+ modal.classList.add("hidden");
602
+ reconnectBtn.removeEventListener("click", handleReconnect);
603
+
604
+ logMsg("Requesting new device selection...");
605
+
606
+ // Trigger port selection
607
+ try {
608
+ await clickConnect();
609
+ // Reset flag on successful connection
610
+ esp32s2ReconnectInProgress = false;
611
+ } catch (err) {
612
+ errorMsg("Failed to reconnect: " + err);
613
+ // Reset flag on error so user can try again
614
+ esp32s2ReconnectInProgress = false;
615
+ }
616
+ };
617
+
618
+ reconnectBtn.addEventListener("click", handleReconnect);
619
+ }
440
620
  });
441
621
  }
442
622
 
443
623
  try {
444
624
  await esploader.initialize();
445
625
  } catch (err) {
446
- // Check if this is an ESP32-S2 that needs reconnection
447
- if (isESP32S2 && isElectron && !esp32s2ReconnectInProgress) {
448
- esp32s2ReconnectInProgress = true;
449
- logMsg("ESP32-S2 Native USB detected - automatic reconnection...");
450
- toggleUIConnected(false);
451
-
452
- try {
453
- await esploader.port.close();
454
- } catch (e) {
455
- console.debug("Port close error:", e);
456
- }
457
-
458
- // Wait for new port to appear
459
- logMsg("Waiting for ESP32-S2 CDC port...");
460
-
461
- const waitForNewPort = new Promise((resolve) => {
462
- const checkInterval = setInterval(() => {
463
- if (navigator.serial && navigator.serial.getPorts) {
464
- navigator.serial.getPorts().then(ports => {
465
- if (ports.length > 0) {
466
- clearInterval(checkInterval);
467
- resolve(ports[0]);
468
- }
469
- });
470
- }
471
- }, 50);
472
-
473
- // Timeout after 500 ms
474
- setTimeout(() => {
475
- clearInterval(checkInterval);
476
- resolve(null);
477
- }, 500);
478
- });
479
-
480
- const newPort = await waitForNewPort;
481
-
482
- if (!newPort) {
483
- esp32s2ReconnectInProgress = false;
484
- throw new Error("ESP32-S2 CDC port did not appear in time");
485
- }
486
-
487
- // Additional small delay to ensure port is ready
488
- await new Promise(resolve => setTimeout(resolve, 100));
489
-
490
- // Open the new port and create ESPLoader directly
491
- await newPort.open({ baudRate: 115200 });
492
- logMsg("Connected successfully.");
493
-
494
- esploader = new esploaderMod.ESPLoader(newPort, {
495
- log: (...args) => logMsg(...args),
496
- debug: (...args) => debugMsg(...args),
497
- error: (...args) => errorMsg(...args),
498
- });
499
-
500
- // Initialize the new connection
501
- await esploader.initialize();
502
-
503
- esp32s2ReconnectInProgress = false;
504
- logMsg("ESP32-S2 reconnection successful!");
505
- } else {
506
- // If ESP32-S2 reconnect is in progress (browser modal), suppress the error
507
- if (esp32s2ReconnectInProgress) {
508
- logMsg("Initialization interrupted for ESP32-S2 reconnection.");
509
- return;
510
- }
511
-
512
- // Not ESP32-S2 or reconnect already attempted
513
- try {
514
- await esploader.disconnect();
515
- } catch (disconnectErr) {
516
- // Ignore disconnect errors
517
- }
518
- throw err;
626
+ // If ESP32-S2 reconnect is in progress (handled by event listener), suppress the error
627
+ if (esp32s2ReconnectInProgress) {
628
+ logMsg("Initialization interrupted for ESP32-S2 reconnection.");
629
+ return;
630
+ }
631
+
632
+ // Not ESP32-S2 or other error
633
+ try {
634
+ await esploader.disconnect();
635
+ } catch (disconnectErr) {
636
+ // Ignore disconnect errors
519
637
  }
638
+ throw err;
520
639
  }
521
640
 
522
641
  logMsg("Connected to " + esploader.chipName);
@@ -526,9 +645,16 @@ async function clickConnect() {
526
645
  currentChipName = esploader.chipName;
527
646
 
528
647
  espStub = await esploader.runStub();
648
+
529
649
  toggleUIConnected(true);
530
650
  toggleUIToolbar(true);
531
651
 
652
+ // Auto-initialize console if it was enabled before
653
+ if (consoleSwitch.checked) {
654
+ logMsg("Auto-initializing console from saved settings...");
655
+ await clickConsole();
656
+ }
657
+
532
658
  // Check if ESP8266 and show filesystem button
533
659
  const isESP8266 = currentChipName && currentChipName.toUpperCase().includes("ESP8266");
534
660
  if (isESP8266) {
@@ -549,13 +675,13 @@ async function clickConnect() {
549
675
 
550
676
  // Set detected flash size in the read size field
551
677
  if (espStub.flashSize) {
552
- const flashSizeBytes = parseInt(espStub.flashSize) * 1024 * 1024; // Convert MB to bytes
678
+ const flashSizeBytes = parseFlashSize(espStub.flashSize);
553
679
  readSize.value = "0x" + flashSizeBytes.toString(16);
554
680
  }
555
681
 
556
682
  // Set the selected baud rate
557
- let baud = parseInt(baudRate.value);
558
- if (baudRates.includes(baud)) {
683
+ let baud = parseInt(baudRateSelect.value);
684
+ if (baudRates.includes(baud) && esploader.chipName !== "ESP8266") {
559
685
  await espStub.setBaudrate(baud);
560
686
  }
561
687
 
@@ -573,9 +699,14 @@ async function clickConnect() {
573
699
  * Change handler for the Baud Rate selector.
574
700
  */
575
701
  async function changeBaudRate() {
576
- saveSetting("baudrate", baudRate.value);
702
+ saveSetting("baudrate", baudRateSelect.value);
577
703
  if (espStub) {
578
- let baud = parseInt(baudRate.value);
704
+ // Skip for ESP8266 as it only supports 115200 baud in stub mode
705
+ if (espStub.chipName === "ESP8266") {
706
+ logMsg("ESP8266 stub only supports 115200 baud");
707
+ return;
708
+ }
709
+ let baud = parseInt(baudRateSelect.value);
579
710
  if (baudRates.includes(baud)) {
580
711
  await espStub.setBaudrate(baud);
581
712
  }
@@ -617,6 +748,364 @@ async function clickShowLog() {
617
748
  updateLogVisibility();
618
749
  }
619
750
 
751
+ /**
752
+ * @name initConsoleUI
753
+ * Initialize console UI, event handlers, and start console instance
754
+ * Extracted helper to avoid duplication across different console init flows
755
+ */
756
+ async function initConsoleUI() {
757
+ // Wait for port to be ready
758
+ await sleep(200);
759
+
760
+ // Show console container and hide commands
761
+ consoleContainer.classList.remove("hidden");
762
+ const commands = document.getElementById("commands");
763
+ if (commands) commands.classList.add("hidden");
764
+
765
+ // Initialize console
766
+ consoleInstance = new ESP32ToolConsole(espStub.port, consoleContainer, true);
767
+ await consoleInstance.init();
768
+
769
+ // Listen for console reset events
770
+ if (consoleResetHandler) {
771
+ consoleContainer.removeEventListener('console-reset', consoleResetHandler);
772
+ }
773
+ consoleResetHandler = async () => {
774
+ if (espStub && typeof espStub.hardReset === 'function') {
775
+ try {
776
+ debugMsg("Resetting device from console...");
777
+ await espStub.hardReset();
778
+ debugMsg("Device reset successful");
779
+ } catch (err) {
780
+ errorMsg("Failed to reset device: " + err.message);
781
+ }
782
+ }
783
+ };
784
+ consoleContainer.addEventListener('console-reset', consoleResetHandler);
785
+
786
+ // Listen for console close events
787
+ if (consoleCloseHandler) {
788
+ consoleContainer.removeEventListener('console-close', consoleCloseHandler);
789
+ }
790
+ consoleCloseHandler = async () => {
791
+ if (!consoleSwitch.checked) return;
792
+ debugMsg("Closing console...");
793
+ consoleSwitch.checked = false;
794
+ saveSetting("console", false);
795
+ await closeConsole();
796
+ };
797
+ consoleContainer.addEventListener('console-close', consoleCloseHandler);
798
+
799
+ logMsg("Console initialized");
800
+ }
801
+
802
+ /**
803
+ * @name clickConsole
804
+ * Change handler for the Console checkbox.
805
+ */
806
+ async function clickConsole() {
807
+ const shouldEnable = consoleSwitch.checked;
808
+
809
+ if (shouldEnable) {
810
+ // After WDT reset, everything is gone - start fresh with port selection
811
+ if (isConnected && espStub && !espStub.connected) {
812
+ // Port was closed after WDT reset - select new port
813
+ try {
814
+ logMsg("Please select the serial port for console mode...");
815
+ const newPort = await navigator.serial.requestPort();
816
+
817
+ // Open the new port at 115200 for console
818
+ await newPort.open({ baudRate: 115200 });
819
+
820
+ // Update espStub to use the new port
821
+ espStub.port = newPort;
822
+ espStub.connected = true;
823
+ if (espStub._parent) {
824
+ espStub._parent.port = newPort;
825
+ }
826
+ if (espLoaderBeforeConsole) {
827
+ espLoaderBeforeConsole.port = newPort;
828
+ }
829
+
830
+ logMsg("Port opened for console at 115200 baud");
831
+ } catch (openErr) {
832
+ errorMsg(`Failed to open port for console: ${openErr.message}`);
833
+ consoleSwitch.checked = false;
834
+ saveSetting("console", false);
835
+ return;
836
+ }
837
+ }
838
+
839
+ // Initialize console if connected and not already created
840
+ if (isConnected && espStub && espStub.port && !consoleInstance) {
841
+ try {
842
+ // CRITICAL: Save current state BEFORE changing anything
843
+ // If espStub has a parent, we need to get the baudrate from the parent!
844
+ // The stub child can not be used for restoring the stub. the parent must be used!
845
+ const loaderToSave = espStub._parent || espStub;
846
+ const currentBaudrate = loaderToSave.currentBaudRate;
847
+ const currentChipFamily = espStub.chipFamily;
848
+ const currentIsStub = espStub.IS_STUB;
849
+
850
+ // CRITICAL: Save the PARENT loader (not the stub child!)
851
+ espLoaderBeforeConsole = loaderToSave;
852
+ baudRateBeforeConsole = currentBaudrate;
853
+ chipFamilyBeforeConsole = currentChipFamily;
854
+
855
+ // Console ALWAYS runs at 115200 baud (firmware default)
856
+ // Always set baudrate to 115200 before opening console
857
+ try {
858
+ await espStub.setBaudrate(115200);
859
+ debugMsg("Baudrate set to 115200 for console");
860
+ } catch (baudErr) {
861
+ logMsg(`Failed to set baudrate to 115200: ${baudErr.message}`);
862
+ }
863
+
864
+ // Enter console mode - handles both USB-JTAG and serial chip devices
865
+ try {
866
+ const portWasClosed = await espStub.enterConsoleMode();
867
+
868
+ if (portWasClosed) {
869
+ // USB-JTAG/OTG device: Port was closed after WDT reset
870
+ debugMsg("Device reset to firmware mode (port closed)");
871
+
872
+ // Wait a bit for device to boot
873
+ await sleep(500);
874
+
875
+ // Check if this is ESP32-S2 (needs port forget and modal) or ESP32-S3 (direct requestPort)
876
+ const isS2 = chipFamilyBeforeConsole === 0x3252; // CHIP_FAMILY_ESP32S2 = 0x3252
877
+
878
+ if (isS2) {
879
+ // ESP32-S2: Forget old port and show modal for port selection
880
+ if (espStub.port && espStub.port.forget) {
881
+ try {
882
+ await espStub.port.forget();
883
+ logMsg("Forgot old port");
884
+ } catch (forgetErr) {
885
+ logMsg(`Port forget error (ignored): ${forgetErr.message}`);
886
+ }
887
+ }
888
+
889
+ // Wait a bit for browser to process
890
+ await sleep(100);
891
+
892
+ // Show modal for port selection (requires user gesture)
893
+ const modal = document.getElementById("esp32s2Modal");
894
+ const reconnectBtn = document.getElementById("butReconnectS2");
895
+
896
+ // Update modal text for console mode
897
+ const modalTitle = modal.querySelector("h2");
898
+ const modalText = modal.querySelector("p");
899
+ if (modalTitle) modalTitle.textContent = "Device has been reset to firmware mode";
900
+ if (modalText) modalText.textContent = "Please click the button below to select the serial port for console.";
901
+
902
+ modal.classList.remove("hidden");
903
+
904
+ // Handle reconnect button click
905
+ const handleReconnect = async () => {
906
+ modal.classList.add("hidden");
907
+ reconnectBtn.removeEventListener("click", handleReconnect);
908
+
909
+ try {
910
+ // Request the NEW port (user gesture from button click)
911
+ debugMsg("Please select the serial port for console mode...");
912
+ const newPort = await navigator.serial.requestPort();
913
+
914
+ // Open the NEW port at 115200 for console
915
+ await newPort.open({ baudRate: 115200 });
916
+ espStub.port = newPort;
917
+ espStub.connected = true;
918
+
919
+ // Keep parent/loader in sync (used by closeConsole)
920
+ if (espStub._parent) {
921
+ espStub._parent.port = newPort;
922
+ }
923
+ if (espLoaderBeforeConsole) {
924
+ espLoaderBeforeConsole.port = newPort;
925
+ }
926
+
927
+ debugMsg("Port opened for console at 115200 baud");
928
+
929
+ // Device is already in firmware mode, port is open at 115200
930
+ // Initialize console directly
931
+ consoleSwitch.checked = true;
932
+ saveSetting("console", true);
933
+
934
+ // Initialize console UI and handlers
935
+ await initConsoleUI();
936
+ } catch (err) {
937
+ errorMsg(`Failed to open port for console: ${err.message}`);
938
+ consoleSwitch.checked = false;
939
+ saveSetting("console", false);
940
+ }
941
+ };
942
+
943
+ reconnectBtn.addEventListener("click", handleReconnect);
944
+ } else {
945
+ // ESP32-S3/C3/C5/C6/H2/P4: Direct requestPort (no modal, no forget)
946
+ try {
947
+ // Request port selection from user (direct, like console branch)
948
+ debugMsg("Please select the serial port again for console mode...");
949
+ const newPort = await navigator.serial.requestPort();
950
+
951
+ // Open the new port at 115200 for console
952
+ await newPort.open({ baudRate: 115200 });
953
+ espStub.port = newPort;
954
+ espStub.connected = true;
955
+
956
+ // Keep parent/loader in sync (used by closeConsole)
957
+ if (espStub._parent) {
958
+ espStub._parent.port = newPort;
959
+ }
960
+ if (espLoaderBeforeConsole) {
961
+ espLoaderBeforeConsole.port = newPort;
962
+ }
963
+
964
+ debugMsg("Port opened for console at 115200 baud");
965
+
966
+ // Device is already in firmware mode, port is open at 115200
967
+ // Initialize console directly
968
+ consoleSwitch.checked = true;
969
+ saveSetting("console", true);
970
+
971
+ // Initialize console UI and handlers
972
+ await initConsoleUI();
973
+ } catch (err) {
974
+ errorMsg(`Failed to open port for console: ${err.message}`);
975
+ consoleSwitch.checked = false;
976
+ saveSetting("console", false);
977
+ }
978
+ }
979
+
980
+ return;
981
+ } else {
982
+ // Serial chip device: Port stays open
983
+ debugMsg("Device reset to firmware mode");
984
+ }
985
+ } catch (err) {
986
+ errorMsg(`Failed to enter console mode: ${err.message}`);
987
+ consoleSwitch.checked = false;
988
+ saveSetting("console", false);
989
+ return;
990
+ }
991
+
992
+ // Wait for:
993
+ // - Firmware to start after reset
994
+ // - Port to be ready for new reader
995
+ await sleep(200);
996
+
997
+ // Initialize console UI and handlers
998
+ await initConsoleUI();
999
+
1000
+ saveSetting("console", true);
1001
+ } catch (err) {
1002
+ errorMsg("Failed to initialize console: " + err.message);
1003
+ consoleSwitch.checked = false;
1004
+ saveSetting("console", false);
1005
+ await closeConsole();
1006
+ }
1007
+ } else if (!isConnected) {
1008
+ // Not connected - just show message
1009
+ consoleSwitch.checked = false;
1010
+ saveSetting("console", false);
1011
+ errorMsg("Please connect to device first");
1012
+ }
1013
+ } else {
1014
+ await closeConsole();
1015
+ saveSetting("console", false);
1016
+ }
1017
+ }
1018
+
1019
+ /**
1020
+ * @name closeConsole
1021
+ * Close console and restore device to bootloader state
1022
+ */
1023
+ async function closeConsole() {
1024
+ // Hide console and show commands again
1025
+ consoleContainer.classList.add("hidden");
1026
+ const commands = document.getElementById("commands");
1027
+ if (commands) commands.classList.remove("hidden");
1028
+
1029
+ if (consoleInstance) {
1030
+ try {
1031
+ await consoleInstance.disconnect();
1032
+ } catch (err) {
1033
+ debugMsg("Error disconnecting console: " + err);
1034
+ }
1035
+ consoleInstance = null;
1036
+ }
1037
+
1038
+ // Restore original state (bootloader + stub + baudrate)
1039
+ if (espLoaderBeforeConsole && Number.isFinite(baudRateBeforeConsole)) {
1040
+ // Check if this is a USB-JTAG/OTG device
1041
+ const isUsbJtag = espLoaderBeforeConsole._isUsbJtagOrOtg === true;
1042
+
1043
+ try {
1044
+ if (isUsbJtag) {
1045
+ // USB-JTAG/OTG devices: Port was lost, need to request new port
1046
+ debugMsg("Please select the serial port again to reconnect...");
1047
+
1048
+ try {
1049
+ // Request port selection from user
1050
+ const newPort = await navigator.serial.requestPort();
1051
+
1052
+ // Update the loader to use the new port
1053
+ espLoaderBeforeConsole.port = newPort;
1054
+
1055
+ debugMsg("Port selected, reconnecting to bootloader...");
1056
+ } catch (portErr) {
1057
+ errorMsg(`Failed to select port: ${portErr.message}`);
1058
+ // Reset connection state to allow fresh connect
1059
+ espStub = undefined;
1060
+ toggleUIConnected(false);
1061
+ espLoaderBeforeConsole = null;
1062
+ baudRateBeforeConsole = null;
1063
+ chipFamilyBeforeConsole = null;
1064
+ return;
1065
+ }
1066
+ }
1067
+
1068
+ // Use reconnectToBootloader() - it handles everything:
1069
+ // - Releases locks
1070
+ // - Resets to bootloader
1071
+ // - Reopens port at 115200
1072
+ // - Syncs with bootloader using correct reset strategy
1073
+ // NOTE: Call on original loader (before console), not on stub
1074
+ await espLoaderBeforeConsole.reconnectToBootloader();
1075
+
1076
+ // Now espLoaderBeforeConsole is in bootloader state (IS_STUB = false)
1077
+ // Reload stub using the reconnected bootloader
1078
+ const newStub = await espLoaderBeforeConsole.runStub();
1079
+ espStub = newStub;
1080
+
1081
+ // Restore original baudrate
1082
+ if (baudRateBeforeConsole !== 115200) {
1083
+ await espStub.setBaudrate(baudRateBeforeConsole);
1084
+ }
1085
+
1086
+ espLoaderBeforeConsole = null;
1087
+ baudRateBeforeConsole = null;
1088
+ chipFamilyBeforeConsole = null;
1089
+ } catch (err) {
1090
+ errorMsg("Failed to restore state after console: " + err.message);
1091
+ // Attempt to disconnect cleanly to allow reconnection
1092
+ try {
1093
+ if (espLoaderBeforeConsole?.port) {
1094
+ await espLoaderBeforeConsole.port.close();
1095
+ }
1096
+ } catch (closeErr) {
1097
+ debugMsg("Failed to close port: " + closeErr);
1098
+ }
1099
+ // Reset connection state to allow fresh connect
1100
+ espStub = undefined;
1101
+ toggleUIConnected(false);
1102
+ espLoaderBeforeConsole = null;
1103
+ baudRateBeforeConsole = null;
1104
+ chipFamilyBeforeConsole = null;
1105
+ }
1106
+ }
1107
+ }
1108
+
620
1109
  /**
621
1110
  * @name updateLogVisibility
622
1111
  * Update log and log controls visibility
@@ -637,6 +1126,39 @@ function updateLogVisibility() {
637
1126
  }
638
1127
  }
639
1128
 
1129
+ /**
1130
+ * @name clickAdvancedMode
1131
+ * Change handler for the Advanced Mode checkbox.
1132
+ */
1133
+ async function clickAdvancedMode() {
1134
+ saveSetting("advanced", advancedMode.checked);
1135
+ updateAdvancedVisibility();
1136
+ }
1137
+
1138
+ /**
1139
+ * @name changeAdvancedParam
1140
+ * Change handler for advanced parameter dropdowns.
1141
+ */
1142
+ async function changeAdvancedParam() {
1143
+ saveSetting("chunkSize", parseInt(chunkSizeSelect.value));
1144
+ saveSetting("blockSize", parseInt(blockSizeSelect.value));
1145
+ saveSetting("maxInFlight", parseInt(maxInFlightSelect.value));
1146
+ }
1147
+
1148
+ /**
1149
+ * @name updateAdvancedVisibility
1150
+ * Update advanced controls visibility
1151
+ */
1152
+ function updateAdvancedVisibility() {
1153
+ if (advancedMode.checked) {
1154
+ advancedRow.style.display = "flex";
1155
+ main.classList.add("advanced-active");
1156
+ } else {
1157
+ advancedRow.style.display = "none";
1158
+ main.classList.remove("advanced-active");
1159
+ }
1160
+ }
1161
+
640
1162
  /**
641
1163
  * @name clickDetectFS
642
1164
  * Detect ESP8266 filesystem and open manager directly
@@ -651,8 +1173,8 @@ async function clickDetectFS() {
651
1173
  butDetectFS.disabled = true;
652
1174
  logMsg('Detecting ESP8266 filesystem...');
653
1175
 
654
- const flashSizeMB = parseInt(espStub.flashSize);
655
- const flashSizeBytes = flashSizeMB * 1024 * 1024;
1176
+ const flashSizeBytes = parseFlashSize(espStub.flashSize);
1177
+ const flashSizeMB = flashSizeBytes / (1024 * 1024);
656
1178
  const esptoolMod = await window.esptoolPackage;
657
1179
 
658
1180
  // Scan flash for filesystem signatures - optimized based on flash size
@@ -848,7 +1370,7 @@ async function clickDetectFS() {
848
1370
 
849
1371
  } catch (e) {
850
1372
  errorMsg(`Failed to detect/open filesystem: ${e.message || e}`);
851
- console.error(e);
1373
+ debugMsg('Filesystem detection error details: ' + e);
852
1374
  } finally {
853
1375
  // Hide progress bar
854
1376
  readProgress.classList.add('hidden');
@@ -870,7 +1392,7 @@ async function clickErase() {
870
1392
  }
871
1393
 
872
1394
  if (confirmed) {
873
- baudRate.disabled = true;
1395
+ baudRateSelect.disabled = true;
874
1396
  butErase.disabled = true;
875
1397
  butProgram.disabled = true;
876
1398
  try {
@@ -882,7 +1404,7 @@ async function clickErase() {
882
1404
  errorMsg(e);
883
1405
  } finally {
884
1406
  butErase.disabled = false;
885
- baudRate.disabled = false;
1407
+ baudRateSelect.disabled = false;
886
1408
  butProgram.disabled = getValidFiles().length == 0;
887
1409
  }
888
1410
  }
@@ -909,7 +1431,7 @@ async function clickProgram() {
909
1431
  });
910
1432
  };
911
1433
 
912
- baudRate.disabled = true;
1434
+ baudRateSelect.disabled = true;
913
1435
  butErase.disabled = true;
914
1436
  butProgram.disabled = true;
915
1437
  for (let i = 0; i < firmware.length; i++) {
@@ -943,7 +1465,7 @@ async function clickProgram() {
943
1465
  progress[i].querySelector("div").style.width = "0";
944
1466
  }
945
1467
  butErase.disabled = false;
946
- baudRate.disabled = false;
1468
+ baudRateSelect.disabled = false;
947
1469
  butProgram.disabled = getValidFiles().length == 0;
948
1470
  logMsg("To run the new firmware, please reset your device.");
949
1471
  }
@@ -1032,7 +1554,7 @@ async function clickReadFlash() {
1032
1554
 
1033
1555
  const defaultFilename = `flash_0x${offset.toString(16)}_0x${size.toString(16)}.bin`;
1034
1556
 
1035
- baudRate.disabled = true;
1557
+ baudRateSelect.disabled = true;
1036
1558
  butErase.disabled = true;
1037
1559
  butProgram.disabled = true;
1038
1560
  butReadFlash.disabled = true;
@@ -1043,13 +1565,42 @@ async function clickReadFlash() {
1043
1565
  try {
1044
1566
  const progressBar = readProgress.querySelector("div");
1045
1567
 
1568
+ // Prepare options object if advanced mode is enabled
1569
+ // Option validation helpers
1570
+ const validateOption = (name, value) => {
1571
+ if (value === undefined) return undefined;
1572
+ if (!Number.isFinite(value) || value <= 0) {
1573
+ throw new Error(`Invalid ${name}: ${value}`);
1574
+ }
1575
+ return value;
1576
+ };
1577
+
1578
+ let options = undefined;
1579
+ let chunkSizeOpt, blockSizeOpt, maxInFlightOpt;
1580
+ if (advancedMode.checked) {
1581
+ chunkSizeOpt = validateOption("chunkSize", parseInt(chunkSizeSelect.value));
1582
+ blockSizeOpt = validateOption("blockSize", parseInt(blockSizeSelect.value));
1583
+ maxInFlightOpt = validateOption("maxInFlight", parseInt(maxInFlightSelect.value));
1584
+ if ((blockSizeOpt ?? maxInFlightOpt) &&
1585
+ (blockSizeOpt === undefined || maxInFlightOpt === undefined)) {
1586
+ throw new Error("blockSize and maxInFlight must be provided together");
1587
+ }
1588
+ options = {
1589
+ chunkSize: chunkSizeOpt,
1590
+ blockSize: blockSizeOpt,
1591
+ maxInFlight: maxInFlightOpt
1592
+ };
1593
+ logMsg(`Advanced mode: chunkSize=0x${options.chunkSize?.toString(16)}, blockSize=${options.blockSize}, maxInFlight=${options.maxInFlight}`);
1594
+ }
1595
+
1046
1596
  const data = await espStub.readFlash(
1047
1597
  offset,
1048
1598
  size,
1049
1599
  (packet, progress, totalSize) => {
1050
1600
  progressBar.style.width =
1051
1601
  Math.floor((progress / totalSize) * 100) + "%";
1052
- }
1602
+ },
1603
+ options
1053
1604
  );
1054
1605
 
1055
1606
  logMsg(`Successfully read ${data.length} bytes from flash`);
@@ -1062,8 +1613,6 @@ async function clickReadFlash() {
1062
1613
  const esptoolMod = await window.esptoolPackage;
1063
1614
  const fsType = esptoolMod.detectFilesystemFromImage(data, chipName);
1064
1615
 
1065
- logMsg(`Filesystem detection: ${fsType} (chipName: ${chipName})`);
1066
-
1067
1616
  if (fsType !== 'unknown') {
1068
1617
  logMsg(`Detected ${fsType} filesystem in read data`);
1069
1618
 
@@ -1090,7 +1639,7 @@ async function clickReadFlash() {
1090
1639
  readProgress.classList.add("hidden");
1091
1640
  readProgress.querySelector("div").style.width = "0";
1092
1641
  butErase.disabled = false;
1093
- baudRate.disabled = false;
1642
+ baudRateSelect.disabled = false;
1094
1643
  butProgram.disabled = getValidFiles().length == 0;
1095
1644
  butReadFlash.disabled = false;
1096
1645
  readOffset.disabled = false;
@@ -1403,16 +1952,37 @@ function toggleUIConnected(connected) {
1403
1952
 
1404
1953
  if (connected) {
1405
1954
  lbl = "Disconnect";
1406
- // Auto-hide header after connection
1955
+ isConnected = true;
1956
+
1957
+ /* DISABLED: Auto-hide header after connection
1407
1958
  setTimeout(() => {
1408
1959
  header.classList.add("header-hidden");
1409
1960
  main.classList.add("no-header-padding");
1410
1961
  }, 2000); // Hide after 2 seconds
1962
+ */
1411
1963
  } else {
1964
+ isConnected = false;
1412
1965
  toggleUIToolbar(false);
1413
- // Show header when disconnected
1966
+
1967
+ // Cleanup console if it was running
1968
+ if (consoleInstance) {
1969
+ consoleInstance.disconnect().catch(err => {
1970
+ debugMsg("Error disconnecting console: " + err);
1971
+ });
1972
+ consoleInstance = null;
1973
+ }
1974
+
1975
+ // Hide console container, show commands, and uncheck switch
1976
+ consoleContainer.classList.add("hidden");
1977
+ const commands = document.getElementById("commands");
1978
+ if (commands) commands.classList.remove("hidden");
1979
+ consoleSwitch.checked = false;
1980
+ saveSetting("console", false);
1981
+
1982
+ /* DISABLED: Show header when disconnected
1414
1983
  header.classList.remove("header-hidden");
1415
1984
  main.classList.remove("no-header-padding");
1985
+ */
1416
1986
  }
1417
1987
  butConnect.textContent = lbl;
1418
1988
  }
@@ -1420,13 +1990,26 @@ function toggleUIConnected(connected) {
1420
1990
  function loadAllSettings() {
1421
1991
  // Load all saved settings or defaults
1422
1992
  autoscroll.checked = loadSetting("autoscroll", true);
1423
- baudRate.value = loadSetting("baudrate", 2000000);
1993
+ baudRateSelect.value = loadSetting("baudrate", 2000000);
1424
1994
  darkMode.checked = loadSetting("darkmode", false);
1425
- debugMode.checked = loadSetting("debugmode", true);
1995
+ debugMode.checked = loadSetting("debugmode", false);
1426
1996
  showLog.checked = loadSetting("showlog", false);
1997
+ consoleSwitch.checked = loadSetting("console", false);
1998
+ advancedMode.checked = loadSetting("advanced", false);
1999
+
2000
+ // Load advanced parameters
2001
+ chunkSizeSelect.value = loadSetting("chunkSize", 0x4000); // 16 KB default
2002
+ blockSizeSelect.value = loadSetting("blockSize", 4095); // 4095 B default
2003
+ maxInFlightSelect.value = loadSetting("maxInFlight", 8190); // 8190 B default
1427
2004
 
1428
2005
  // Apply show log setting
1429
2006
  updateLogVisibility();
2007
+
2008
+ // Don't show console container here - it will be initialized after connect
2009
+ // if consoleSwitch.checked is true
2010
+
2011
+ // Apply advanced mode visibility
2012
+ updateAdvancedVisibility();
1430
2013
  }
1431
2014
 
1432
2015
  function loadSetting(setting, defaultValue) {
@@ -1586,15 +2169,14 @@ async function detectFilesystemType(offset, size) {
1586
2169
  */
1587
2170
  async function loadLittlefsModule() {
1588
2171
  if (!littlefsModulePromise) {
1589
- // Use absolute path from root for better compatibility with GitHub Pages
1590
- const basePath = window.location.pathname.endsWith('/')
1591
- ? window.location.pathname
1592
- : window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1);
2172
+ // Derive base path from current document URL (works for all hosting layouts)
2173
+ const basePath = new URL(".", window.location.href).pathname;
1593
2174
  const modulePath = `${basePath}src/wasm/littlefs/index.js`;
1594
2175
 
1595
2176
  littlefsModulePromise = import(modulePath)
1596
2177
  .catch(error => {
1597
- console.error('Failed to load LittleFS module from:', modulePath, error);
2178
+ errorMsg('Failed to load LittleFS module from: ' + modulePath);
2179
+ debugMsg('LittleFS module load error: ' + error);
1598
2180
  littlefsModulePromise = null; // Reset on error so it can be retried
1599
2181
  throw error;
1600
2182
  });
@@ -1612,7 +2194,7 @@ function resetLittleFSState() {
1612
2194
  // Don't call destroy() - it can cause crashes
1613
2195
  // Just let garbage collection handle it
1614
2196
  } catch (e) {
1615
- console.error('Error cleaning up LittleFS:', e);
2197
+ debugMsg('Error cleaning up LittleFS: ' + e);
1616
2198
  }
1617
2199
  }
1618
2200
 
@@ -1632,7 +2214,7 @@ function resetLittleFSState() {
1632
2214
  littlefsFileList.innerHTML = '';
1633
2215
  }
1634
2216
  } catch (e) {
1635
- console.error('Error resetting LittleFS UI:', e);
2217
+ debugMsg('Error resetting LittleFS UI: ' + e);
1636
2218
  }
1637
2219
  }
1638
2220
 
@@ -1671,9 +2253,7 @@ async function openLittleFS(partition) {
1671
2253
  logMsg('Mounting LittleFS filesystem...');
1672
2254
 
1673
2255
  // Import constants from esptool module
1674
- const basePath = window.location.pathname.endsWith('/')
1675
- ? window.location.pathname
1676
- : window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1);
2256
+ const basePath = new URL(".", window.location.href).pathname;
1677
2257
  const esptoolModulePath = `${basePath}js/modules/esptool.js`;
1678
2258
  const {
1679
2259
  LITTLEFS_BLOCK_SIZE_CANDIDATES,
@@ -1820,9 +2400,7 @@ async function openFatFS(partition) {
1820
2400
  }
1821
2401
 
1822
2402
  // Load FatFS module
1823
- const basePath = window.location.pathname.endsWith('/')
1824
- ? window.location.pathname
1825
- : window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1);
2403
+ const basePath = new URL(".", window.location.href).pathname;
1826
2404
  const modulePath = `${basePath}src/wasm/fatfs/index.js`;
1827
2405
  const module = await import(modulePath);
1828
2406
  const { createFatFSFromImage, createFatFS } = module;
@@ -1903,7 +2481,7 @@ async function openFatFS(partition) {
1903
2481
  logMsg('FatFS filesystem opened successfully');
1904
2482
  } catch (e) {
1905
2483
  errorMsg(`Failed to open FatFS: ${e.message || e}`);
1906
- console.error('FatFS open error:', e);
2484
+ debugMsg('FatFS open error details: ' + e);
1907
2485
  resetLittleFSState();
1908
2486
  }
1909
2487
  }
@@ -1944,9 +2522,7 @@ async function openSPIFFS(partition) {
1944
2522
  logMsg(`Partition size: ${formatSize(partition.size)} (${partition.size} bytes)`);
1945
2523
 
1946
2524
  // Import SPIFFS module
1947
- const basePath = window.location.pathname.endsWith('/')
1948
- ? window.location.pathname
1949
- : window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1);
2525
+ const basePath = new URL(".", window.location.href).pathname;
1950
2526
  const modulePath = `${basePath}js/modules/esptool.js`;
1951
2527
 
1952
2528
  const {
@@ -2163,10 +2739,9 @@ async function openSPIFFS(partition) {
2163
2739
  refreshLittleFS();
2164
2740
 
2165
2741
  logMsg('SPIFFS filesystem opened successfully');
2166
- logMsg('Note: SPIFFS is a flat filesystem - directories are not supported.');
2167
2742
  } catch (e) {
2168
2743
  errorMsg(`Failed to open SPIFFS: ${e.message || e}`);
2169
- console.error('SPIFFS open error:', e);
2744
+ debugMsg('SPIFFS open error details: ' + e);
2170
2745
  resetLittleFSState();
2171
2746
  }
2172
2747
  }
@@ -2505,7 +3080,7 @@ function clickLittlefsClose() {
2505
3080
  currentLittleFS.destroy();
2506
3081
  }
2507
3082
  } catch (e) {
2508
- console.error(`Error destroying ${fsName}:`, e);
3083
+ debugMsg(`Error destroying ${fsName}: ` + e);
2509
3084
  }
2510
3085
  currentLittleFS = null;
2511
3086
  }