esp32tool 1.1.8 → 1.2.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 (49) 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 +225 -35
  8. package/dist/cli.d.ts +17 -0
  9. package/dist/cli.js +458 -0
  10. package/dist/esp_loader.d.ts +129 -21
  11. package/dist/esp_loader.js +1227 -222
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.js +37 -4
  14. package/dist/node-usb-adapter.d.ts +47 -0
  15. package/dist/node-usb-adapter.js +725 -0
  16. package/dist/stubs/index.d.ts +1 -2
  17. package/dist/stubs/index.js +4 -0
  18. package/dist/web/index.js +1 -1
  19. package/electron/cli-main.cjs +74 -0
  20. package/electron/main.cjs +338 -0
  21. package/electron/main.js +7 -2
  22. package/favicon.ico +0 -0
  23. package/fix-cli-imports.cjs +127 -0
  24. package/generate-icons.sh +89 -0
  25. package/icons/icon-128.png +0 -0
  26. package/icons/icon-144.png +0 -0
  27. package/icons/icon-152.png +0 -0
  28. package/icons/icon-192.png +0 -0
  29. package/icons/icon-384.png +0 -0
  30. package/icons/icon-512.png +0 -0
  31. package/icons/icon-72.png +0 -0
  32. package/icons/icon-96.png +0 -0
  33. package/index.html +94 -64
  34. package/install-android.html +411 -0
  35. package/js/modules/esptool.js +1 -1
  36. package/js/script.js +165 -160
  37. package/js/webusb-serial.js +1017 -0
  38. package/license.md +1 -1
  39. package/manifest.json +89 -0
  40. package/package.cli.json +29 -0
  41. package/package.json +31 -21
  42. package/screenshots/desktop.png +0 -0
  43. package/screenshots/mobile.png +0 -0
  44. package/src/cli.ts +618 -0
  45. package/src/esp_loader.ts +1438 -254
  46. package/src/index.ts +69 -3
  47. package/src/node-usb-adapter.ts +924 -0
  48. package/src/stubs/index.ts +4 -1
  49. package/sw.js +155 -0
package/js/script.js CHANGED
@@ -1,3 +1,12 @@
1
+ // Import WebUSB serial support for Android compatibility
2
+ import { WebUSBSerial, requestSerialPort } from './webusb-serial.js';
3
+
4
+ // Make requestSerialPort available globally for esptool.js
5
+ // Use defensive assignment to avoid accidental overwrites
6
+ if (!globalThis.requestSerialPort) {
7
+ globalThis.requestSerialPort = requestSerialPort;
8
+ }
9
+
1
10
  let espStub;
2
11
  let esp32s2ReconnectInProgress = false;
3
12
  let currentLittleFS = null;
@@ -8,6 +17,7 @@ let currentFilesystemType = null; // 'littlefs', 'fatfs', or 'spiffs'
8
17
  let littlefsModulePromise = null; // Cache for LittleFS WASM module
9
18
  let lastReadFlashData = null; // Store last read flash data for ESP8266
10
19
  let currentChipName = null; // Store chip name globally
20
+ let isConnected = false; // Track connection state
11
21
 
12
22
  /**
13
23
  * Get display name for current filesystem type
@@ -96,7 +106,7 @@ const isElectron = window.electronAPI && window.electronAPI.isElectron;
96
106
  const maxLogLength = 100;
97
107
  const log = document.getElementById("log");
98
108
  const butConnect = document.getElementById("butConnect");
99
- const baudRate = document.getElementById("baudRate");
109
+ const baudRateSelect = document.getElementById("baudRate");
100
110
  const butClear = document.getElementById("butClear");
101
111
  const butErase = document.getElementById("butErase");
102
112
  const butProgram = document.getElementById("butProgram");
@@ -190,7 +200,7 @@ document.addEventListener("DOMContentLoaded", () => {
190
200
  updateUploadRowsVisibility();
191
201
 
192
202
  autoscroll.addEventListener("click", clickAutoscroll);
193
- baudRate.addEventListener("change", changeBaudRate);
203
+ baudRateSelect.addEventListener("change", changeBaudRate);
194
204
  darkMode.addEventListener("click", clickDarkMode);
195
205
  debugMode.addEventListener("click", clickDebugMode);
196
206
  showLog.addEventListener("click", clickShowLog);
@@ -228,7 +238,8 @@ document.addEventListener("DOMContentLoaded", () => {
228
238
  }
229
239
  });
230
240
 
231
- if ("serial" in navigator) {
241
+ // Check for Web Serial or WebUSB support
242
+ if ("serial" in navigator || "usb" in navigator) {
232
243
  const notSupported = document.getElementById("notSupported");
233
244
  notSupported.classList.add("hidden");
234
245
  }
@@ -244,7 +255,7 @@ function initBaudRate() {
244
255
  var option = document.createElement("option");
245
256
  option.text = rate + " Baud";
246
257
  option.value = rate;
247
- baudRate.add(option);
258
+ baudRateSelect.add(option);
248
259
  }
249
260
  }
250
261
 
@@ -266,31 +277,8 @@ function debugMsg(...args) {
266
277
  if (!debugMode.checked) {
267
278
  return;
268
279
  }
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
-
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
280
 
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> ";
281
+ let prefix = "";
294
282
  for (let arg of args) {
295
283
  if (arg === undefined) {
296
284
  logMsg(prefix + "undefined");
@@ -355,19 +343,60 @@ function formatMacAddr(macAddr) {
355
343
  .join(":");
356
344
  }
357
345
 
346
+ function toHex(value) {
347
+ return "0x" + value.toString(16).padStart(2, "0");
348
+ }
349
+
350
+ /**
351
+ * Parse flash size string (e.g., "256KB", "4MB") to bytes
352
+ * @param {string} sizeStr - Flash size string with unit (KB or MB)
353
+ * @returns {number} Size in bytes
354
+ */
355
+ function parseFlashSize(sizeStr) {
356
+ if (!sizeStr || typeof sizeStr !== 'string') {
357
+ return 0;
358
+ }
359
+
360
+ // Extract number and unit
361
+ const match = sizeStr.match(/^(\d+)(KB|MB)$/i);
362
+ if (!match) {
363
+ // If no unit, assume it's already in MB (legacy behavior)
364
+ const num = parseInt(sizeStr);
365
+ return isNaN(num) ? 0 : num * 1024 * 1024;
366
+ }
367
+
368
+ const value = parseInt(match[1]);
369
+ const unit = match[2].toUpperCase();
370
+
371
+ if (unit === 'KB') {
372
+ return value * 1024; // KB to bytes
373
+ } else if (unit === 'MB') {
374
+ return value * 1024 * 1024; // MB to bytes
375
+ }
376
+
377
+ return 0;
378
+ }
379
+
358
380
  /**
359
381
  * @name clickConnect
360
382
  * Click handler for the connect/disconnect button.
361
383
  */
362
384
  async function clickConnect() {
385
+ console.log('[clickConnect] Function called');
386
+
363
387
  if (espStub) {
388
+ console.log('[clickConnect] Already connected, disconnecting...');
364
389
  // Remove disconnect event listener to prevent it from firing during manual disconnect
365
390
  if (espStub.handleDisconnect) {
366
391
  espStub.removeEventListener("disconnect", espStub.handleDisconnect);
367
392
  }
368
393
 
369
394
  await espStub.disconnect();
370
- await espStub.port.close();
395
+ try {
396
+ await espStub.port?.close?.();
397
+ } catch (e) {
398
+ // ignore double-close
399
+ }
371
400
  toggleUIConnected(false);
372
401
  espStub = undefined;
373
402
 
@@ -377,13 +406,45 @@ async function clickConnect() {
377
406
  return;
378
407
  }
379
408
 
409
+ console.log('[clickConnect] Getting esploaderMod...');
380
410
  const esploaderMod = await window.esptoolPackage;
381
411
 
382
- let esploader = await esploaderMod.connect({
383
- log: (...args) => logMsg(...args),
384
- debug: (...args) => debugMsg(...args),
385
- error: (...args) => errorMsg(...args),
386
- });
412
+ // Platform detection: Android always uses WebUSB, Desktop uses Web Serial
413
+ const userAgent = navigator.userAgent || '';
414
+ const isAndroid = /Android/i.test(userAgent);
415
+
416
+ // Only log platform details to UI in debug mode (avoid fingerprinting surface)
417
+ if (debugMode.checked) {
418
+ const platformMsg = `Platform: ${isAndroid ? 'Android' : 'Desktop'} (UA: ${userAgent.substring(0, 50)}...)`;
419
+ logMsg(platformMsg);
420
+ }
421
+ logMsg(`Using: ${isAndroid ? 'WebUSB' : 'Web Serial'}`);
422
+
423
+ let esploader;
424
+
425
+ if (isAndroid) {
426
+ // Android: Use WebUSB directly
427
+ console.log('[Connect] Using WebUSB for Android');
428
+ try {
429
+ const port = await WebUSBSerial.requestPort((...args) => logMsg(...args));
430
+ esploader = await esploaderMod.connectWithPort(port, {
431
+ log: (...args) => logMsg(...args),
432
+ debug: (...args) => debugMsg(...args),
433
+ error: (...args) => errorMsg(...args),
434
+ });
435
+ } catch (err) {
436
+ logMsg(`WebUSB connection failed: ${err.message || err}`);
437
+ throw err;
438
+ }
439
+ } else {
440
+ // Desktop: Use Web Serial (standard esptool connect)
441
+ console.log('[Connect] Using Web Serial for Desktop');
442
+ esploader = await esploaderMod.connect({
443
+ log: (...args) => logMsg(...args),
444
+ debug: (...args) => debugMsg(...args),
445
+ error: (...args) => errorMsg(...args),
446
+ });
447
+ }
387
448
 
388
449
  // Store port info for ESP32-S2 detection
389
450
  let portInfo = esploader.port?.getInfo ? esploader.port.getInfo() : {};
@@ -404,119 +465,73 @@ async function clickConnect() {
404
465
  espStub = undefined;
405
466
 
406
467
  try {
468
+ // Close the port first
407
469
  await esploader.port.close();
408
470
 
409
- if (esploader.port.forget) {
471
+ // For Android WebUSB: ESP32-S2 automatic reconnection doesn't work
472
+ // Show message and let user reconnect manually with BOOT button
473
+ if (isAndroid) {
474
+ logMsg("ESP32-S2 has switched to CDC mode");
475
+ logMsg("Please press and HOLD the BOOT button on your ESP32-S2, then click Connect");
476
+ esp32s2ReconnectInProgress = false;
477
+ return;
478
+ }
479
+ // For Desktop Web Serial: Use the modal dialog approach
480
+ if (!isAndroid && esploader.port.forget) {
410
481
  await esploader.port.forget();
411
482
  }
412
483
  } catch (disconnectErr) {
413
484
  // Ignore disconnect errors
485
+ console.warn("Error during disconnect:", disconnectErr);
414
486
  }
415
487
 
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);
488
+ // Show modal dialog ONLY for Desktop
489
+ if (!isAndroid) {
490
+ const modal = document.getElementById("esp32s2Modal");
491
+ const reconnectBtn = document.getElementById("butReconnectS2");
426
492
 
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);
493
+ modal.classList.remove("hidden");
494
+
495
+ // Handle reconnect button click
496
+ const handleReconnect = async () => {
497
+ modal.classList.add("hidden");
498
+ reconnectBtn.removeEventListener("click", handleReconnect);
499
+
500
+ logMsg("Requesting new device selection...");
501
+
502
+ // Trigger port selection
503
+ try {
504
+ await clickConnect();
505
+ // Reset flag on successful connection
506
+ esp32s2ReconnectInProgress = false;
507
+ } catch (err) {
508
+ errorMsg("Failed to reconnect: " + err);
509
+ // Reset flag on error so user can try again
510
+ esp32s2ReconnectInProgress = false;
511
+ }
512
+ };
513
+
514
+ reconnectBtn.addEventListener("click", handleReconnect);
515
+ }
440
516
  });
441
517
  }
442
518
 
443
519
  try {
444
520
  await esploader.initialize();
445
521
  } 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;
522
+ // If ESP32-S2 reconnect is in progress (handled by event listener), suppress the error
523
+ if (esp32s2ReconnectInProgress) {
524
+ logMsg("Initialization interrupted for ESP32-S2 reconnection.");
525
+ return;
526
+ }
527
+
528
+ // Not ESP32-S2 or other error
529
+ try {
530
+ await esploader.disconnect();
531
+ } catch (disconnectErr) {
532
+ // Ignore disconnect errors
519
533
  }
534
+ throw err;
520
535
  }
521
536
 
522
537
  logMsg("Connected to " + esploader.chipName);
@@ -526,6 +541,7 @@ async function clickConnect() {
526
541
  currentChipName = esploader.chipName;
527
542
 
528
543
  espStub = await esploader.runStub();
544
+
529
545
  toggleUIConnected(true);
530
546
  toggleUIToolbar(true);
531
547
 
@@ -549,12 +565,12 @@ async function clickConnect() {
549
565
 
550
566
  // Set detected flash size in the read size field
551
567
  if (espStub.flashSize) {
552
- const flashSizeBytes = parseInt(espStub.flashSize) * 1024 * 1024; // Convert MB to bytes
568
+ const flashSizeBytes = parseFlashSize(espStub.flashSize);
553
569
  readSize.value = "0x" + flashSizeBytes.toString(16);
554
570
  }
555
571
 
556
572
  // Set the selected baud rate
557
- let baud = parseInt(baudRate.value);
573
+ let baud = parseInt(baudRateSelect.value);
558
574
  if (baudRates.includes(baud)) {
559
575
  await espStub.setBaudrate(baud);
560
576
  }
@@ -573,9 +589,9 @@ async function clickConnect() {
573
589
  * Change handler for the Baud Rate selector.
574
590
  */
575
591
  async function changeBaudRate() {
576
- saveSetting("baudrate", baudRate.value);
592
+ saveSetting("baudrate", baudRateSelect.value);
577
593
  if (espStub) {
578
- let baud = parseInt(baudRate.value);
594
+ let baud = parseInt(baudRateSelect.value);
579
595
  if (baudRates.includes(baud)) {
580
596
  await espStub.setBaudrate(baud);
581
597
  }
@@ -651,8 +667,8 @@ async function clickDetectFS() {
651
667
  butDetectFS.disabled = true;
652
668
  logMsg('Detecting ESP8266 filesystem...');
653
669
 
654
- const flashSizeMB = parseInt(espStub.flashSize);
655
- const flashSizeBytes = flashSizeMB * 1024 * 1024;
670
+ const flashSizeBytes = parseFlashSize(espStub.flashSize);
671
+ const flashSizeMB = flashSizeBytes / (1024 * 1024);
656
672
  const esptoolMod = await window.esptoolPackage;
657
673
 
658
674
  // Scan flash for filesystem signatures - optimized based on flash size
@@ -870,7 +886,7 @@ async function clickErase() {
870
886
  }
871
887
 
872
888
  if (confirmed) {
873
- baudRate.disabled = true;
889
+ baudRateSelect.disabled = true;
874
890
  butErase.disabled = true;
875
891
  butProgram.disabled = true;
876
892
  try {
@@ -882,7 +898,7 @@ async function clickErase() {
882
898
  errorMsg(e);
883
899
  } finally {
884
900
  butErase.disabled = false;
885
- baudRate.disabled = false;
901
+ baudRateSelect.disabled = false;
886
902
  butProgram.disabled = getValidFiles().length == 0;
887
903
  }
888
904
  }
@@ -909,7 +925,7 @@ async function clickProgram() {
909
925
  });
910
926
  };
911
927
 
912
- baudRate.disabled = true;
928
+ baudRateSelect.disabled = true;
913
929
  butErase.disabled = true;
914
930
  butProgram.disabled = true;
915
931
  for (let i = 0; i < firmware.length; i++) {
@@ -943,7 +959,7 @@ async function clickProgram() {
943
959
  progress[i].querySelector("div").style.width = "0";
944
960
  }
945
961
  butErase.disabled = false;
946
- baudRate.disabled = false;
962
+ baudRateSelect.disabled = false;
947
963
  butProgram.disabled = getValidFiles().length == 0;
948
964
  logMsg("To run the new firmware, please reset your device.");
949
965
  }
@@ -1032,7 +1048,7 @@ async function clickReadFlash() {
1032
1048
 
1033
1049
  const defaultFilename = `flash_0x${offset.toString(16)}_0x${size.toString(16)}.bin`;
1034
1050
 
1035
- baudRate.disabled = true;
1051
+ baudRateSelect.disabled = true;
1036
1052
  butErase.disabled = true;
1037
1053
  butProgram.disabled = true;
1038
1054
  butReadFlash.disabled = true;
@@ -1062,8 +1078,6 @@ async function clickReadFlash() {
1062
1078
  const esptoolMod = await window.esptoolPackage;
1063
1079
  const fsType = esptoolMod.detectFilesystemFromImage(data, chipName);
1064
1080
 
1065
- logMsg(`Filesystem detection: ${fsType} (chipName: ${chipName})`);
1066
-
1067
1081
  if (fsType !== 'unknown') {
1068
1082
  logMsg(`Detected ${fsType} filesystem in read data`);
1069
1083
 
@@ -1090,7 +1104,7 @@ async function clickReadFlash() {
1090
1104
  readProgress.classList.add("hidden");
1091
1105
  readProgress.querySelector("div").style.width = "0";
1092
1106
  butErase.disabled = false;
1093
- baudRate.disabled = false;
1107
+ baudRateSelect.disabled = false;
1094
1108
  butProgram.disabled = getValidFiles().length == 0;
1095
1109
  butReadFlash.disabled = false;
1096
1110
  readOffset.disabled = false;
@@ -1420,7 +1434,7 @@ function toggleUIConnected(connected) {
1420
1434
  function loadAllSettings() {
1421
1435
  // Load all saved settings or defaults
1422
1436
  autoscroll.checked = loadSetting("autoscroll", true);
1423
- baudRate.value = loadSetting("baudrate", 2000000);
1437
+ baudRateSelect.value = loadSetting("baudrate", 2000000);
1424
1438
  darkMode.checked = loadSetting("darkmode", false);
1425
1439
  debugMode.checked = loadSetting("debugmode", true);
1426
1440
  showLog.checked = loadSetting("showlog", false);
@@ -1586,10 +1600,8 @@ async function detectFilesystemType(offset, size) {
1586
1600
  */
1587
1601
  async function loadLittlefsModule() {
1588
1602
  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);
1603
+ // Derive base path from current document URL (works for all hosting layouts)
1604
+ const basePath = new URL(".", window.location.href).pathname;
1593
1605
  const modulePath = `${basePath}src/wasm/littlefs/index.js`;
1594
1606
 
1595
1607
  littlefsModulePromise = import(modulePath)
@@ -1671,9 +1683,7 @@ async function openLittleFS(partition) {
1671
1683
  logMsg('Mounting LittleFS filesystem...');
1672
1684
 
1673
1685
  // 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);
1686
+ const basePath = new URL(".", window.location.href).pathname;
1677
1687
  const esptoolModulePath = `${basePath}js/modules/esptool.js`;
1678
1688
  const {
1679
1689
  LITTLEFS_BLOCK_SIZE_CANDIDATES,
@@ -1820,9 +1830,7 @@ async function openFatFS(partition) {
1820
1830
  }
1821
1831
 
1822
1832
  // 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);
1833
+ const basePath = new URL(".", window.location.href).pathname;
1826
1834
  const modulePath = `${basePath}src/wasm/fatfs/index.js`;
1827
1835
  const module = await import(modulePath);
1828
1836
  const { createFatFSFromImage, createFatFS } = module;
@@ -1944,9 +1952,7 @@ async function openSPIFFS(partition) {
1944
1952
  logMsg(`Partition size: ${formatSize(partition.size)} (${partition.size} bytes)`);
1945
1953
 
1946
1954
  // 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);
1955
+ const basePath = new URL(".", window.location.href).pathname;
1950
1956
  const modulePath = `${basePath}js/modules/esptool.js`;
1951
1957
 
1952
1958
  const {
@@ -2163,7 +2169,6 @@ async function openSPIFFS(partition) {
2163
2169
  refreshLittleFS();
2164
2170
 
2165
2171
  logMsg('SPIFFS filesystem opened successfully');
2166
- logMsg('Note: SPIFFS is a flat filesystem - directories are not supported.');
2167
2172
  } catch (e) {
2168
2173
  errorMsg(`Failed to open SPIFFS: ${e.message || e}`);
2169
2174
  console.error('SPIFFS open error:', e);