esp32tool 1.6.6 → 1.6.7

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
@@ -1,8 +1,8 @@
1
1
  // Import WebUSB serial support for Android compatibility
2
- import { WebUSBSerial, requestSerialPort } from './webusb-serial.js';
3
- import { ESP32ToolConsole } from './console.js';
4
- import { HexEditor } from './hex-editor.js';
5
- import { NVSEditor } from './nvs-editor.js';
2
+ import { WebUSBSerial, requestSerialPort } from "./webusb-serial.js";
3
+ import { ESP32ToolConsole } from "./console.js";
4
+ import { HexEditor } from "./hex-editor.js";
5
+ import { NVSEditor } from "./nvs-editor.js";
6
6
 
7
7
  // Make requestSerialPort available globally for esptool.js
8
8
  // Use defensive assignment to avoid accidental overwrites
@@ -25,7 +25,7 @@ let espStub;
25
25
  let esp32s2ReconnectInProgress = false;
26
26
  let currentLittleFS = null;
27
27
  let currentLittleFSPartition = null;
28
- let currentLittleFSPath = '/';
28
+ let currentLittleFSPath = "/";
29
29
  let currentLittleFSBlockSize = 4096;
30
30
  let currentFilesystemType = null; // 'littlefs', 'fatfs', or 'spiffs'
31
31
  let littlefsModulePromise = null; // Cache for LittleFS WASM module
@@ -47,12 +47,16 @@ let consoleBootloaderHandlerModule = null;
47
47
  * Get display name for current filesystem type
48
48
  */
49
49
  function getFilesystemDisplayName() {
50
- if (!currentFilesystemType) return 'Filesystem';
50
+ if (!currentFilesystemType) return "Filesystem";
51
51
  switch (currentFilesystemType) {
52
- case 'littlefs': return 'LittleFS';
53
- case 'fatfs': return 'FatFS';
54
- case 'spiffs': return 'SPIFFS';
55
- default: return 'Filesystem';
52
+ case "littlefs":
53
+ return "LittleFS";
54
+ case "fatfs":
55
+ return "FatFS";
56
+ case "spiffs":
57
+ return "SPIFFS";
58
+ default:
59
+ return "Filesystem";
56
60
  }
57
61
  }
58
62
 
@@ -64,71 +68,75 @@ function clearAllCachedData() {
64
68
  if (currentLittleFS) {
65
69
  try {
66
70
  // Only call destroy if it exists (LittleFS has it, FatFS/SPIFFS don't)
67
- if (typeof currentLittleFS.destroy === 'function') {
71
+ if (typeof currentLittleFS.destroy === "function") {
68
72
  currentLittleFS.destroy();
69
73
  }
70
74
  } catch (e) {
71
- debugMsg('Error destroying filesystem: ' + e);
75
+ debugMsg("Error destroying filesystem: " + e);
72
76
  }
73
77
  }
74
-
78
+
75
79
  // Reset filesystem state
76
80
  currentLittleFS = null;
77
81
  currentLittleFSPartition = null;
78
- currentLittleFSPath = '/';
82
+ currentLittleFSPath = "/";
79
83
  currentLittleFSBlockSize = 4096;
80
84
  currentFilesystemType = null;
81
85
  lastReadFlashData = null;
82
86
  currentChipName = null;
83
87
  currentMacAddr = null;
84
-
88
+
85
89
  // Hide filesystem manager
86
- littlefsManager.classList.add('hidden');
87
-
90
+ littlefsManager.classList.add("hidden");
91
+
88
92
  // Clear partition list
89
- partitionList.innerHTML = '';
90
- partitionList.classList.add('hidden');
91
-
93
+ partitionList.innerHTML = "";
94
+ partitionList.classList.add("hidden");
95
+
92
96
  // Show the Read Partition Table button again
93
- butReadPartitions.classList.remove('hidden');
94
-
97
+ butReadPartitions.classList.remove("hidden");
98
+
95
99
  // Close NVS editor if open
96
100
  if (nvsEditorInstance) {
97
- try { nvsEditorInstance.close(); } catch (_) {}
101
+ try {
102
+ nvsEditorInstance.close();
103
+ } catch (_) {}
98
104
  nvsEditorInstance = null;
99
105
  }
100
- nvseditorContainer.classList.add('hidden');
101
- document.body.classList.remove('nvseditor-active');
102
-
106
+ nvseditorContainer.classList.add("hidden");
107
+ document.body.classList.remove("nvseditor-active");
108
+
103
109
  // Hide Detect FS button
104
- butDetectFS.classList.add('hidden');
105
-
110
+ butDetectFS.classList.add("hidden");
111
+
106
112
  // Hide Open FS Manager button (if it exists)
107
113
  if (butOpenFSManager) {
108
- butOpenFSManager.classList.add('hidden');
114
+ butOpenFSManager.classList.add("hidden");
109
115
  }
110
-
116
+
111
117
  // Hide ESP8266 info (if it exists)
112
- const esp8266Info = document.getElementById('esp8266Info');
118
+ const esp8266Info = document.getElementById("esp8266Info");
113
119
  if (esp8266Info) {
114
- esp8266Info.classList.add('hidden');
120
+ esp8266Info.classList.add("hidden");
115
121
  }
116
-
122
+
117
123
  // Clear file input
118
124
  if (littlefsFileInput) {
119
- littlefsFileInput.value = '';
125
+ littlefsFileInput.value = "";
120
126
  }
121
-
127
+
122
128
  // Reset buttons
123
129
  butLittlefsUpload.disabled = true;
124
-
130
+
125
131
  // Clear any cached module promises
126
132
  littlefsModulePromise = null;
127
-
128
- logMsg('All cached data cleared');
133
+
134
+ logMsg("All cached data cleared");
129
135
  }
130
136
 
131
- const baudRates = [2000000, 1500000, 921600, 500000, 460800, 230400, 153600, 128000, 115200];
137
+ const baudRates = [
138
+ 2000000, 1500000, 921600, 500000, 460800, 230400, 153600, 128000, 115200,
139
+ ];
132
140
 
133
141
  // Advanced read flash parameters
134
142
  // chunkSize: Amount of data to request from ESP in one command (in KB)
@@ -138,7 +146,7 @@ const chunkSizes = [
138
146
  { label: "16 KB (WebUSB)", value: 0x4000 },
139
147
  { label: "64 KB", value: 0x10000 },
140
148
  { label: "128 KB (Desktop)", value: 0x20000 },
141
- { label: "256 KB", value: 0x40000 }
149
+ { label: "256 KB", value: 0x40000 },
142
150
  ];
143
151
 
144
152
  // blockSize: Size of each data block sent by ESP (in bytes)
@@ -154,7 +162,7 @@ const blockSizes = [
154
162
  { label: "1024 B", value: 1024 },
155
163
  { label: "1984 B", value: 1984 },
156
164
  { label: "2024 B", value: 2024 },
157
- { label: "3968 B (Desktop)", value: 3968 }
165
+ { label: "3968 B (Desktop)", value: 3968 },
158
166
  ];
159
167
 
160
168
  // maxInFlight: Maximum unacknowledged bytes (in bytes)
@@ -176,7 +184,7 @@ const maxInFlights = [
176
184
  { label: "31744 B", value: 31744 },
177
185
  { label: "63488 B", value: 63488 },
178
186
  { label: "126976 B", value: 126976 },
179
- { label: "253952 B", value: 253952 }
187
+ { label: "253952 B", value: 253952 },
180
188
  ];
181
189
 
182
190
  // Check if running in Electron
@@ -248,7 +256,7 @@ const nvseditorContainer = document.getElementById("nvseditor-container");
248
256
 
249
257
  // NVS and partition table layout constants
250
258
  const PARTITION_TABLE_OFFSET = 0x8000;
251
- const PARTITION_TABLE_SIZE = 0x1000;
259
+ const PARTITION_TABLE_SIZE = 0x1000;
252
260
 
253
261
  let currentViewedFile = null;
254
262
  let currentViewedFileData = null;
@@ -256,17 +264,18 @@ let currentViewedFileData = null;
256
264
  // Mobile detection
257
265
  function isMobileDevice() {
258
266
  const userAgent = navigator.userAgent || navigator.vendor || window.opera;
259
-
267
+
260
268
  // Check for mobile user agents
261
- const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
269
+ const mobileRegex =
270
+ /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
262
271
  const isMobileUA = mobileRegex.test(userAgent);
263
-
272
+
264
273
  // Check for touch support
265
- const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
266
-
274
+ const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
275
+
267
276
  // Check screen size
268
277
  const isSmallScreen = window.innerWidth <= 768;
269
-
278
+
270
279
  return isMobileUA || (hasTouch && isSmallScreen);
271
280
  }
272
281
 
@@ -277,20 +286,20 @@ function isMobileDevice() {
277
286
  */
278
287
  function isUsingWebUSB() {
279
288
  // If we have an active connection, check the port's isWebUSB property
280
- if (espStub && espStub.port && typeof espStub.port.isWebUSB !== 'undefined') {
289
+ if (espStub && espStub.port && typeof espStub.port.isWebUSB !== "undefined") {
281
290
  return espStub.port.isWebUSB === true;
282
291
  }
283
-
292
+
284
293
  // Fallback: Check if we're on a mobile device (likely using WebUSB)
285
294
  if (isMobileDevice()) {
286
295
  return true;
287
296
  }
288
-
297
+
289
298
  // Check if Web Serial is NOT available but USB is (WebUSB only)
290
299
  if (!("serial" in navigator) && "usb" in navigator) {
291
300
  return true;
292
301
  }
293
-
302
+
294
303
  // Default to Web Serial (desktop)
295
304
  return false;
296
305
  }
@@ -302,26 +311,26 @@ function isUsingWebUSB() {
302
311
  */
303
312
  function getDefaultAdvancedParams() {
304
313
  const isWebUSB = isUsingWebUSB();
305
-
314
+
306
315
  return {
307
- chunkSize: isWebUSB ? 0x4000 : 0x20000, // 16 KB for WebUSB, 128 KB for Desktop
308
- blockSize: isWebUSB ? 248 : 3968, // 248 B for WebUSB, 3968 B for Desktop
309
- maxInFlight: isWebUSB ? 248 : 15872 // 248 B for WebUSB, 15872 B for Desktop
316
+ chunkSize: isWebUSB ? 0x4000 : 0x20000, // 16 KB for WebUSB, 128 KB for Desktop
317
+ blockSize: isWebUSB ? 248 : 3968, // 248 B for WebUSB, 3968 B for Desktop
318
+ maxInFlight: isWebUSB ? 248 : 15872, // 248 B for WebUSB, 15872 B for Desktop
310
319
  };
311
320
  }
312
321
 
313
322
  // Update mobile classes and padding
314
323
  function updateMobileClasses() {
315
324
  const isMobile = isMobileDevice();
316
-
325
+
317
326
  if (isMobile) {
318
- document.body.classList.add('mobile-device');
319
- document.body.classList.add('no-hover');
327
+ document.body.classList.add("mobile-device");
328
+ document.body.classList.add("no-hover");
320
329
  } else {
321
- document.body.classList.remove('mobile-device');
322
- document.body.classList.remove('no-hover');
330
+ document.body.classList.remove("mobile-device");
331
+ document.body.classList.remove("no-hover");
323
332
  }
324
-
333
+
325
334
  // Update main padding to match header height
326
335
  updateMainPadding();
327
336
  }
@@ -346,13 +355,13 @@ const debouncedUpdateMobileClasses = debounce(updateMobileClasses, 250);
346
355
  updateMobileClasses();
347
356
 
348
357
  // Update on resize and orientation change
349
- window.addEventListener('resize', debouncedUpdateMobileClasses);
350
- window.addEventListener('orientationchange', debouncedUpdateMobileClasses);
358
+ window.addEventListener("resize", debouncedUpdateMobileClasses);
359
+ window.addEventListener("orientationchange", debouncedUpdateMobileClasses);
351
360
 
352
361
  document.addEventListener("DOMContentLoaded", () => {
353
362
  butConnect.addEventListener("click", () => {
354
363
  clickConnect().catch(async (e) => {
355
- debugMsg('Connection error: ' + e);
364
+ debugMsg("Connection error: " + e);
356
365
  errorMsg(e.message || e);
357
366
  if (espStub) {
358
367
  await espStub.disconnect();
@@ -378,8 +387,8 @@ document.addEventListener("DOMContentLoaded", () => {
378
387
  butLittlefsMkdir.addEventListener("click", clickLittlefsMkdir);
379
388
  butCloseFileViewer.addEventListener("click", closeFileViewer);
380
389
  butDownloadFromViewer.addEventListener("click", downloadFromViewer);
381
- tabText.addEventListener("click", () => switchViewerTab('text'));
382
- tabHex.addEventListener("click", () => switchViewerTab('hex'));
390
+ tabText.addEventListener("click", () => switchViewerTab("text"));
391
+ tabHex.addEventListener("click", () => switchViewerTab("hex"));
383
392
  littlefsFileInput.addEventListener("change", () => {
384
393
  butLittlefsUpload.disabled = !littlefsFileInput.files.length;
385
394
  });
@@ -389,10 +398,10 @@ document.addEventListener("DOMContentLoaded", () => {
389
398
  for (let i = 0; i < offsets.length; i++) {
390
399
  offsets[i].addEventListener("change", checkProgrammable);
391
400
  }
392
-
401
+
393
402
  // Initialize upload rows visibility - only show first row
394
403
  updateUploadRowsVisibility();
395
-
404
+
396
405
  bindCheckboxSetting(autoscroll, "autoscroll");
397
406
  consoleSwitch.addEventListener("click", clickConsole);
398
407
  baudRateSelect.addEventListener("change", changeBaudRate);
@@ -401,7 +410,9 @@ document.addEventListener("DOMContentLoaded", () => {
401
410
  blockSizeSelect.addEventListener("change", changeAdvancedParam);
402
411
  maxInFlightSelect.addEventListener("change", changeAdvancedParam);
403
412
  bindCheckboxSetting(darkMode, "darkmode", () => updateTheme());
404
- bindCheckboxSetting(debugMode, "debugmode", (v) => logMsg("Debug mode " + (v ? "enabled" : "disabled")));
413
+ bindCheckboxSetting(debugMode, "debugmode", (v) =>
414
+ logMsg("Debug mode " + (v ? "enabled" : "disabled")),
415
+ );
405
416
  bindCheckboxSetting(showLog, "showlog", () => updateLogVisibility());
406
417
  window.addEventListener("error", function (event) {
407
418
  console.log("Got an uncaught error: ", event.error);
@@ -418,7 +429,7 @@ document.addEventListener("DOMContentLoaded", () => {
418
429
  loadAllSettings();
419
430
  updateTheme();
420
431
  logMsg("ESP32Tool loaded.");
421
-
432
+
422
433
  // Set initial main padding based on header height
423
434
  updateMainPadding();
424
435
  });
@@ -435,7 +446,7 @@ function initBaudRate() {
435
446
  function initAdvancedParams() {
436
447
  // Get default values based on environment (Desktop vs WebUSB)
437
448
  const defaults = getDefaultAdvancedParams();
438
-
449
+
439
450
  // Initialize chunkSize dropdown
440
451
  for (let item of chunkSizes) {
441
452
  const option = document.createElement("option");
@@ -474,37 +485,45 @@ function initAdvancedParams() {
474
485
  function updateAdvancedParamsForConnection() {
475
486
  // Get the correct defaults based on actual connection
476
487
  const defaults = getDefaultAdvancedParams();
477
-
488
+
478
489
  // Get current values
479
490
  const currentChunkSize = parseInt(chunkSizeSelect.value);
480
491
  const currentBlockSize = parseInt(blockSizeSelect.value);
481
492
  const currentMaxInFlight = parseInt(maxInFlightSelect.value);
482
-
493
+
483
494
  // Check if values are at old defaults (need updating)
484
- const oldWebUSBDefaults = { chunkSize: 0x4000, blockSize: 248, maxInFlight: 248 };
485
- const oldDesktopDefaults = { chunkSize: 0x40000, blockSize: 3968, maxInFlight: 15872 };
486
-
487
- const isAtWebUSBDefaults =
495
+ const oldWebUSBDefaults = {
496
+ chunkSize: 0x4000,
497
+ blockSize: 248,
498
+ maxInFlight: 248,
499
+ };
500
+ const oldDesktopDefaults = {
501
+ chunkSize: 0x40000,
502
+ blockSize: 3968,
503
+ maxInFlight: 15872,
504
+ };
505
+
506
+ const isAtWebUSBDefaults =
488
507
  currentChunkSize === oldWebUSBDefaults.chunkSize &&
489
508
  currentBlockSize === oldWebUSBDefaults.blockSize &&
490
509
  currentMaxInFlight === oldWebUSBDefaults.maxInFlight;
491
-
492
- const isAtDesktopDefaults =
510
+
511
+ const isAtDesktopDefaults =
493
512
  currentChunkSize === oldDesktopDefaults.chunkSize &&
494
513
  currentBlockSize === oldDesktopDefaults.blockSize &&
495
514
  currentMaxInFlight === oldDesktopDefaults.maxInFlight;
496
-
515
+
497
516
  // Only update if at defaults (user hasn't customized)
498
517
  if (isAtWebUSBDefaults || isAtDesktopDefaults) {
499
518
  chunkSizeSelect.value = defaults.chunkSize;
500
519
  blockSizeSelect.value = defaults.blockSize;
501
520
  maxInFlightSelect.value = defaults.maxInFlight;
502
-
521
+
503
522
  // Save the new values
504
523
  saveSetting("chunkSize", defaults.chunkSize);
505
524
  saveSetting("blockSize", defaults.blockSize);
506
525
  saveSetting("maxInFlight", defaults.maxInFlight);
507
-
526
+
508
527
  const connectionType = isUsingWebUSB() ? "WebUSB" : "Web Serial";
509
528
  debugMsg(`Advanced parameters updated for ${connectionType} connection`);
510
529
  }
@@ -606,7 +625,10 @@ function buildAdvancedOptions() {
606
625
 
607
626
  const chunkSize = validate("chunkSize", parseInt(chunkSizeSelect.value));
608
627
  const blockSize = validate("blockSize", parseInt(blockSizeSelect.value));
609
- const maxInFlight = validate("maxInFlight", parseInt(maxInFlightSelect.value));
628
+ const maxInFlight = validate(
629
+ "maxInFlight",
630
+ parseInt(maxInFlightSelect.value),
631
+ );
610
632
 
611
633
  // blockSize and maxInFlight must both be finite or both non-finite
612
634
  const hasBlockSize = Number.isFinite(blockSize);
@@ -624,10 +646,10 @@ function buildAdvancedOptions() {
624
646
  * @returns {number} Size in bytes
625
647
  */
626
648
  function parseFlashSize(sizeStr) {
627
- if (!sizeStr || typeof sizeStr !== 'string') {
649
+ if (!sizeStr || typeof sizeStr !== "string") {
628
650
  return 0;
629
651
  }
630
-
652
+
631
653
  // Extract number and unit
632
654
  const match = sizeStr.match(/^(\d+)(KB|MB)$/i);
633
655
  if (!match) {
@@ -635,16 +657,16 @@ function parseFlashSize(sizeStr) {
635
657
  const num = parseInt(sizeStr);
636
658
  return isNaN(num) ? 0 : num * 1024 * 1024;
637
659
  }
638
-
660
+
639
661
  const value = parseInt(match[1]);
640
662
  const unit = match[2].toUpperCase();
641
-
642
- if (unit === 'KB') {
663
+
664
+ if (unit === "KB") {
643
665
  return value * 1024; // KB to bytes
644
- } else if (unit === 'MB') {
666
+ } else if (unit === "MB") {
645
667
  return value * 1024 * 1024; // MB to bytes
646
668
  }
647
-
669
+
648
670
  return 0;
649
671
  }
650
672
 
@@ -653,15 +675,15 @@ function parseFlashSize(sizeStr) {
653
675
  * Click handler for the connect/disconnect button.
654
676
  */
655
677
  async function clickConnect() {
656
- console.log('[clickConnect] Function called');
657
-
678
+ console.log("[clickConnect] Function called");
679
+
658
680
  if (espStub) {
659
- console.log('[clickConnect] Already connected, disconnecting...');
681
+ console.log("[clickConnect] Already connected, disconnecting...");
660
682
  // Remove disconnect event listener to prevent it from firing during manual disconnect
661
683
  if (espStub.handleDisconnect) {
662
684
  espStub.removeEventListener("disconnect", espStub.handleDisconnect);
663
685
  }
664
-
686
+
665
687
  await espStub.disconnect();
666
688
  try {
667
689
  await espStub.port?.close?.();
@@ -670,32 +692,32 @@ async function clickConnect() {
670
692
  }
671
693
  toggleUIConnected(false);
672
694
  espStub = undefined;
673
-
695
+
674
696
  // Clear all cached data and state
675
697
  clearAllCachedData();
676
-
698
+
677
699
  return;
678
700
  }
679
701
 
680
- console.log('[clickConnect] Getting esploaderMod...');
702
+ console.log("[clickConnect] Getting esploaderMod...");
681
703
  const esploaderMod = await window.esptoolPackage;
682
704
 
683
705
  // Platform detection: Android always uses WebUSB, Desktop uses Web Serial
684
- const userAgent = navigator.userAgent || '';
706
+ const userAgent = navigator.userAgent || "";
685
707
  const isAndroid = /Android/i.test(userAgent);
686
-
708
+
687
709
  // Only log platform details to UI in debug mode (avoid fingerprinting surface)
688
710
  if (debugMode.checked) {
689
- const platformMsg = `Platform: ${isAndroid ? 'Android' : 'Desktop'} (UA: ${userAgent.substring(0, 50)}...)`;
711
+ const platformMsg = `Platform: ${isAndroid ? "Android" : "Desktop"} (UA: ${userAgent.substring(0, 50)}...)`;
690
712
  logMsg(platformMsg);
691
713
  }
692
- logMsg(`Using: ${isAndroid ? 'WebUSB' : 'Web Serial'}`);
693
-
714
+ logMsg(`Using: ${isAndroid ? "WebUSB" : "Web Serial"}`);
715
+
694
716
  let esploader;
695
-
717
+
696
718
  if (isAndroid) {
697
719
  // Android: Use WebUSB directly
698
- console.log('[Connect] Using WebUSB for Android');
720
+ console.log("[Connect] Using WebUSB for Android");
699
721
  try {
700
722
  const port = await WebUSBSerial.requestPort((...args) => logMsg(...args));
701
723
  esploader = await esploaderMod.connectWithPort(port, {
@@ -709,14 +731,14 @@ async function clickConnect() {
709
731
  }
710
732
  } else {
711
733
  // Desktop: Use Web Serial (standard esptool connect)
712
- console.log('[Connect] Using Web Serial for Desktop');
734
+ console.log("[Connect] Using Web Serial for Desktop");
713
735
  esploader = await esploaderMod.connect({
714
736
  log: (...args) => logMsg(...args),
715
737
  debug: (...args) => debugMsg(...args),
716
738
  error: (...args) => errorMsg(...args),
717
739
  });
718
740
  }
719
-
741
+
720
742
  // Handle ESP32-S2 Native USB reconnection requirement for BROWSER
721
743
  // Only add listener if not already in reconnect mode
722
744
  if (!esp32s2ReconnectInProgress) {
@@ -725,13 +747,13 @@ async function clickConnect() {
725
747
  if (esp32s2ReconnectInProgress) {
726
748
  return;
727
749
  }
728
-
750
+
729
751
  esp32s2ReconnectInProgress = true;
730
752
  logMsg("ESP32-S2 Native USB detected!");
731
753
  toggleUIConnected(false);
732
754
  const previousStubPort = espStub?.port;
733
755
  espStub = undefined;
734
-
756
+
735
757
  try {
736
758
  // Close the port first
737
759
  await esploader.port.close();
@@ -744,20 +766,20 @@ async function clickConnect() {
744
766
  // Ignore port close errors
745
767
  debugMsg(`Port close error (ignored): ${closeErr.message}`);
746
768
  }
747
-
769
+
748
770
  // Show modal dialog
749
771
  const modal = document.getElementById("esp32s2Modal");
750
772
  const reconnectBtn = document.getElementById("butReconnectS2");
751
-
773
+
752
774
  modal.classList.remove("hidden");
753
-
775
+
754
776
  // Handle reconnect button click
755
777
  const handleReconnect = async () => {
756
778
  modal.classList.add("hidden");
757
779
  reconnectBtn.removeEventListener("click", handleReconnect);
758
-
780
+
759
781
  logMsg("Requesting new device selection...");
760
-
782
+
761
783
  // Trigger port selection
762
784
  try {
763
785
  await clickConnect();
@@ -772,7 +794,7 @@ async function clickConnect() {
772
794
  reconnectBtn.addEventListener("click", handleReconnect);
773
795
  });
774
796
  }
775
-
797
+
776
798
  try {
777
799
  await esploader.initialize();
778
800
  } catch (err) {
@@ -781,7 +803,7 @@ async function clickConnect() {
781
803
  logMsg("Initialization interrupted for ESP32-S2 reconnection.");
782
804
  return;
783
805
  }
784
-
806
+
785
807
  // Not ESP32-S2 or other error
786
808
  try {
787
809
  await esploader.disconnect();
@@ -799,52 +821,53 @@ async function clickConnect() {
799
821
  currentMacAddr = formatMacAddr(esploader.macAddr());
800
822
 
801
823
  espStub = await esploader.runStub();
802
-
824
+
803
825
  // Update advanced parameters based on actual connection type (WebUSB vs Web Serial)
804
826
  // Only update if user hasn't manually changed them (still at defaults)
805
827
  updateAdvancedParamsForConnection();
806
-
828
+
807
829
  toggleUIConnected(true);
808
830
  toggleUIToolbar(true);
809
-
831
+
810
832
  // Auto-initialize console if it was enabled before
811
833
  if (consoleSwitch.checked) {
812
834
  logMsg("Auto-initializing console from saved settings...");
813
835
  await clickConsole();
814
836
  }
815
-
837
+
816
838
  // Check if ESP8266 and show filesystem button
817
- const isESP8266 = currentChipName && currentChipName.toUpperCase().includes("ESP8266");
839
+ const isESP8266 =
840
+ currentChipName && currentChipName.toUpperCase().includes("ESP8266");
818
841
  if (isESP8266) {
819
842
  // Hide partition table button for ESP8266
820
- butReadPartitions.classList.add('hidden');
821
- butNVSEditor.classList.add('hidden');
822
-
843
+ butReadPartitions.classList.add("hidden");
844
+ butNVSEditor.classList.add("hidden");
845
+
823
846
  // Show ESP8266 filesystem detection button
824
- butDetectFS.classList.remove('hidden');
847
+ butDetectFS.classList.remove("hidden");
825
848
  } else {
826
849
  // Show partition table button for ESP32
827
- butReadPartitions.classList.remove('hidden');
828
- butNVSEditor.classList.remove('hidden');
829
-
850
+ butReadPartitions.classList.remove("hidden");
851
+ butNVSEditor.classList.remove("hidden");
852
+
830
853
  // Hide ESP8266 filesystem detection button
831
854
  if (butDetectFS) {
832
- butDetectFS.classList.add('hidden');
855
+ butDetectFS.classList.add("hidden");
833
856
  }
834
857
  }
835
-
858
+
836
859
  // Set detected flash size in the read size field
837
860
  if (espStub.flashSize) {
838
861
  const flashSizeBytes = parseFlashSize(espStub.flashSize);
839
862
  readSize.value = "0x" + flashSizeBytes.toString(16);
840
863
  }
841
-
864
+
842
865
  // Set the selected baud rate
843
866
  let baud = parseInt(baudRateSelect.value);
844
867
  if (baudRates.includes(baud) && esploader.chipName !== "ESP8266") {
845
868
  await espStub.setBaudrate(baud);
846
869
  }
847
-
870
+
848
871
  // Store disconnect handler so we can remove it later
849
872
  const handleDisconnect = () => {
850
873
  toggleUIConnected(false);
@@ -899,10 +922,14 @@ function showS2Modal(title, text) {
899
922
  if (modalTitle) modalTitle.textContent = title;
900
923
  if (modalText) modalText.textContent = text;
901
924
  modal.classList.remove("hidden");
902
- reconnectBtn.addEventListener("click", () => {
903
- modal.classList.add("hidden");
904
- resolve();
905
- }, { once: true });
925
+ reconnectBtn.addEventListener(
926
+ "click",
927
+ () => {
928
+ modal.classList.add("hidden");
929
+ resolve();
930
+ },
931
+ { once: true },
932
+ );
906
933
  });
907
934
  }
908
935
 
@@ -932,9 +959,9 @@ function assignPort(newPort) {
932
959
  async function openConsolePortAndInit(newPort) {
933
960
  await newPort.open({ baudRate: 115200 });
934
961
  assignPort(newPort);
935
-
962
+
936
963
  debugMsg("Port opened for console at 115200 baud");
937
-
964
+
938
965
  // WebUSB needs extra time for USB interface and streams to stabilize
939
966
  if (isUsingWebUSB()) {
940
967
  let retries = 30;
@@ -947,10 +974,10 @@ async function openConsolePortAndInit(newPort) {
947
974
  }
948
975
  debugMsg("WebUSB port streams ready for console");
949
976
  }
950
-
977
+
951
978
  consoleSwitch.checked = true;
952
979
  saveSetting("console", true);
953
-
980
+
954
981
  await initConsoleUI();
955
982
  }
956
983
 
@@ -961,56 +988,71 @@ async function openConsolePortAndInit(newPort) {
961
988
  async function initConsoleUI() {
962
989
  // Wait for port to be ready
963
990
  await sleep(200);
964
-
991
+
965
992
  // Show console container and hide commands
966
993
  consoleContainer.classList.remove("hidden");
967
-
994
+
968
995
  // Add console-active class to body for mobile styling
969
996
  document.body.classList.add("console-active");
970
997
  const commands = document.getElementById("commands");
971
998
  if (commands) commands.classList.add("hidden");
972
-
999
+
973
1000
  // Initialize console
974
1001
  consoleInstance = new ESP32ToolConsole(espStub.port, consoleContainer, true);
975
1002
  await consoleInstance.init();
976
-
1003
+
977
1004
  // Listen for console reset events
978
1005
  if (consoleResetHandler) {
979
- consoleContainer.removeEventListener('console-reset', consoleResetHandler);
1006
+ consoleContainer.removeEventListener("console-reset", consoleResetHandler);
980
1007
  }
981
1008
  consoleResetHandler = async () => {
982
- if (espLoaderBeforeConsole && typeof espLoaderBeforeConsole.resetInConsoleMode === 'function') {
1009
+ if (
1010
+ espLoaderBeforeConsole &&
1011
+ typeof espLoaderBeforeConsole.resetInConsoleMode === "function"
1012
+ ) {
983
1013
  try {
984
1014
  const isS2 = chipFamilyBeforeConsole === CHIP_FAMILY_ESP32S2;
985
-
1015
+
986
1016
  if (isS2) {
987
- debugMsg("ESP32-S2 console reset: entering bootloader, then WDT reset to firmware...");
988
-
1017
+ debugMsg(
1018
+ "ESP32-S2 console reset: entering bootloader, then WDT reset to firmware...",
1019
+ );
1020
+
989
1021
  if (consoleInstance) {
990
1022
  await consoleInstance.disconnect();
991
1023
  }
992
-
1024
+
993
1025
  await espLoaderBeforeConsole.resetInConsoleMode();
994
-
1026
+
995
1027
  const waitTime = isUsingWebUSB() ? 1000 : 500;
996
1028
  await sleep(waitTime);
997
-
1029
+
998
1030
  // Step 1: Select bootloader port for WDT reset
999
1031
  const portLabel = isUsingWebUSB() ? "USB device" : "serial port";
1000
- await showS2Modal("Select bootloader port", `Select the ${portLabel} (bootloader) to reset the device.`);
1032
+ await showS2Modal(
1033
+ "Select bootloader port",
1034
+ `Select the ${portLabel} (bootloader) to reset the device.`,
1035
+ );
1001
1036
  const bootloaderPort = await requestPort();
1002
-
1037
+
1003
1038
  debugMsg("Syncing with bootloader and performing WDT reset...");
1004
1039
  await espLoaderBeforeConsole.syncAndWdtReset(bootloaderPort);
1005
- try { await bootloaderPort.close(); } catch (e) { /* port may already be gone */ }
1006
-
1040
+ try {
1041
+ await bootloaderPort.close();
1042
+ } catch (e) {
1043
+ /* port may already be gone */
1044
+ }
1045
+
1007
1046
  debugMsg("Waiting for device to boot into firmware...");
1008
1047
  await sleep(waitTime);
1009
-
1048
+
1010
1049
  // Step 2: Select firmware port for console
1011
- await showS2Modal("Select console port", `Select the ${portLabel} (firmware) for console.`);
1050
+ await showS2Modal(
1051
+ "Select console port",
1052
+ `Select the ${portLabel} (firmware) for console.`,
1053
+ );
1012
1054
  const consolePort = await requestPort();
1013
-
1055
+
1014
1056
  await consolePort.open({ baudRate: 115200 });
1015
1057
  assignPort(consolePort);
1016
1058
  await consoleInstance.reconnect(consolePort);
@@ -1025,11 +1067,11 @@ async function initConsoleUI() {
1025
1067
  }
1026
1068
  }
1027
1069
  };
1028
- consoleContainer.addEventListener('console-reset', consoleResetHandler);
1029
-
1070
+ consoleContainer.addEventListener("console-reset", consoleResetHandler);
1071
+
1030
1072
  // Listen for console close events
1031
1073
  if (consoleCloseHandler) {
1032
- consoleContainer.removeEventListener('console-close', consoleCloseHandler);
1074
+ consoleContainer.removeEventListener("console-close", consoleCloseHandler);
1033
1075
  }
1034
1076
  consoleCloseHandler = async () => {
1035
1077
  if (!consoleSwitch.checked) return;
@@ -1038,22 +1080,28 @@ async function initConsoleUI() {
1038
1080
  saveSetting("console", false);
1039
1081
  await closeConsole();
1040
1082
  };
1041
- consoleContainer.addEventListener('console-close', consoleCloseHandler);
1042
-
1083
+ consoleContainer.addEventListener("console-close", consoleCloseHandler);
1084
+
1043
1085
  // Listen for console bootloader detection events
1044
1086
  // The console detects bootloader patterns in real-time as data arrives
1045
1087
  // and dispatches this event when bootloader is detected
1046
1088
  if (consoleBootloaderHandlerModule) {
1047
- consoleContainer.removeEventListener('console-bootloader', consoleBootloaderHandlerModule);
1089
+ consoleContainer.removeEventListener(
1090
+ "console-bootloader",
1091
+ consoleBootloaderHandlerModule,
1092
+ );
1048
1093
  }
1049
1094
  consoleBootloaderHandlerModule = async () => {
1050
1095
  logMsg("Console detected bootloader mode - resetting to firmware...");
1051
- if (espLoaderBeforeConsole && typeof espLoaderBeforeConsole.resetInConsoleMode === 'function') {
1096
+ if (
1097
+ espLoaderBeforeConsole &&
1098
+ typeof espLoaderBeforeConsole.resetInConsoleMode === "function"
1099
+ ) {
1052
1100
  try {
1053
1101
  await espLoaderBeforeConsole.resetInConsoleMode();
1054
1102
  logMsg("✅ Device reset to firmware mode");
1055
1103
  // Clear console to see new output after reset
1056
- if (consoleInstance && typeof consoleInstance.clear === 'function') {
1104
+ if (consoleInstance && typeof consoleInstance.clear === "function") {
1057
1105
  consoleInstance.clear();
1058
1106
  }
1059
1107
  } catch (err) {
@@ -1061,8 +1109,11 @@ async function initConsoleUI() {
1061
1109
  }
1062
1110
  }
1063
1111
  };
1064
- consoleContainer.addEventListener('console-bootloader', consoleBootloaderHandlerModule);
1065
-
1112
+ consoleContainer.addEventListener(
1113
+ "console-bootloader",
1114
+ consoleBootloaderHandlerModule,
1115
+ );
1116
+
1066
1117
  logMsg("Console initialized");
1067
1118
  }
1068
1119
 
@@ -1072,7 +1123,7 @@ async function initConsoleUI() {
1072
1123
  */
1073
1124
  async function clickConsole() {
1074
1125
  const shouldEnable = consoleSwitch.checked;
1075
-
1126
+
1076
1127
  if (shouldEnable) {
1077
1128
  // After WDT reset, everything is gone - start fresh with port selection
1078
1129
  // Initialize console if connected and not already created
@@ -1098,30 +1149,32 @@ async function clickConsole() {
1098
1149
  } catch (baudErr) {
1099
1150
  logMsg(`Failed to set baudrate to 115200: ${baudErr.message}`);
1100
1151
  }
1101
-
1152
+
1102
1153
  // Enter console mode - handles both USB-JTAG and serial chip devices
1103
1154
  try {
1104
1155
  const portWasClosed = await espStub.enterConsoleMode();
1105
-
1156
+
1106
1157
  if (portWasClosed) {
1107
1158
  // USB-JTAG/OTG device: Port was closed after WDT reset
1108
1159
  debugMsg("Device reset to firmware mode (port closed)");
1109
-
1160
+
1110
1161
  // Wait for device to boot and USB port to become available
1111
1162
  // Android/WebUSB needs more time than Desktop for USB enumeration
1112
1163
  const isWebUSB = isUsingWebUSB();
1113
1164
  const waitTime = isWebUSB ? 1000 : 500; // 1s for Android, 500ms for Desktop
1114
- debugMsg(`Waiting ${waitTime}ms for device to boot and USB to enumerate...`);
1165
+ debugMsg(
1166
+ `Waiting ${waitTime}ms for device to boot and USB to enumerate...`,
1167
+ );
1115
1168
  await sleep(waitTime);
1116
-
1169
+
1117
1170
  // Check if this is ESP32-S2 or if we're on Android (WebUSB)
1118
1171
  // Both need modal for user gesture
1119
1172
  const isS2 = chipFamilyBeforeConsole === CHIP_FAMILY_ESP32S2;
1120
1173
  const needsModal = isS2 || isWebUSB;
1121
-
1174
+
1122
1175
  if (needsModal) {
1123
1176
  // ESP32-S2 (all platforms) or Android (all chips): Use modal for user gesture
1124
-
1177
+
1125
1178
  // After WDT reset, the USB device re-enumerates and creates a NEW port
1126
1179
  // The old port is dead and will be garbage collected by the browser
1127
1180
  // We just need to clear our references to it
@@ -1129,7 +1182,7 @@ async function clickConsole() {
1129
1182
  espStub.connected = false;
1130
1183
  espStub._writer = undefined;
1131
1184
  espStub._reader = undefined;
1132
-
1185
+
1133
1186
  if (espStub._parent) {
1134
1187
  espStub._parent.port = null;
1135
1188
  espStub._parent.connected = false;
@@ -1138,16 +1191,18 @@ async function clickConsole() {
1138
1191
  espLoaderBeforeConsole.port = null;
1139
1192
  espLoaderBeforeConsole.connected = false;
1140
1193
  }
1141
-
1142
- debugMsg("Old port references cleared (USB device will re-enumerate)");
1143
-
1194
+
1195
+ debugMsg(
1196
+ "Old port references cleared (USB device will re-enumerate)",
1197
+ );
1198
+
1144
1199
  // Wait for browser to process port closure and USB re-enumeration
1145
1200
  await sleep(300);
1146
-
1201
+
1147
1202
  const portLabel = isWebUSB ? "USB device" : "serial port";
1148
1203
  await showS2Modal(
1149
1204
  "Device has been reset to firmware mode",
1150
- `Please click the button below to select the ${portLabel} for console.`
1205
+ `Please click the button below to select the ${portLabel} for console.`,
1151
1206
  );
1152
1207
  }
1153
1208
 
@@ -1159,7 +1214,7 @@ async function clickConsole() {
1159
1214
  consoleSwitch.checked = false;
1160
1215
  saveSetting("console", false);
1161
1216
  }
1162
-
1217
+
1163
1218
  return;
1164
1219
  } else {
1165
1220
  // Serial chip device: Port stays open
@@ -1171,15 +1226,15 @@ async function clickConsole() {
1171
1226
  saveSetting("console", false);
1172
1227
  return;
1173
1228
  }
1174
-
1229
+
1175
1230
  // Wait for:
1176
1231
  // - Firmware to start after reset
1177
1232
  // - Port to be ready for new reader
1178
1233
  await sleep(500);
1179
-
1234
+
1180
1235
  // Initialize console UI and handlers
1181
1236
  await initConsoleUI();
1182
-
1237
+
1183
1238
  saveSetting("console", true);
1184
1239
  } catch (err) {
1185
1240
  errorMsg("Failed to initialize console: " + err.message);
@@ -1206,7 +1261,7 @@ async function clickConsole() {
1206
1261
  async function closeConsole() {
1207
1262
  // Remove console-active class from body FIRST to restore visibility
1208
1263
  document.body.classList.remove("console-active");
1209
-
1264
+
1210
1265
  // Hide console and show commands again
1211
1266
  consoleContainer.classList.add("hidden");
1212
1267
  const commands = document.getElementById("commands");
@@ -1215,7 +1270,7 @@ async function closeConsole() {
1215
1270
  // Force display to ensure it's visible
1216
1271
  commands.style.display = "";
1217
1272
  }
1218
-
1273
+
1219
1274
  // Restore original state (bootloader + stub + baudrate)
1220
1275
  if (espLoaderBeforeConsole && Number.isFinite(baudRateBeforeConsole)) {
1221
1276
  // Disconnect console first to release locks
@@ -1227,34 +1282,44 @@ async function closeConsole() {
1227
1282
  }
1228
1283
  consoleInstance = null;
1229
1284
  }
1230
-
1285
+
1231
1286
  // Remove console event handlers
1232
1287
  if (consoleResetHandler) {
1233
- consoleContainer.removeEventListener('console-reset', consoleResetHandler);
1288
+ consoleContainer.removeEventListener(
1289
+ "console-reset",
1290
+ consoleResetHandler,
1291
+ );
1234
1292
  consoleResetHandler = null;
1235
1293
  }
1236
1294
  if (consoleCloseHandler) {
1237
- consoleContainer.removeEventListener('console-close', consoleCloseHandler);
1295
+ consoleContainer.removeEventListener(
1296
+ "console-close",
1297
+ consoleCloseHandler,
1298
+ );
1238
1299
  consoleCloseHandler = null;
1239
1300
  }
1240
1301
  if (consoleBootloaderHandlerModule) {
1241
- consoleContainer.removeEventListener('console-bootloader', consoleBootloaderHandlerModule);
1302
+ consoleContainer.removeEventListener(
1303
+ "console-bootloader",
1304
+ consoleBootloaderHandlerModule,
1305
+ );
1242
1306
  consoleBootloaderHandlerModule = null;
1243
1307
  }
1244
-
1308
+
1245
1309
  // Use esp_loader's exitConsoleMode function
1246
1310
  try {
1247
- const needsManualReconnect = await espLoaderBeforeConsole.exitConsoleMode();
1248
-
1311
+ const needsManualReconnect =
1312
+ await espLoaderBeforeConsole.exitConsoleMode();
1313
+
1249
1314
  if (needsManualReconnect) {
1250
1315
  // Port has changed, need to select new port
1251
1316
  // logMsg("Port changed - please select the new port");
1252
1317
  toggleUIConnected(false);
1253
1318
  espStub = undefined;
1254
-
1319
+
1255
1320
  // Wait a moment for port to stabilize
1256
1321
  await sleep(1000);
1257
-
1322
+
1258
1323
  // Trigger port selection
1259
1324
  try {
1260
1325
  await clickConnect();
@@ -1296,7 +1361,7 @@ async function closeConsole() {
1296
1361
  */
1297
1362
  function updateLogVisibility() {
1298
1363
  const logControls = document.querySelector(".log-controls");
1299
-
1364
+
1300
1365
  if (showLog.checked) {
1301
1366
  log.classList.remove("hidden");
1302
1367
  if (logControls) {
@@ -1352,17 +1417,17 @@ function updateAdvancedVisibility() {
1352
1417
  function updateMainPadding() {
1353
1418
  // Use requestAnimationFrame to ensure DOM has updated
1354
1419
  requestAnimationFrame(() => {
1355
- const header = document.querySelector('.header');
1356
- const main = document.querySelector('.main');
1357
-
1420
+ const header = document.querySelector(".header");
1421
+ const main = document.querySelector(".main");
1422
+
1358
1423
  // Guard against missing elements
1359
1424
  if (!header || !main) {
1360
1425
  return;
1361
1426
  }
1362
-
1427
+
1363
1428
  const headerHeight = header.offsetHeight;
1364
1429
  // Add small buffer (10px) for better spacing
1365
- main.style.paddingTop = (headerHeight + 10) + 'px';
1430
+ main.style.paddingTop = headerHeight + 10 + "px";
1366
1431
  });
1367
1432
  }
1368
1433
 
@@ -1372,21 +1437,21 @@ function updateMainPadding() {
1372
1437
  */
1373
1438
  async function clickDetectFS() {
1374
1439
  if (!espStub || !espStub.flashSize) {
1375
- errorMsg('Not connected or flash size unknown');
1440
+ errorMsg("Not connected or flash size unknown");
1376
1441
  return;
1377
1442
  }
1378
-
1443
+
1379
1444
  try {
1380
1445
  butDetectFS.disabled = true;
1381
- logMsg('Detecting ESP8266 filesystem...');
1382
-
1446
+ logMsg("Detecting ESP8266 filesystem...");
1447
+
1383
1448
  const flashSizeBytes = parseFlashSize(espStub.flashSize);
1384
1449
  const flashSizeMB = flashSizeBytes / (1024 * 1024);
1385
1450
  const esptoolMod = await window.esptoolPackage;
1386
-
1451
+
1387
1452
  // Scan flash for filesystem signatures - optimized based on flash size
1388
1453
  let scanOffsets = [];
1389
-
1454
+
1390
1455
  if (flashSizeMB >= 4) {
1391
1456
  // 4MB/8MB/16MB Flash
1392
1457
  scanOffsets = [
@@ -1413,19 +1478,19 @@ async function clickDetectFS() {
1413
1478
  { offset: 0x05b000, size: 0x20000 }, // Covers 128KB, 64KB, 32KB (0x05b000, 0x06b000, 0x073000)
1414
1479
  ];
1415
1480
  }
1416
-
1481
+
1417
1482
  // Collect all found filesystems
1418
1483
  const foundFilesystems = [];
1419
-
1484
+
1420
1485
  for (const scan of scanOffsets) {
1421
1486
  if (scan.offset + scan.size > flashSizeBytes) {
1422
1487
  continue;
1423
1488
  }
1424
-
1489
+
1425
1490
  try {
1426
1491
  logMsg(`Scanning at 0x${scan.offset.toString(16)}...`);
1427
1492
  const scanData = await espStub.readFlash(scan.offset, scan.size);
1428
-
1493
+
1429
1494
  // Check multiple offsets within the read data
1430
1495
  const checkOffsets = [];
1431
1496
  if (scan.size > 0x10000) {
@@ -1437,90 +1502,114 @@ async function clickDetectFS() {
1437
1502
  // Small read - check only start position
1438
1503
  checkOffsets.push(scan.offset);
1439
1504
  }
1440
-
1505
+
1441
1506
  for (const checkOffset of checkOffsets) {
1442
1507
  const dataOffset = checkOffset - scan.offset;
1443
1508
  if (dataOffset + 0x10000 > scanData.length) continue;
1444
-
1509
+
1445
1510
  const checkData = scanData.slice(dataOffset, dataOffset + 0x10000);
1446
- const scannedLayout = esptoolMod.scanESP8266Filesystem(checkData, checkOffset, flashSizeBytes);
1447
-
1511
+ const scannedLayout = esptoolMod.scanESP8266Filesystem(
1512
+ checkData,
1513
+ checkOffset,
1514
+ flashSizeBytes,
1515
+ );
1516
+
1448
1517
  if (scannedLayout) {
1449
- const fsType = esptoolMod.detectFilesystemFromImage(checkData, currentChipName);
1450
-
1518
+ const fsType = esptoolMod.detectFilesystemFromImage(
1519
+ checkData,
1520
+ currentChipName,
1521
+ );
1522
+
1451
1523
  // Validate: Check if it's a real filesystem with valid magic
1452
- if (fsType !== 'unknown') {
1524
+ if (fsType !== "unknown") {
1453
1525
  foundFilesystems.push({
1454
1526
  layout: scannedLayout,
1455
1527
  fsType: fsType,
1456
- data: checkData
1528
+ data: checkData,
1457
1529
  });
1458
- logMsg(`Found ${fsType.toUpperCase()} at 0x${scannedLayout.start.toString(16)} - 0x${scannedLayout.end.toString(16)} (${formatSize(scannedLayout.size)})`);
1530
+ logMsg(
1531
+ `Found ${fsType.toUpperCase()} at 0x${scannedLayout.start.toString(16)} - 0x${scannedLayout.end.toString(16)} (${formatSize(scannedLayout.size)})`,
1532
+ );
1459
1533
  }
1460
1534
  }
1461
1535
  }
1462
-
1463
1536
  } catch (e) {
1464
1537
  // Continue scanning
1465
- logMsg(`Scan at 0x${scan.offset.toString(16)} failed: ${e.message || e}`);
1538
+ logMsg(
1539
+ `Scan at 0x${scan.offset.toString(16)} failed: ${e.message || e}`,
1540
+ );
1466
1541
  }
1467
1542
  }
1468
-
1543
+
1469
1544
  // Choose the best filesystem from found ones
1470
1545
  let detectedLayout = null;
1471
-
1546
+
1472
1547
  if (foundFilesystems.length === 0) {
1473
1548
  // No filesystem found - use fallback
1474
- logMsg('No filesystem found by scanning, using default layout...');
1549
+ logMsg("No filesystem found by scanning, using default layout...");
1475
1550
  const fsLayouts = esptoolMod.getESP8266FilesystemLayout(flashSizeMB);
1476
1551
  if (fsLayouts && fsLayouts.length > 0) {
1477
1552
  detectedLayout = fsLayouts[0];
1478
- logMsg(`Using default layout for ${flashSizeMB}MB flash: 0x${detectedLayout.start.toString(16)} - 0x${detectedLayout.end.toString(16)} (${formatSize(detectedLayout.size)})`);
1553
+ logMsg(
1554
+ `Using default layout for ${flashSizeMB}MB flash: 0x${detectedLayout.start.toString(16)} - 0x${detectedLayout.end.toString(16)} (${formatSize(detectedLayout.size)})`,
1555
+ );
1479
1556
  }
1480
1557
  } else if (foundFilesystems.length === 1) {
1481
1558
  // Only one found - use it
1482
1559
  detectedLayout = foundFilesystems[0].layout;
1483
- logMsg(`Using detected filesystem at 0x${detectedLayout.start.toString(16)}`);
1560
+ logMsg(
1561
+ `Using detected filesystem at 0x${detectedLayout.start.toString(16)}`,
1562
+ );
1484
1563
  } else {
1485
1564
  // Multiple found - choose the best one
1486
1565
  // Prefer filesystems with valid size from block_count over layout-based sizes
1487
- logMsg(`Found ${foundFilesystems.length} filesystems, selecting best match...`);
1488
-
1566
+ logMsg(
1567
+ `Found ${foundFilesystems.length} filesystems, selecting best match...`,
1568
+ );
1569
+
1489
1570
  // Sort by: 1) Has valid block_count (size not from layout), 2) Smallest offset
1490
1571
  foundFilesystems.sort((a, b) => {
1491
1572
  // Check if size matches a known layout (indicates fallback was used)
1492
- const aIsLayout = [0x1fa000, 0x2fa000, 0x0fa000, 0x5fa000, 0x6fa000, 0xdfa000, 0xefa000].includes(a.layout.size);
1493
- const bIsLayout = [0x1fa000, 0x2fa000, 0x0fa000, 0x5fa000, 0x6fa000, 0xdfa000, 0xefa000].includes(b.layout.size);
1494
-
1573
+ const aIsLayout = [
1574
+ 0x1fa000, 0x2fa000, 0x0fa000, 0x5fa000, 0x6fa000, 0xdfa000, 0xefa000,
1575
+ ].includes(a.layout.size);
1576
+ const bIsLayout = [
1577
+ 0x1fa000, 0x2fa000, 0x0fa000, 0x5fa000, 0x6fa000, 0xdfa000, 0xefa000,
1578
+ ].includes(b.layout.size);
1579
+
1495
1580
  if (aIsLayout !== bIsLayout) {
1496
1581
  return aIsLayout ? 1 : -1; // Prefer non-layout (real block_count)
1497
1582
  }
1498
-
1583
+
1499
1584
  return a.layout.start - b.layout.start; // Then prefer smaller offset
1500
1585
  });
1501
-
1586
+
1502
1587
  detectedLayout = foundFilesystems[0].layout;
1503
- logMsg(`Selected filesystem at 0x${detectedLayout.start.toString(16)} (${formatSize(detectedLayout.size)})`);
1588
+ logMsg(
1589
+ `Selected filesystem at 0x${detectedLayout.start.toString(16)} (${formatSize(detectedLayout.size)})`,
1590
+ );
1504
1591
  }
1505
-
1592
+
1506
1593
  if (!detectedLayout) {
1507
- errorMsg('No filesystem layout found for this flash size');
1594
+ errorMsg("No filesystem layout found for this flash size");
1508
1595
  butDetectFS.disabled = false;
1509
1596
  return;
1510
1597
  }
1511
-
1598
+
1512
1599
  // Show progress bar
1513
- readProgress.classList.remove('hidden');
1514
- const progressBar = readProgress.querySelector('div');
1600
+ readProgress.classList.remove("hidden");
1601
+ const progressBar = readProgress.querySelector("div");
1515
1602
  if (progressBar) {
1516
- progressBar.style.width = '0%';
1603
+ progressBar.style.width = "0%";
1517
1604
  }
1518
-
1605
+
1519
1606
  // Read the filesystem with real progress tracking
1520
- logMsg(`Reading ${formatSize(detectedLayout.size)} from 0x${detectedLayout.start.toString(16)}...`);
1521
-
1607
+ logMsg(
1608
+ `Reading ${formatSize(detectedLayout.size)} from 0x${detectedLayout.start.toString(16)}...`,
1609
+ );
1610
+
1522
1611
  const fsData = await espStub.readFlash(
1523
- detectedLayout.start,
1612
+ detectedLayout.start,
1524
1613
  detectedLayout.size,
1525
1614
  (packet, progress, totalSize) => {
1526
1615
  // Update progress bar with real progress
@@ -1528,59 +1617,61 @@ async function clickDetectFS() {
1528
1617
  const percentage = (progress / totalSize) * 100;
1529
1618
  progressBar.style.width = `${percentage.toFixed(1)}%`;
1530
1619
  }
1531
- }
1620
+ },
1532
1621
  );
1533
-
1622
+
1534
1623
  // Keep progress bar at 100% for a moment
1535
1624
  if (progressBar) {
1536
- progressBar.style.width = '100%';
1625
+ progressBar.style.width = "100%";
1537
1626
  }
1538
- await new Promise(resolve => setTimeout(resolve, 300));
1539
- readProgress.classList.add('hidden');
1540
-
1627
+ await new Promise((resolve) => setTimeout(resolve, 300));
1628
+ readProgress.classList.add("hidden");
1629
+
1541
1630
  // Store the data for later use
1542
1631
  lastReadFlashData = fsData;
1543
1632
 
1544
1633
  // Hide Open FS Manager since we're opening directly
1545
- butOpenFSManager.classList.add('hidden');
1634
+ butOpenFSManager.classList.add("hidden");
1546
1635
 
1547
1636
  // Detect filesystem type
1548
- const fsType = esptoolMod.detectFilesystemFromImage(fsData, currentChipName);
1637
+ const fsType = esptoolMod.detectFilesystemFromImage(
1638
+ fsData,
1639
+ currentChipName,
1640
+ );
1549
1641
  logMsg(`Detected filesystem type: ${fsType.toUpperCase()}`);
1550
-
1551
- if (fsType === 'unknown') {
1552
- errorMsg('Could not detect filesystem type');
1642
+
1643
+ if (fsType === "unknown") {
1644
+ errorMsg("Could not detect filesystem type");
1553
1645
  butDetectFS.disabled = false;
1554
1646
  return;
1555
1647
  }
1556
-
1648
+
1557
1649
  // Create a partition object for compatibility
1558
1650
  const partition = {
1559
- name: 'filesystem',
1651
+ name: "filesystem",
1560
1652
  type: 0x01,
1561
- subtype: fsType === 'littlefs' ? 0x82 : (fsType === 'fatfs' ? 0x81 : 0x82),
1653
+ subtype: fsType === "littlefs" ? 0x82 : fsType === "fatfs" ? 0x81 : 0x82,
1562
1654
  offset: detectedLayout.start,
1563
1655
  size: detectedLayout.size,
1564
1656
  _readData: fsData,
1565
1657
  _blockSize: detectedLayout.block,
1566
- _pageSize: detectedLayout.page
1658
+ _pageSize: detectedLayout.page,
1567
1659
  };
1568
-
1660
+
1569
1661
  // Open the filesystem directly
1570
- if (fsType === 'littlefs') {
1662
+ if (fsType === "littlefs") {
1571
1663
  await openLittleFS(partition);
1572
- } else if (fsType === 'fatfs') {
1664
+ } else if (fsType === "fatfs") {
1573
1665
  await openFatFS(partition);
1574
- } else if (fsType === 'spiffs') {
1666
+ } else if (fsType === "spiffs") {
1575
1667
  await openSPIFFS(partition);
1576
1668
  }
1577
-
1578
1669
  } catch (e) {
1579
1670
  errorMsg(`Failed to detect/open filesystem: ${e.message || e}`);
1580
- debugMsg('Filesystem detection error details: ' + e);
1671
+ debugMsg("Filesystem detection error details: " + e);
1581
1672
  } finally {
1582
1673
  // Hide progress bar
1583
- readProgress.classList.add('hidden');
1674
+ readProgress.classList.add("hidden");
1584
1675
  butDetectFS.disabled = false;
1585
1676
  }
1586
1677
  }
@@ -1591,13 +1682,17 @@ async function clickDetectFS() {
1591
1682
  */
1592
1683
  async function clickErase() {
1593
1684
  let confirmed = false;
1594
-
1685
+
1595
1686
  if (isElectron) {
1596
- confirmed = await window.electronAPI.showConfirm("This will erase the entire flash. Click OK to continue.");
1687
+ confirmed = await window.electronAPI.showConfirm(
1688
+ "This will erase the entire flash. Click OK to continue.",
1689
+ );
1597
1690
  } else {
1598
- confirmed = window.confirm("This will erase the entire flash. Click OK to continue.");
1691
+ confirmed = window.confirm(
1692
+ "This will erase the entire flash. Click OK to continue.",
1693
+ );
1599
1694
  }
1600
-
1695
+
1601
1696
  if (confirmed) {
1602
1697
  baudRateSelect.disabled = true;
1603
1698
  butErase.disabled = true;
@@ -1730,14 +1825,14 @@ async function checkFirmware(event) {
1730
1825
  function updateUploadRowsVisibility() {
1731
1826
  const uploadRows = document.querySelectorAll(".upload");
1732
1827
  let lastFilledIndex = -1;
1733
-
1828
+
1734
1829
  // Find the last filled row
1735
1830
  for (let i = 0; i < firmware.length; i++) {
1736
1831
  if (firmware[i].files.length > 0) {
1737
1832
  lastFilledIndex = i;
1738
1833
  }
1739
1834
  }
1740
-
1835
+
1741
1836
  // Show rows up to lastFilledIndex + 1 (next empty row), minimum 1 row
1742
1837
  for (let i = 0; i < uploadRows.length; i++) {
1743
1838
  if (i <= lastFilledIndex + 1) {
@@ -1762,9 +1857,11 @@ async function clickReadFlash() {
1762
1857
  }
1763
1858
 
1764
1859
  // Create filename with chip type and MAC address
1765
- const chipInfo = currentChipName ? currentChipName.replace(/\s+/g, '_') : 'ESP';
1766
- const macInfo = currentMacAddr ? currentMacAddr.replace(/:/g, '') : '';
1767
- const defaultFilename = `${chipInfo}${macInfo ? '_' + macInfo : ''}_flash_0x${offset.toString(16)}_0x${size.toString(16)}.bin`;
1860
+ const chipInfo = currentChipName
1861
+ ? currentChipName.replace(/\s+/g, "_")
1862
+ : "ESP";
1863
+ const macInfo = currentMacAddr ? currentMacAddr.replace(/:/g, "") : "";
1864
+ const defaultFilename = `${chipInfo}${macInfo ? "_" + macInfo : ""}_flash_0x${offset.toString(16)}_0x${size.toString(16)}.bin`;
1768
1865
 
1769
1866
  baudRateSelect.disabled = true;
1770
1867
  butErase.disabled = true;
@@ -1780,7 +1877,9 @@ async function clickReadFlash() {
1780
1877
  // Prepare options object if advanced mode is enabled
1781
1878
  const options = buildAdvancedOptions();
1782
1879
  if (options) {
1783
- logMsg(`Advanced mode: chunkSize=0x${options.chunkSize?.toString(16)}, blockSize=${options.blockSize}, maxInFlight=${options.maxInFlight}`);
1880
+ logMsg(
1881
+ `Advanced mode: chunkSize=0x${options.chunkSize?.toString(16)}, blockSize=${options.blockSize}, maxInFlight=${options.maxInFlight}`,
1882
+ );
1784
1883
  }
1785
1884
 
1786
1885
  const data = await espStub.readFlash(
@@ -1790,39 +1889,38 @@ async function clickReadFlash() {
1790
1889
  progressBar.style.width =
1791
1890
  Math.floor((progress / totalSize) * 100) + "%";
1792
1891
  },
1793
- options
1892
+ options,
1794
1893
  );
1795
1894
 
1796
1895
  logMsg(`Successfully read ${data.length} bytes from flash`);
1797
1896
 
1798
1897
  // Save file using Electron API or browser download
1799
1898
  await saveDataToFile(data, defaultFilename);
1800
-
1899
+
1801
1900
  // Check if this looks like a filesystem
1802
- const chipName = currentChipName || '';
1901
+ const chipName = currentChipName || "";
1803
1902
  const esptoolMod = await window.esptoolPackage;
1804
1903
  const fsType = esptoolMod.detectFilesystemFromImage(data, chipName);
1805
-
1806
- if (fsType !== 'unknown') {
1904
+
1905
+ if (fsType !== "unknown") {
1807
1906
  logMsg(`Detected ${fsType} filesystem in read data`);
1808
-
1907
+
1809
1908
  // Store the read data and metadata for later use
1810
1909
  lastReadFlashData = {
1811
1910
  data: data,
1812
1911
  offset: offset,
1813
1912
  size: size,
1814
- fsType: fsType
1913
+ fsType: fsType,
1815
1914
  };
1816
-
1915
+
1817
1916
  // Show "Open FS Manager" button
1818
- butOpenFSManager.classList.remove('hidden');
1917
+ butOpenFSManager.classList.remove("hidden");
1819
1918
  logMsg('Click "Open FS Manager" to access the filesystem');
1820
1919
  } else {
1821
1920
  // Hide button if no filesystem detected
1822
- butOpenFSManager.classList.add('hidden');
1921
+ butOpenFSManager.classList.add("hidden");
1823
1922
  lastReadFlashData = null;
1824
1923
  }
1825
-
1826
1924
  } catch (e) {
1827
1925
  errorMsg("Failed to read flash: " + e);
1828
1926
  } finally {
@@ -1861,12 +1959,12 @@ async function clickHexEditor() {
1861
1959
  }
1862
1960
 
1863
1961
  // Show the container and progress overlay immediately
1864
- hexeditorContainer.classList.remove('hidden');
1865
- document.body.classList.add('hexeditor-active');
1962
+ hexeditorContainer.classList.remove("hidden");
1963
+ document.body.classList.add("hexeditor-active");
1866
1964
 
1867
1965
  // Build a temporary UI for progress display
1868
1966
  hexEditorInstance.initProgressUI();
1869
- hexEditorInstance.showProgress('Reading flash...', 0);
1967
+ hexEditorInstance.showProgress("Reading flash...", 0);
1870
1968
 
1871
1969
  // Prepare options
1872
1970
  const options = buildAdvancedOptions();
@@ -1878,10 +1976,10 @@ async function clickHexEditor() {
1878
1976
  const pct = Math.floor((progress / totalSize) * 100);
1879
1977
  hexEditorInstance.showProgress(
1880
1978
  `Reading flash... ${pct}% (${(progress / 1024).toFixed(0)} / ${(totalSize / 1024).toFixed(0)} KB)`,
1881
- pct
1979
+ pct,
1882
1980
  );
1883
1981
  },
1884
- options
1982
+ options,
1885
1983
  );
1886
1984
 
1887
1985
  logMsg(`Successfully read ${data.length} bytes from flash for hex editor`);
@@ -1913,7 +2011,7 @@ async function clickHexEditor() {
1913
2011
 
1914
2012
  editor.showProgress(
1915
2013
  `Writing sector at 0x${flashAddr.toString(16).toUpperCase()}... (${written + 1}/${total})`,
1916
- Math.floor((written / total) * 100)
2014
+ Math.floor((written / total) * 100),
1917
2015
  );
1918
2016
 
1919
2017
  await espStub.flashData(
@@ -1922,15 +2020,15 @@ async function clickHexEditor() {
1922
2020
  const sectorPct = Math.floor((bytesWritten / totalBytes) * 100);
1923
2021
  editor.showProgress(
1924
2022
  `Writing sector at 0x${flashAddr.toString(16).toUpperCase()}... ${sectorPct}% (${written + 1}/${total})`,
1925
- Math.floor(((written + bytesWritten / totalBytes) / total) * 100)
2023
+ Math.floor(((written + bytesWritten / totalBytes) / total) * 100),
1926
2024
  );
1927
2025
  },
1928
- flashAddr
2026
+ flashAddr,
1929
2027
  );
1930
2028
  written++;
1931
2029
  }
1932
2030
 
1933
- editor.showProgress('Write complete!', 100);
2031
+ editor.showProgress("Write complete!", 100);
1934
2032
  logMsg(`Successfully wrote ${total} sector(s) to flash`);
1935
2033
  await sleep(500);
1936
2034
  editor.hideProgress();
@@ -1938,20 +2036,21 @@ async function clickHexEditor() {
1938
2036
 
1939
2037
  // Set up close handler
1940
2038
  hexEditorInstance.onClose = () => {
1941
- hexeditorContainer.classList.add('hidden');
1942
- document.body.classList.remove('hexeditor-active');
2039
+ hexeditorContainer.classList.add("hidden");
2040
+ document.body.classList.remove("hexeditor-active");
1943
2041
  hexEditorInstance = null;
1944
2042
  };
1945
-
1946
2043
  } catch (e) {
1947
2044
  errorMsg("Failed to read flash for hex editor: " + e);
1948
- hexeditorContainer.classList.add('hidden');
1949
- document.body.classList.remove('hexeditor-active');
2045
+ hexeditorContainer.classList.add("hidden");
2046
+ document.body.classList.remove("hexeditor-active");
1950
2047
  if (hexEditorInstance) {
1951
- // close() handles ResizeObserver/keydown cleanup; onClose is not yet wired here so null manually
1952
- try { hexEditorInstance.close(); } catch (_) {}
1953
- hexEditorInstance = null;
1954
- }
2048
+ // close() handles ResizeObserver/keydown cleanup; onClose is not yet wired here so null manually
2049
+ try {
2050
+ hexEditorInstance.close();
2051
+ } catch (_) {}
2052
+ hexEditorInstance = null;
2053
+ }
1955
2054
  } finally {
1956
2055
  butHexEditor.disabled = false;
1957
2056
  }
@@ -1967,14 +2066,20 @@ async function clickNVSEditor() {
1967
2066
 
1968
2067
  // Guard against losing unsaved changes
1969
2068
  if (nvsEditorInstance && nvsEditorInstance.modified === true) {
1970
- if (!confirm('You have unsaved changes in the NVS editor. Discard changes and reload?')) {
2069
+ if (
2070
+ !confirm(
2071
+ "You have unsaved changes in the NVS editor. Discard changes and reload?",
2072
+ )
2073
+ ) {
1971
2074
  butNVSEditor.disabled = false;
1972
2075
  return;
1973
2076
  }
1974
2077
  // User confirmed - close existing editor
1975
- nvseditorContainer.classList.add('hidden');
1976
- document.body.classList.remove('nvseditor-active');
1977
- try { nvsEditorInstance.close(); } catch (_) {}
2078
+ nvseditorContainer.classList.add("hidden");
2079
+ document.body.classList.remove("nvseditor-active");
2080
+ try {
2081
+ nvsEditorInstance.close();
2082
+ } catch (_) {}
1978
2083
  nvsEditorInstance = null;
1979
2084
  }
1980
2085
 
@@ -1984,10 +2089,10 @@ async function clickNVSEditor() {
1984
2089
  if (!nvsEditorInstance) {
1985
2090
  nvsEditorInstance = new NVSEditor(nvseditorContainer);
1986
2091
  }
1987
- nvseditorContainer.classList.remove('hidden');
1988
- document.body.classList.add('nvseditor-active');
2092
+ nvseditorContainer.classList.remove("hidden");
2093
+ document.body.classList.add("nvseditor-active");
1989
2094
  nvsEditorInstance.initProgressUI();
1990
- nvsEditorInstance.showProgress('Reading partition table...', 0);
2095
+ nvsEditorInstance.showProgress("Reading partition table...", 0);
1991
2096
 
1992
2097
  const MAX_PT_ATTEMPTS = 10;
1993
2098
  let partitions = [];
@@ -1999,19 +2104,25 @@ async function clickNVSEditor() {
1999
2104
  if (partitions.length > 0) break;
2000
2105
  }
2001
2106
  if (partitions.length === 0) {
2002
- throw new Error('No valid partition table found after ' + MAX_PT_ATTEMPTS + ' attempts');
2107
+ throw new Error(
2108
+ "No valid partition table found after " + MAX_PT_ATTEMPTS + " attempts",
2109
+ );
2003
2110
  }
2004
2111
 
2005
2112
  // Step 2: Find NVS partition (type=0x01 data, subtype=0x02 nvs)
2006
- const nvsPartition = partitions.find(p => p.type === 0x01 && p.subtype === 0x02);
2113
+ const nvsPartition = partitions.find(
2114
+ (p) => p.type === 0x01 && p.subtype === 0x02,
2115
+ );
2007
2116
  if (!nvsPartition) {
2008
- throw new Error('No NVS partition found in partition table');
2117
+ throw new Error("No NVS partition found in partition table");
2009
2118
  }
2010
2119
 
2011
- logMsg(`Found NVS partition "${nvsPartition.name}" at 0x${nvsPartition.offset.toString(16)}, size ${nvsPartition.size} bytes`);
2120
+ logMsg(
2121
+ `Found NVS partition "${nvsPartition.name}" at 0x${nvsPartition.offset.toString(16)}, size ${nvsPartition.size} bytes`,
2122
+ );
2012
2123
 
2013
2124
  // Step 3: Read the NVS partition
2014
- nvsEditorInstance.showProgress('Reading NVS partition...', 10);
2125
+ nvsEditorInstance.showProgress("Reading NVS partition...", 10);
2015
2126
  const options = buildAdvancedOptions();
2016
2127
  const nvsData = await espStub.readFlash(
2017
2128
  nvsPartition.offset,
@@ -2020,16 +2131,16 @@ async function clickNVSEditor() {
2020
2131
  const pct = 10 + Math.floor((progress / totalSize) * 85);
2021
2132
  nvsEditorInstance.showProgress(
2022
2133
  `Reading NVS... ${Math.floor((progress / totalSize) * 100)}% (${(progress / 1024).toFixed(0)} / ${(totalSize / 1024).toFixed(0)} KB)`,
2023
- pct
2134
+ pct,
2024
2135
  );
2025
2136
  },
2026
- options
2137
+ options,
2027
2138
  );
2028
2139
 
2029
2140
  logMsg(`Successfully read ${nvsData.length} bytes from NVS partition`);
2030
2141
 
2031
2142
  // Step 4: Open NVS editor
2032
- nvsEditorInstance.showProgress('Parsing NVS data...', 95);
2143
+ nvsEditorInstance.showProgress("Parsing NVS data...", 95);
2033
2144
  nvsEditorInstance.open(nvsData, nvsPartition.offset, nvsPartition.name);
2034
2145
 
2035
2146
  // Step 5: Set up write handler
@@ -2037,25 +2148,25 @@ async function clickNVSEditor() {
2037
2148
  const editor = nvsEditorInstance;
2038
2149
  if (!editor) return;
2039
2150
 
2040
- editor.showProgress('Writing NVS partition...', 0);
2151
+ editor.showProgress("Writing NVS partition...", 0);
2041
2152
 
2042
2153
  try {
2043
2154
  const nvsBuffer = editedData.buffer.slice(
2044
2155
  editedData.byteOffset,
2045
- editedData.byteOffset + editedData.byteLength
2046
- );
2047
- await espStub.flashData(
2048
- nvsBuffer,
2049
- (bytesWritten, totalBytes) => {
2050
- const pct = Math.floor((bytesWritten / totalBytes) * 100);
2051
- editor.showProgress(`Writing NVS... ${pct}%`, pct);
2052
- },
2053
- nvsPartition.offset
2054
- );
2156
+ editedData.byteOffset + editedData.byteLength,
2157
+ );
2158
+ await espStub.flashData(
2159
+ nvsBuffer,
2160
+ (bytesWritten, totalBytes) => {
2161
+ const pct = Math.floor((bytesWritten / totalBytes) * 100);
2162
+ editor.showProgress(`Writing NVS... ${pct}%`, pct);
2163
+ },
2164
+ nvsPartition.offset,
2165
+ );
2055
2166
 
2056
- editor.showProgress('Write complete!', 100);
2057
- logMsg('NVS partition written successfully');
2058
- await sleep(500);
2167
+ editor.showProgress("Write complete!", 100);
2168
+ logMsg("NVS partition written successfully");
2169
+ await sleep(500);
2059
2170
  } finally {
2060
2171
  editor.hideProgress();
2061
2172
  }
@@ -2063,17 +2174,18 @@ async function clickNVSEditor() {
2063
2174
 
2064
2175
  // Step 6: Set up close handler
2065
2176
  nvsEditorInstance.onClose = () => {
2066
- nvseditorContainer.classList.add('hidden');
2067
- document.body.classList.remove('nvseditor-active');
2177
+ nvseditorContainer.classList.add("hidden");
2178
+ document.body.classList.remove("nvseditor-active");
2068
2179
  nvsEditorInstance = null;
2069
2180
  };
2070
-
2071
2181
  } catch (e) {
2072
- errorMsg('Failed to open NVS editor: ' + e);
2073
- nvseditorContainer.classList.add('hidden');
2074
- document.body.classList.remove('nvseditor-active');
2182
+ errorMsg("Failed to open NVS editor: " + e);
2183
+ nvseditorContainer.classList.add("hidden");
2184
+ document.body.classList.remove("nvseditor-active");
2075
2185
  if (nvsEditorInstance) {
2076
- try { nvsEditorInstance.close(); } catch (_) {}
2186
+ try {
2187
+ nvsEditorInstance.close();
2188
+ } catch (_) {}
2077
2189
  nvsEditorInstance = null;
2078
2190
  }
2079
2191
  } finally {
@@ -2087,10 +2199,10 @@ async function clickNVSEditor() {
2087
2199
  */
2088
2200
  async function clickOpenFSManager() {
2089
2201
  if (!lastReadFlashData) {
2090
- errorMsg('No filesystem data available. Please read flash first.');
2202
+ errorMsg("No filesystem data available. Please read flash first.");
2091
2203
  return;
2092
2204
  }
2093
-
2205
+
2094
2206
  try {
2095
2207
  // Create a pseudo-partition object for the read data
2096
2208
  const pseudoPartition = {
@@ -2098,10 +2210,10 @@ async function clickOpenFSManager() {
2098
2210
  offset: lastReadFlashData.offset,
2099
2211
  size: lastReadFlashData.size,
2100
2212
  type: 0x01,
2101
- subtype: lastReadFlashData.fsType === 'fatfs' ? 0x81 : 0x82,
2102
- _readData: lastReadFlashData.data // Store the already-read data
2213
+ subtype: lastReadFlashData.fsType === "fatfs" ? 0x81 : 0x82,
2214
+ _readData: lastReadFlashData.data, // Store the already-read data
2103
2215
  };
2104
-
2216
+
2105
2217
  await openFilesystem(pseudoPartition);
2106
2218
  } catch (e) {
2107
2219
  errorMsg(`Failed to open filesystem: ${e.message || e}`);
@@ -2113,7 +2225,6 @@ async function clickOpenFSManager() {
2113
2225
  * Click handler for the read partitions button.
2114
2226
  */
2115
2227
  async function clickReadPartitions() {
2116
-
2117
2228
  butReadPartitions.disabled = true;
2118
2229
  butErase.disabled = true;
2119
2230
  butProgram.disabled = true;
@@ -2137,15 +2248,18 @@ async function clickReadPartitions() {
2137
2248
  }
2138
2249
 
2139
2250
  if (partitions.length === 0) {
2140
- errorMsg("No valid partition table found after " + MAX_ATTEMPTS + " attempts");
2251
+ errorMsg(
2252
+ "No valid partition table found after " + MAX_ATTEMPTS + " attempts",
2253
+ );
2141
2254
  return;
2142
2255
  }
2143
2256
 
2144
- logMsg(`Found ${partitions.length} partition(s) at 0x${foundOffset.toString(16)}`);
2145
-
2257
+ logMsg(
2258
+ `Found ${partitions.length} partition(s) at 0x${foundOffset.toString(16)}`,
2259
+ );
2260
+
2146
2261
  // Display partitions
2147
2262
  displayPartitions(partitions);
2148
-
2149
2263
  } catch (e) {
2150
2264
  errorMsg("Failed to read partition table: " + e);
2151
2265
  } finally {
@@ -2166,16 +2280,24 @@ function parsePartitionTable(data) {
2166
2280
 
2167
2281
  for (let i = 0; i < data.length; i += PARTITION_ENTRY_SIZE) {
2168
2282
  const magic = data[i] | (data[i + 1] << 8);
2169
-
2283
+
2170
2284
  if (magic !== PARTITION_MAGIC) {
2171
2285
  break; // End of partition table
2172
2286
  }
2173
2287
 
2174
2288
  const type = data[i + 2];
2175
2289
  const subtype = data[i + 3];
2176
- const offset = data[i + 4] | (data[i + 5] << 8) | (data[i + 6] << 16) | (data[i + 7] << 24);
2177
- const size = data[i + 8] | (data[i + 9] << 8) | (data[i + 10] << 16) | (data[i + 11] << 24);
2178
-
2290
+ const offset =
2291
+ data[i + 4] |
2292
+ (data[i + 5] << 8) |
2293
+ (data[i + 6] << 16) |
2294
+ (data[i + 7] << 24);
2295
+ const size =
2296
+ data[i + 8] |
2297
+ (data[i + 9] << 8) |
2298
+ (data[i + 10] << 16) |
2299
+ (data[i + 11] << 24);
2300
+
2179
2301
  // Read name (16 bytes, null-terminated)
2180
2302
  let name = "";
2181
2303
  for (let j = 12; j < 28; j++) {
@@ -2183,17 +2305,33 @@ function parsePartitionTable(data) {
2183
2305
  name += String.fromCharCode(data[i + j]);
2184
2306
  }
2185
2307
 
2186
- const flags = data[i + 28] | (data[i + 29] << 8) | (data[i + 30] << 16) | (data[i + 31] << 24);
2308
+ const flags =
2309
+ data[i + 28] |
2310
+ (data[i + 29] << 8) |
2311
+ (data[i + 30] << 16) |
2312
+ (data[i + 31] << 24);
2187
2313
 
2188
2314
  // Get type names
2189
2315
  const typeNames = { 0x00: "app", 0x01: "data" };
2190
2316
  const appSubtypes = {
2191
- 0x00: "factory", 0x10: "ota_0", 0x11: "ota_1", 0x12: "ota_2",
2192
- 0x13: "ota_3", 0x14: "ota_4", 0x15: "ota_5", 0x20: "test"
2317
+ 0x00: "factory",
2318
+ 0x10: "ota_0",
2319
+ 0x11: "ota_1",
2320
+ 0x12: "ota_2",
2321
+ 0x13: "ota_3",
2322
+ 0x14: "ota_4",
2323
+ 0x15: "ota_5",
2324
+ 0x20: "test",
2193
2325
  };
2194
2326
  const dataSubtypes = {
2195
- 0x00: "ota", 0x01: "phy", 0x02: "nvs", 0x03: "coredump",
2196
- 0x04: "nvs_keys", 0x05: "efuse", 0x81: "fat", 0x82: "spiffs"
2327
+ 0x00: "ota",
2328
+ 0x01: "phy",
2329
+ 0x02: "nvs",
2330
+ 0x03: "coredump",
2331
+ 0x04: "nvs_keys",
2332
+ 0x05: "efuse",
2333
+ 0x81: "fat",
2334
+ 0x82: "spiffs",
2197
2335
  };
2198
2336
 
2199
2337
  const typeName = typeNames[type] || `0x${type.toString(16)}`;
@@ -2214,7 +2352,7 @@ function parsePartitionTable(data) {
2214
2352
  size,
2215
2353
  flags,
2216
2354
  typeName,
2217
- subtypeName
2355
+ subtypeName,
2218
2356
  });
2219
2357
  }
2220
2358
 
@@ -2227,17 +2365,17 @@ function parsePartitionTable(data) {
2227
2365
  function displayPartitions(partitions) {
2228
2366
  partitionList.innerHTML = "";
2229
2367
  partitionList.classList.remove("hidden");
2230
-
2368
+
2231
2369
  // Hide the Read Partition Table button after successful read
2232
2370
  butReadPartitions.classList.add("hidden");
2233
2371
 
2234
2372
  const table = document.createElement("table");
2235
2373
  table.className = "partition-table-display";
2236
-
2374
+
2237
2375
  // Header
2238
2376
  const thead = document.createElement("thead");
2239
2377
  const headerRow = document.createElement("tr");
2240
- ["Name", "Type", "SubType", "Offset", "Size", "Action"].forEach(text => {
2378
+ ["Name", "Type", "SubType", "Offset", "Size", "Action"].forEach((text) => {
2241
2379
  const th = document.createElement("th");
2242
2380
  th.textContent = text;
2243
2381
  headerRow.appendChild(th);
@@ -2247,39 +2385,39 @@ function displayPartitions(partitions) {
2247
2385
 
2248
2386
  // Body
2249
2387
  const tbody = document.createElement("tbody");
2250
- partitions.forEach(partition => {
2388
+ partitions.forEach((partition) => {
2251
2389
  const row = document.createElement("tr");
2252
-
2390
+
2253
2391
  // Name
2254
2392
  const nameCell = document.createElement("td");
2255
2393
  nameCell.setAttribute("data-label", "Name");
2256
2394
  nameCell.textContent = partition.name;
2257
2395
  row.appendChild(nameCell);
2258
-
2396
+
2259
2397
  // Type
2260
2398
  const typeCell = document.createElement("td");
2261
2399
  typeCell.setAttribute("data-label", "Type");
2262
2400
  typeCell.textContent = partition.typeName;
2263
2401
  row.appendChild(typeCell);
2264
-
2402
+
2265
2403
  // SubType
2266
2404
  const subtypeCell = document.createElement("td");
2267
2405
  subtypeCell.setAttribute("data-label", "SubType");
2268
2406
  subtypeCell.textContent = partition.subtypeName;
2269
2407
  row.appendChild(subtypeCell);
2270
-
2408
+
2271
2409
  // Offset
2272
2410
  const offsetCell = document.createElement("td");
2273
2411
  offsetCell.setAttribute("data-label", "Offset");
2274
2412
  offsetCell.textContent = `0x${partition.offset.toString(16)}`;
2275
2413
  row.appendChild(offsetCell);
2276
-
2414
+
2277
2415
  // Size
2278
2416
  const sizeCell = document.createElement("td");
2279
2417
  sizeCell.setAttribute("data-label", "Size");
2280
2418
  sizeCell.textContent = formatSize(partition.size);
2281
2419
  row.appendChild(sizeCell);
2282
-
2420
+
2283
2421
  // Action
2284
2422
  const actionCell = document.createElement("td");
2285
2423
  actionCell.setAttribute("data-label", "Action");
@@ -2288,23 +2426,26 @@ function displayPartitions(partitions) {
2288
2426
  downloadBtn.className = "partition-download-btn";
2289
2427
  downloadBtn.onclick = () => downloadPartition(partition);
2290
2428
  actionCell.appendChild(downloadBtn);
2291
-
2429
+
2292
2430
  // Add "Open FS" button for data partitions with filesystem
2293
2431
  // 0x81 = FAT, 0x82 = SPIFFS (often contains LittleFS)
2294
- if (partition.type === 0x01 && (partition.subtype === 0x81 || partition.subtype === 0x82)) {
2432
+ if (
2433
+ partition.type === 0x01 &&
2434
+ (partition.subtype === 0x81 || partition.subtype === 0x82)
2435
+ ) {
2295
2436
  const fsBtn = document.createElement("button");
2296
2437
  fsBtn.textContent = "Open FS";
2297
2438
  fsBtn.className = "littlefs-fs-button";
2298
2439
  fsBtn.onclick = () => openFilesystem(partition);
2299
2440
  actionCell.appendChild(fsBtn);
2300
2441
  }
2301
-
2442
+
2302
2443
  row.appendChild(actionCell);
2303
-
2444
+
2304
2445
  tbody.appendChild(row);
2305
2446
  });
2306
2447
  table.appendChild(tbody);
2307
-
2448
+
2308
2449
  partitionList.appendChild(table);
2309
2450
  }
2310
2451
 
@@ -2313,9 +2454,11 @@ function displayPartitions(partitions) {
2313
2454
  */
2314
2455
  async function downloadPartition(partition) {
2315
2456
  // Create filename with chip type and MAC address
2316
- const chipInfo = currentChipName ? currentChipName.replace(/\s+/g, '_') : 'ESP';
2317
- const macInfo = currentMacAddr ? currentMacAddr.replace(/:/g, '') : '';
2318
- const defaultFilename = `${chipInfo}${macInfo ? '_' + macInfo : ''}_${partition.name}_0x${partition.offset.toString(16)}.bin`;
2457
+ const chipInfo = currentChipName
2458
+ ? currentChipName.replace(/\s+/g, "_")
2459
+ : "ESP";
2460
+ const macInfo = currentMacAddr ? currentMacAddr.replace(/:/g, "") : "";
2461
+ const defaultFilename = `${chipInfo}${macInfo ? "_" + macInfo : ""}_${partition.name}_0x${partition.offset.toString(16)}.bin`;
2319
2462
 
2320
2463
  const partitionProgress = document.getElementById("partitionProgress");
2321
2464
  const progressBar = partitionProgress.querySelector("div");
@@ -2325,7 +2468,7 @@ async function downloadPartition(partition) {
2325
2468
  progressBar.style.width = "0%";
2326
2469
 
2327
2470
  logMsg(
2328
- `Downloading partition "${partition.name}" (${formatSize(partition.size)})...`
2471
+ `Downloading partition "${partition.name}" (${formatSize(partition.size)})...`,
2329
2472
  );
2330
2473
 
2331
2474
  const data = await espStub.readFlash(
@@ -2334,7 +2477,7 @@ async function downloadPartition(partition) {
2334
2477
  (packet, progress, totalSize) => {
2335
2478
  const percent = Math.floor((progress / totalSize) * 100);
2336
2479
  progressBar.style.width = percent + "%";
2337
- }
2480
+ },
2338
2481
  );
2339
2482
 
2340
2483
  // Save file using Electron API or browser download
@@ -2367,7 +2510,7 @@ function formatSize(bytes) {
2367
2510
  * Click handler for the clear button.
2368
2511
  */
2369
2512
  async function clickClear() {
2370
- // reset(); Reset function wasnt declared.
2513
+ // reset(); Reset function wasnt declared.
2371
2514
  log.innerHTML = "";
2372
2515
  }
2373
2516
 
@@ -2402,30 +2545,29 @@ function toggleUIConnected(connected) {
2402
2545
  let lbl = "Connect";
2403
2546
  const header = document.querySelector(".header");
2404
2547
  const main = document.querySelector(".main");
2405
-
2548
+
2406
2549
  if (connected) {
2407
2550
  lbl = "Disconnect";
2408
2551
  isConnected = true;
2409
-
2410
2552
  } else {
2411
2553
  isConnected = false;
2412
2554
  toggleUIToolbar(false);
2413
-
2555
+
2414
2556
  // Cleanup console if it was running
2415
2557
  if (consoleInstance) {
2416
- consoleInstance.disconnect().catch(err => {
2558
+ consoleInstance.disconnect().catch((err) => {
2417
2559
  debugMsg("Error disconnecting console: " + err);
2418
2560
  });
2419
2561
  consoleInstance = null;
2420
2562
  }
2421
-
2563
+
2422
2564
  // Hide console container, show commands, and uncheck switch
2423
2565
  consoleContainer.classList.add("hidden");
2424
2566
  const commands = document.getElementById("commands");
2425
2567
  if (commands) commands.classList.remove("hidden");
2426
2568
  consoleSwitch.checked = false;
2427
2569
  saveSetting("console", false);
2428
-
2570
+
2429
2571
  // Close hex editor if open
2430
2572
  if (hexEditorInstance) {
2431
2573
  hexEditorInstance.close();
@@ -2433,11 +2575,13 @@ function toggleUIConnected(connected) {
2433
2575
  }
2434
2576
  // Close NVS editor if open (disconnect or unexpected port loss)
2435
2577
  if (nvsEditorInstance) {
2436
- try { nvsEditorInstance.close(); } catch (_) {}
2578
+ try {
2579
+ nvsEditorInstance.close();
2580
+ } catch (_) {}
2437
2581
  nvsEditorInstance = null;
2438
2582
  }
2439
- nvseditorContainer.classList.add('hidden');
2440
- document.body.classList.remove('nvseditor-active');
2583
+ nvseditorContainer.classList.add("hidden");
2584
+ document.body.classList.remove("nvseditor-active");
2441
2585
  }
2442
2586
  butConnect.textContent = lbl;
2443
2587
  }
@@ -2445,7 +2589,7 @@ function toggleUIConnected(connected) {
2445
2589
  function loadAllSettings() {
2446
2590
  // Get default values based on environment (Desktop vs WebUSB)
2447
2591
  const defaults = getDefaultAdvancedParams();
2448
-
2592
+
2449
2593
  // Load all saved settings or defaults
2450
2594
  autoscroll.checked = loadSetting("autoscroll", true);
2451
2595
  baudRateSelect.value = loadSetting("baudrate", 2000000);
@@ -2454,18 +2598,18 @@ function loadAllSettings() {
2454
2598
  showLog.checked = loadSetting("showlog", false);
2455
2599
  consoleSwitch.checked = loadSetting("console", false);
2456
2600
  advancedMode.checked = loadSetting("advanced", false);
2457
-
2601
+
2458
2602
  // Load advanced parameters with environment-specific defaults
2459
2603
  chunkSizeSelect.value = loadSetting("chunkSize", defaults.chunkSize);
2460
2604
  blockSizeSelect.value = loadSetting("blockSize", defaults.blockSize);
2461
2605
  maxInFlightSelect.value = loadSetting("maxInFlight", defaults.maxInFlight);
2462
-
2606
+
2463
2607
  // Apply show log setting
2464
2608
  updateLogVisibility();
2465
-
2609
+
2466
2610
  // Don't show console container here - it will be initialized after connect
2467
2611
  // if consoleSwitch.checked is true
2468
-
2612
+
2469
2613
  // Apply advanced mode visibility
2470
2614
  updateAdvancedVisibility();
2471
2615
  }
@@ -2498,9 +2642,9 @@ async function saveDataToFile(data, defaultFilename) {
2498
2642
  // Use Electron's native save dialog
2499
2643
  const result = await window.electronAPI.saveFile(
2500
2644
  Array.from(data), // Convert Uint8Array to regular array for IPC
2501
- defaultFilename
2645
+ defaultFilename,
2502
2646
  );
2503
-
2647
+
2504
2648
  if (result.success) {
2505
2649
  logMsg(`File saved: ${result.filePath}`);
2506
2650
  } else if (result.canceled) {
@@ -2529,12 +2673,12 @@ async function saveDataToFile(data, defaultFilename) {
2529
2673
  async function readFileFromDisk() {
2530
2674
  if (isElectron) {
2531
2675
  const result = await window.electronAPI.openFile();
2532
-
2676
+
2533
2677
  if (result.success) {
2534
2678
  return {
2535
2679
  data: new Uint8Array(result.data),
2536
2680
  filename: result.filename,
2537
- filePath: result.filePath
2681
+ filePath: result.filePath,
2538
2682
  };
2539
2683
  } else if (result.canceled) {
2540
2684
  return null;
@@ -2545,25 +2689,24 @@ async function readFileFromDisk() {
2545
2689
  return null;
2546
2690
  }
2547
2691
 
2548
-
2549
2692
  /**
2550
2693
  * Open and mount a filesystem partition
2551
2694
  */
2552
2695
  async function openFilesystem(partition) {
2553
2696
  try {
2554
2697
  logMsg(`Detecting filesystem type for partition "${partition.name}"...`);
2555
-
2698
+
2556
2699
  // Detect filesystem type
2557
2700
  const fsType = await detectFilesystemType(partition.offset, partition.size);
2558
-
2559
- if (fsType === 'littlefs') {
2701
+
2702
+ if (fsType === "littlefs") {
2560
2703
  await openLittleFS(partition);
2561
- } else if (fsType === 'fatfs') {
2704
+ } else if (fsType === "fatfs") {
2562
2705
  await openFatFS(partition);
2563
- } else if (fsType === 'spiffs') {
2706
+ } else if (fsType === "spiffs") {
2564
2707
  await openSPIFFS(partition);
2565
2708
  } else {
2566
- errorMsg('Unknown filesystem type. Cannot open partition.');
2709
+ errorMsg("Unknown filesystem type. Cannot open partition.");
2567
2710
  }
2568
2711
  } catch (e) {
2569
2712
  errorMsg(`Failed to open filesystem: ${e.message || e}`);
@@ -2572,14 +2715,14 @@ async function openFilesystem(partition) {
2572
2715
 
2573
2716
  /**
2574
2717
  * Detect filesystem type by reading partition header
2575
- *
2718
+ *
2576
2719
  * Uses the centralized detectFilesystemFromImage function from the esptool module.
2577
2720
  * This function properly validates filesystem structures:
2578
- *
2721
+ *
2579
2722
  * - LittleFS: Validates superblock at block 0/1 with "littlefs" magic at offset 8 and version check
2580
2723
  * - FatFS: Checks for FAT boot signature (0xAA55) and FAT signature strings
2581
2724
  * - SPIFFS: Checks for SPIFFS magic number (0x20140529)
2582
- *
2725
+ *
2583
2726
  * Falls back to SPIFFS if no filesystem is detected.
2584
2727
  */
2585
2728
  async function detectFilesystemType(offset, size) {
@@ -2587,34 +2730,33 @@ async function detectFilesystemType(offset, size) {
2587
2730
  // Read first 8KB or entire partition if smaller
2588
2731
  const readSize = Math.min(8192, size);
2589
2732
  const data = await espStub.readFlash(offset, readSize);
2590
-
2733
+
2591
2734
  if (data.length < 32) {
2592
- logMsg('Partition too small, assuming SPIFFS');
2593
- return 'spiffs';
2735
+ logMsg("Partition too small, assuming SPIFFS");
2736
+ return "spiffs";
2594
2737
  }
2595
-
2738
+
2596
2739
  // Get chip name for ESP8266-specific detection
2597
- const chipName = currentChipName || '';
2598
-
2740
+ const chipName = currentChipName || "";
2741
+
2599
2742
  // Use the detectFilesystemFromImage function from esptool package
2600
2743
  const esptoolMod = await window.esptoolPackage;
2601
2744
  const fsType = esptoolMod.detectFilesystemFromImage(data, chipName);
2602
-
2745
+
2603
2746
  // Convert FilesystemType enum to lowercase string
2604
2747
  const fsTypeStr = fsType.toLowerCase();
2605
-
2606
- if (fsTypeStr !== 'unknown') {
2748
+
2749
+ if (fsTypeStr !== "unknown") {
2607
2750
  logMsg(`Detected filesystem: ${fsTypeStr}`);
2608
2751
  return fsTypeStr;
2609
2752
  }
2610
-
2753
+
2611
2754
  // Default: If no clear signature found, assume SPIFFS
2612
- logMsg('No clear filesystem signature found, assuming SPIFFS');
2613
- return 'spiffs';
2614
-
2755
+ logMsg("No clear filesystem signature found, assuming SPIFFS");
2756
+ return "spiffs";
2615
2757
  } catch (err) {
2616
2758
  errorMsg(`Failed to detect filesystem type: ${err.message || err}`);
2617
- return 'spiffs'; // Safe fallback
2759
+ return "spiffs"; // Safe fallback
2618
2760
  }
2619
2761
  }
2620
2762
 
@@ -2626,14 +2768,13 @@ async function loadLittlefsModule() {
2626
2768
  // Derive base path from current document URL (works for all hosting layouts)
2627
2769
  const basePath = new URL(".", window.location.href).pathname;
2628
2770
  const modulePath = `${basePath}src/wasm/littlefs/index.js`;
2629
-
2630
- littlefsModulePromise = import(modulePath)
2631
- .catch(error => {
2632
- errorMsg('Failed to load LittleFS module from: ' + modulePath);
2633
- debugMsg('LittleFS module load error: ' + error);
2634
- littlefsModulePromise = null; // Reset on error so it can be retried
2635
- throw error;
2636
- });
2771
+
2772
+ littlefsModulePromise = import(modulePath).catch((error) => {
2773
+ errorMsg("Failed to load LittleFS module from: " + modulePath);
2774
+ debugMsg("LittleFS module load error: " + error);
2775
+ littlefsModulePromise = null; // Reset on error so it can be retried
2776
+ throw error;
2777
+ });
2637
2778
  }
2638
2779
  return littlefsModulePromise;
2639
2780
  }
@@ -2648,27 +2789,27 @@ function resetLittleFSState() {
2648
2789
  // Don't call destroy() - it can cause crashes
2649
2790
  // Just let garbage collection handle it
2650
2791
  } catch (e) {
2651
- debugMsg('Error cleaning up LittleFS: ' + e);
2792
+ debugMsg("Error cleaning up LittleFS: " + e);
2652
2793
  }
2653
2794
  }
2654
-
2795
+
2655
2796
  currentLittleFS = null;
2656
2797
  currentLittleFSPartition = null;
2657
- currentLittleFSPath = '/';
2798
+ currentLittleFSPath = "/";
2658
2799
  currentLittleFSBlockSize = 4096;
2659
-
2800
+
2660
2801
  // Hide UI - safely check if elements exist
2661
2802
  try {
2662
2803
  if (littlefsManager) {
2663
- littlefsManager.classList.add('hidden');
2804
+ littlefsManager.classList.add("hidden");
2664
2805
  }
2665
-
2806
+
2666
2807
  // Clear file list
2667
2808
  if (littlefsFileList) {
2668
- littlefsFileList.innerHTML = '';
2809
+ littlefsFileList.innerHTML = "";
2669
2810
  }
2670
2811
  } catch (e) {
2671
- debugMsg('Error resetting LittleFS UI: ' + e);
2812
+ debugMsg("Error resetting LittleFS UI: " + e);
2672
2813
  }
2673
2814
  }
2674
2815
 
@@ -2677,80 +2818,88 @@ function resetLittleFSState() {
2677
2818
  */
2678
2819
  async function openLittleFS(partition) {
2679
2820
  try {
2680
- logMsg(`Reading LittleFS partition "${partition.name}" (${formatSize(partition.size)})...`);
2681
-
2821
+ logMsg(
2822
+ `Reading LittleFS partition "${partition.name}" (${formatSize(partition.size)})...`,
2823
+ );
2824
+
2682
2825
  let data;
2683
-
2826
+
2684
2827
  // Check if data was already read (from Read Flash button)
2685
2828
  if (partition._readData) {
2686
2829
  data = partition._readData;
2687
- logMsg('Using already-read flash data');
2830
+ logMsg("Using already-read flash data");
2688
2831
  } else {
2689
2832
  // Read entire partition
2690
2833
  const partitionProgress = document.getElementById("partitionProgress");
2691
2834
  const progressBar = partitionProgress.querySelector("div");
2692
2835
  partitionProgress.classList.remove("hidden");
2693
-
2836
+
2694
2837
  data = await espStub.readFlash(
2695
2838
  partition.offset,
2696
2839
  partition.size,
2697
2840
  (packet, progress, totalSize) => {
2698
2841
  const percent = Math.floor((progress / totalSize) * 100);
2699
2842
  progressBar.style.width = percent + "%";
2700
- }
2843
+ },
2701
2844
  );
2702
-
2845
+
2703
2846
  partitionProgress.classList.add("hidden");
2704
2847
  progressBar.style.width = "0%";
2705
2848
  }
2706
-
2707
- logMsg('Mounting LittleFS filesystem...');
2708
-
2849
+
2850
+ logMsg("Mounting LittleFS filesystem...");
2851
+
2709
2852
  // Import constants from esptool module
2710
2853
  const basePath = new URL(".", window.location.href).pathname;
2711
2854
  const esptoolModulePath = `${basePath}js/modules/esptool.js`;
2712
- const {
2855
+ const {
2713
2856
  LITTLEFS_BLOCK_SIZE_CANDIDATES,
2714
- ESP8266_LITTLEFS_BLOCK_SIZE_CANDIDATES
2857
+ ESP8266_LITTLEFS_BLOCK_SIZE_CANDIDATES,
2715
2858
  } = await import(esptoolModulePath);
2716
-
2859
+
2717
2860
  // Get chip-specific block sizes using defined constants
2718
- const chipName = currentChipName || '';
2861
+ const chipName = currentChipName || "";
2719
2862
  const isESP8266 = chipName.toUpperCase().includes("ESP8266");
2720
- const blockSizes = isESP8266 ? ESP8266_LITTLEFS_BLOCK_SIZE_CANDIDATES : LITTLEFS_BLOCK_SIZE_CANDIDATES;
2721
-
2863
+ const blockSizes = isESP8266
2864
+ ? ESP8266_LITTLEFS_BLOCK_SIZE_CANDIDATES
2865
+ : LITTLEFS_BLOCK_SIZE_CANDIDATES;
2866
+
2722
2867
  let fs = null;
2723
2868
  let blockSize = 0;
2724
-
2869
+
2725
2870
  // Use cached module loader
2726
2871
  const module = await loadLittlefsModule();
2727
2872
  const { createLittleFSFromImage, formatDiskVersion } = module;
2728
-
2873
+
2729
2874
  for (const bs of blockSizes) {
2730
2875
  try {
2731
2876
  const blockCount = Math.floor(partition.size / bs);
2732
-
2877
+
2733
2878
  // ESP8266-specific parameters (from main.py)
2734
- const mountOptions = isESP8266 ? {
2735
- blockSize: bs,
2736
- blockCount: blockCount,
2737
- readSize: 64,
2738
- progSize: 64,
2739
- cacheSize: 64,
2740
- lookaheadSize: 64,
2741
- nameMax: 32,
2742
- blockCycles: 16,
2743
- } : {
2744
- blockSize: bs,
2745
- blockCount: blockCount,
2746
- };
2747
-
2879
+ const mountOptions = isESP8266
2880
+ ? {
2881
+ blockSize: bs,
2882
+ blockCount: blockCount,
2883
+ readSize: 64,
2884
+ progSize: 64,
2885
+ cacheSize: 64,
2886
+ lookaheadSize: 64,
2887
+ nameMax: 32,
2888
+ blockCycles: 16,
2889
+ }
2890
+ : {
2891
+ blockSize: bs,
2892
+ blockCount: blockCount,
2893
+ };
2894
+
2748
2895
  fs = await createLittleFSFromImage(data, mountOptions);
2749
-
2896
+
2750
2897
  // Try to list root to verify it works
2751
- fs.list('/');
2898
+ fs.list("/");
2752
2899
  blockSize = bs;
2753
- logMsg(`Successfully mounted LittleFS with block size ${bs}${isESP8266 ? ' (ESP8266 parameters)' : ''}`);
2900
+ logMsg(
2901
+ `Successfully mounted LittleFS with block size ${bs}${isESP8266 ? " (ESP8266 parameters)" : ""}`,
2902
+ );
2754
2903
  break;
2755
2904
  } catch (err) {
2756
2905
  // Try next block size
@@ -2758,44 +2907,44 @@ async function openLittleFS(partition) {
2758
2907
  fs = null;
2759
2908
  }
2760
2909
  }
2761
-
2910
+
2762
2911
  if (!fs) {
2763
- throw new Error('Failed to mount LittleFS with any block size');
2912
+ throw new Error("Failed to mount LittleFS with any block size");
2764
2913
  }
2765
-
2914
+
2766
2915
  // Store filesystem instance
2767
2916
  currentLittleFS = fs;
2768
2917
  currentLittleFSPartition = partition;
2769
- currentLittleFSPath = '/';
2918
+ currentLittleFSPath = "/";
2770
2919
  currentLittleFSBlockSize = blockSize;
2771
- currentFilesystemType = 'littlefs';
2772
-
2920
+ currentFilesystemType = "littlefs";
2921
+
2773
2922
  // Update UI
2774
2923
  littlefsPartitionName.textContent = partition.name;
2775
2924
  littlefsPartitionSize.textContent = formatSize(partition.size);
2776
-
2925
+
2777
2926
  // Get disk version
2778
2927
  try {
2779
2928
  const diskVer = fs.getDiskVersion();
2780
- const major = (diskVer >> 16) & 0xFFFF;
2781
- const minor = diskVer & 0xFFFF;
2929
+ const major = (diskVer >> 16) & 0xffff;
2930
+ const minor = diskVer & 0xffff;
2782
2931
  littlefsDiskVersion.textContent = `LittleFS v${major}.${minor}`;
2783
2932
  } catch (e) {
2784
- littlefsDiskVersion.textContent = '';
2933
+ littlefsDiskVersion.textContent = "";
2785
2934
  }
2786
-
2935
+
2787
2936
  // Show manager
2788
- littlefsManager.classList.remove('hidden');
2789
-
2937
+ littlefsManager.classList.remove("hidden");
2938
+
2790
2939
  // Enable all operations for LittleFS (including directories)
2791
2940
  butLittlefsUpload.disabled = false;
2792
2941
  butLittlefsMkdir.disabled = false;
2793
2942
  butLittlefsWrite.disabled = false;
2794
-
2943
+
2795
2944
  // Load files
2796
2945
  refreshLittleFS();
2797
-
2798
- logMsg('LittleFS filesystem opened successfully');
2946
+
2947
+ logMsg("LittleFS filesystem opened successfully");
2799
2948
  } catch (e) {
2800
2949
  errorMsg(`Failed to open LittleFS: ${e.message || e}`);
2801
2950
  // Don't call destroy() - just reset state
@@ -2808,42 +2957,46 @@ async function openLittleFS(partition) {
2808
2957
  */
2809
2958
  async function openFatFS(partition) {
2810
2959
  try {
2811
- logMsg(`Reading FatFS partition "${partition.name}" (${formatSize(partition.size)})...`);
2812
-
2960
+ logMsg(
2961
+ `Reading FatFS partition "${partition.name}" (${formatSize(partition.size)})...`,
2962
+ );
2963
+
2813
2964
  let data;
2814
-
2965
+
2815
2966
  // Check if data was already read (from Read Flash button)
2816
2967
  if (partition._readData) {
2817
2968
  data = partition._readData;
2818
- logMsg('Using already-read flash data');
2969
+ logMsg("Using already-read flash data");
2819
2970
  } else {
2820
2971
  // Read entire partition
2821
2972
  const partitionProgress = document.getElementById("partitionProgress");
2822
2973
  const progressBar = partitionProgress.querySelector("div");
2823
2974
  partitionProgress.classList.remove("hidden");
2824
-
2975
+
2825
2976
  data = await espStub.readFlash(
2826
2977
  partition.offset,
2827
2978
  partition.size,
2828
2979
  (packet, progress, totalSize) => {
2829
2980
  const percent = Math.floor((progress / totalSize) * 100);
2830
2981
  progressBar.style.width = percent + "%";
2831
- }
2982
+ },
2832
2983
  );
2833
-
2984
+
2834
2985
  partitionProgress.classList.add("hidden");
2835
2986
  progressBar.style.width = "0%";
2836
2987
  }
2837
-
2838
- logMsg('Mounting FatFS filesystem...');
2839
- logMsg(`Partition size: ${formatSize(partition.size)} (${partition.size} bytes)`);
2840
-
2988
+
2989
+ logMsg("Mounting FatFS filesystem...");
2990
+ logMsg(
2991
+ `Partition size: ${formatSize(partition.size)} (${partition.size} bytes)`,
2992
+ );
2993
+
2841
2994
  // Check if FAT filesystem starts at offset 0x1000 (common for ESP8266/ESP32)
2842
2995
  let fatOffset = 0;
2843
2996
  if (data.length >= 0x1000 + 512) {
2844
2997
  const bootSigAt0 = data[510] | (data[511] << 8);
2845
2998
  const bootSigAt0x1000 = data[0x1000 + 510] | (data[0x1000 + 511] << 8);
2846
-
2999
+
2847
3000
  // If boot signature is at 0x1000 but not at 0, use offset 0x1000
2848
3001
  if (bootSigAt0x1000 === 0xaa55 && bootSigAt0 !== 0xaa55) {
2849
3002
  fatOffset = 0x1000;
@@ -2852,38 +3005,40 @@ async function openFatFS(partition) {
2852
3005
  data = data.slice(fatOffset);
2853
3006
  }
2854
3007
  }
2855
-
3008
+
2856
3009
  // Load FatFS module
2857
3010
  const basePath = new URL(".", window.location.href).pathname;
2858
3011
  const modulePath = `${basePath}src/wasm/fatfs/index.js`;
2859
3012
  const module = await import(modulePath);
2860
3013
  const { createFatFSFromImage, createFatFS } = module;
2861
-
3014
+
2862
3015
  // Use 4096 block size (ESP32 standard)
2863
3016
  let blockSize = 4096;
2864
3017
  let blockCount = Math.max(1, Math.floor(data.length / blockSize));
2865
3018
  if (blockCount <= 0) {
2866
3019
  blockCount = 1;
2867
3020
  }
2868
-
3021
+
2869
3022
  let fs = null;
2870
-
3023
+
2871
3024
  // First try to mount existing FatFS from image
2872
3025
  try {
2873
- logMsg(`Trying to mount FatFS with block size ${blockSize} (${blockCount} blocks)...`);
2874
-
3026
+ logMsg(
3027
+ `Trying to mount FatFS with block size ${blockSize} (${blockCount} blocks)...`,
3028
+ );
3029
+
2875
3030
  fs = await createFatFSFromImage(data, {
2876
3031
  blockSize: blockSize,
2877
3032
  blockCount: blockCount,
2878
3033
  });
2879
-
3034
+
2880
3035
  logMsg(`FatFS instance created, attempting to list files...`);
2881
3036
  const files = fs.list();
2882
3037
  logMsg(`Successfully listed ${files.length} files/directories`);
2883
3038
  logMsg(`Successfully mounted FatFS`);
2884
3039
  } catch (err) {
2885
3040
  logMsg(`Failed to mount existing FatFS: ${err.message || err}`);
2886
-
3041
+
2887
3042
  // If mounting fails, create a new empty formatted filesystem
2888
3043
  // Note: This does NOT use the image data - it creates a blank filesystem
2889
3044
  if (createFatFS) {
@@ -2895,47 +3050,53 @@ async function openFatFS(partition) {
2895
3050
  formatOnInit: true,
2896
3051
  });
2897
3052
  logMsg(`Created new formatted FatFS`);
2898
- logMsg(`Partition appears blank/unformatted. You can format and save to initialize it.`);
3053
+ logMsg(
3054
+ `Partition appears blank/unformatted. You can format and save to initialize it.`,
3055
+ );
2899
3056
  } catch (createErr) {
2900
- logMsg(`Failed to create new FatFS: ${createErr.message || createErr}`);
3057
+ logMsg(
3058
+ `Failed to create new FatFS: ${createErr.message || createErr}`,
3059
+ );
2901
3060
  throw err; // Throw original error
2902
3061
  }
2903
3062
  } else {
2904
3063
  throw err;
2905
3064
  }
2906
3065
  }
2907
-
3066
+
2908
3067
  if (!fs) {
2909
- throw new Error('Failed to mount FatFS with any block size. The partition may not contain a valid FAT filesystem or may be corrupted.');
3068
+ throw new Error(
3069
+ "Failed to mount FatFS with any block size. The partition may not contain a valid FAT filesystem or may be corrupted.",
3070
+ );
2910
3071
  }
2911
-
3072
+
2912
3073
  // Store filesystem instance and block size
2913
3074
  currentLittleFS = fs;
2914
3075
  currentLittleFSPartition = partition;
2915
- currentLittleFSPath = '/';
3076
+ currentLittleFSPath = "/";
2916
3077
  currentLittleFSBlockSize = blockSize;
2917
- currentFilesystemType = 'fatfs';
2918
-
3078
+ currentFilesystemType = "fatfs";
3079
+
2919
3080
  // Update UI
2920
3081
  littlefsPartitionName.textContent = partition.name;
2921
3082
  littlefsPartitionSize.textContent = formatSize(partition.size);
2922
- littlefsDiskVersion.textContent = 'FAT';
2923
-
3083
+ littlefsDiskVersion.textContent = "FAT";
3084
+
2924
3085
  // Show manager
2925
- littlefsManager.classList.remove('hidden');
2926
-
3086
+ littlefsManager.classList.remove("hidden");
3087
+
2927
3088
  // Enable all operations for FatFS (including directories)
2928
3089
  butLittlefsUpload.disabled = false;
2929
3090
  butLittlefsMkdir.disabled = false;
2930
3091
  butLittlefsWrite.disabled = false;
2931
-
3092
+
2932
3093
  // Load files
2933
3094
  refreshLittleFS();
2934
-
2935
- logMsg('FatFS filesystem opened successfully');
3095
+
3096
+ logMsg("FatFS filesystem opened successfully");
2936
3097
  } catch (e) {
2937
3098
  errorMsg(`Failed to open FatFS: ${e.message || e}`);
2938
- debugMsg('FatFS open error details: ' + e);
3099
+ debugMsg("FatFS open error details: " + e);
2939
3100
  resetLittleFSState();
2940
3101
  }
2941
3102
  }
@@ -2945,57 +3106,65 @@ async function openFatFS(partition) {
2945
3106
  */
2946
3107
  async function openSPIFFS(partition) {
2947
3108
  try {
2948
- logMsg(`Reading SPIFFS partition "${partition.name}" (${formatSize(partition.size)})...`);
2949
-
3109
+ logMsg(
3110
+ `Reading SPIFFS partition "${partition.name}" (${formatSize(partition.size)})...`,
3111
+ );
3112
+
2950
3113
  let data;
2951
-
3114
+
2952
3115
  // Check if data was already read (from Read Flash button)
2953
3116
  if (partition._readData) {
2954
3117
  data = partition._readData;
2955
- logMsg('Using already-read flash data');
3118
+ logMsg("Using already-read flash data");
2956
3119
  } else {
2957
3120
  // Read entire partition
2958
3121
  const partitionProgress = document.getElementById("partitionProgress");
2959
3122
  const progressBar = partitionProgress.querySelector("div");
2960
3123
  partitionProgress.classList.remove("hidden");
2961
-
3124
+
2962
3125
  data = await espStub.readFlash(
2963
3126
  partition.offset,
2964
3127
  partition.size,
2965
3128
  (packet, progress, totalSize) => {
2966
3129
  const percent = Math.floor((progress / totalSize) * 100);
2967
3130
  progressBar.style.width = percent + "%";
2968
- }
3131
+ },
2969
3132
  );
2970
-
3133
+
2971
3134
  partitionProgress.classList.add("hidden");
2972
3135
  progressBar.style.width = "0%";
2973
3136
  }
2974
-
2975
- logMsg('Parsing SPIFFS filesystem...');
2976
- logMsg(`Partition size: ${formatSize(partition.size)} (${partition.size} bytes)`);
2977
-
3137
+
3138
+ logMsg("Parsing SPIFFS filesystem...");
3139
+ logMsg(
3140
+ `Partition size: ${formatSize(partition.size)} (${partition.size} bytes)`,
3141
+ );
3142
+
2978
3143
  // Import SPIFFS module
2979
3144
  const basePath = new URL(".", window.location.href).pathname;
2980
3145
  const modulePath = `${basePath}js/modules/esptool.js`;
2981
3146
 
2982
- const {
2983
- SpiffsFS,
2984
- SpiffsReader,
2985
- SpiffsBuildConfig,
3147
+ const {
3148
+ SpiffsFS,
3149
+ SpiffsReader,
3150
+ SpiffsBuildConfig,
2986
3151
  DEFAULT_SPIFFS_CONFIG,
2987
3152
  ESP8266_SPIFFS_PAGE_SIZE,
2988
- ESP8266_SPIFFS_BLOCK_SIZE
3153
+ ESP8266_SPIFFS_BLOCK_SIZE,
2989
3154
  } = await import(modulePath);
2990
-
3155
+
2991
3156
  // Get chip-specific parameters
2992
- const chipName = currentChipName || '';
3157
+ const chipName = currentChipName || "";
2993
3158
  const isESP8266 = chipName.toUpperCase().includes("ESP8266");
2994
-
3159
+
2995
3160
  // ESP8266 uses different SPIFFS parameters (from main.py)
2996
- const pageSize = isESP8266 ? ESP8266_SPIFFS_PAGE_SIZE : DEFAULT_SPIFFS_CONFIG.pageSize || 256;
2997
- const blockSize = isESP8266 ? ESP8266_SPIFFS_BLOCK_SIZE : DEFAULT_SPIFFS_CONFIG.blockSize || 4096;
2998
-
3161
+ const pageSize = isESP8266
3162
+ ? ESP8266_SPIFFS_PAGE_SIZE
3163
+ : DEFAULT_SPIFFS_CONFIG.pageSize || 256;
3164
+ const blockSize = isESP8266
3165
+ ? ESP8266_SPIFFS_BLOCK_SIZE
3166
+ : DEFAULT_SPIFFS_CONFIG.blockSize || 4096;
3167
+
2999
3168
  // Create build config with partition size and chip-specific parameters
3000
3169
  const config = new SpiffsBuildConfig({
3001
3170
  ...DEFAULT_SPIFFS_CONFIG,
@@ -3003,17 +3172,19 @@ async function openSPIFFS(partition) {
3003
3172
  pageSize: pageSize,
3004
3173
  blockSize: blockSize,
3005
3174
  });
3006
-
3007
- logMsg(`Using SPIFFS config: page_size=${pageSize}, block_size=${blockSize}${isESP8266 ? ' (ESP8266)' : ''}`);
3008
-
3175
+
3176
+ logMsg(
3177
+ `Using SPIFFS config: page_size=${pageSize}, block_size=${blockSize}${isESP8266 ? " (ESP8266)" : ""}`,
3178
+ );
3179
+
3009
3180
  // Create reader and parse existing files
3010
3181
  const reader = new SpiffsReader(data, config);
3011
3182
  reader.parse();
3012
-
3183
+
3013
3184
  // Get file list
3014
3185
  const files = reader.listFiles();
3015
3186
  logMsg(`Found ${files.length} files in SPIFFS`);
3016
-
3187
+
3017
3188
  // Create a wrapper object that mimics LittleFS interface with full read/write support
3018
3189
  const spiffsWrapper = {
3019
3190
  _reader: reader,
@@ -3022,102 +3193,108 @@ async function openSPIFFS(partition) {
3022
3193
  _config: config,
3023
3194
  _originalData: data, // Store original image data
3024
3195
  _modified: false,
3025
-
3026
- list: function(path = '/') {
3196
+
3197
+ list: function (path = "/") {
3027
3198
  // Normalize path
3028
- const normalizedPath = path === '/' ? '' : path.replace(/^\//, '').replace(/\/$/, '');
3029
-
3199
+ const normalizedPath =
3200
+ path === "/" ? "" : path.replace(/^\//, "").replace(/\/$/, "");
3201
+
3030
3202
  // Get all files with proper path property for UI compatibility
3031
- const allFiles = this._files.map(f => {
3032
- const fileName = f.name.startsWith('/') ? f.name.substring(1) : f.name;
3203
+ const allFiles = this._files.map((f) => {
3204
+ const fileName = f.name.startsWith("/")
3205
+ ? f.name.substring(1)
3206
+ : f.name;
3033
3207
  return {
3034
3208
  name: fileName,
3035
- path: '/' + fileName, // Add path property for UI
3036
- type: 'file',
3209
+ path: "/" + fileName, // Add path property for UI
3210
+ type: "file",
3037
3211
  size: f.size,
3038
- _data: f.data
3212
+ _data: f.data,
3039
3213
  };
3040
3214
  });
3041
-
3215
+
3042
3216
  // If root, return all files
3043
3217
  if (!normalizedPath) {
3044
3218
  return allFiles;
3045
3219
  }
3046
-
3220
+
3047
3221
  // Filter by path prefix
3048
- const prefix = normalizedPath + '/';
3049
- return allFiles.filter(f => f.name.startsWith(prefix));
3222
+ const prefix = normalizedPath + "/";
3223
+ return allFiles.filter((f) => f.name.startsWith(prefix));
3050
3224
  },
3051
-
3052
- read: function(path) {
3053
- const normalizedPath = path.startsWith('/') ? path.substring(1) : path;
3054
- const file = this._files.find(f => {
3055
- const fname = f.name.startsWith('/') ? f.name.substring(1) : f.name;
3225
+
3226
+ read: function (path) {
3227
+ const normalizedPath = path.startsWith("/") ? path.substring(1) : path;
3228
+ const file = this._files.find((f) => {
3229
+ const fname = f.name.startsWith("/") ? f.name.substring(1) : f.name;
3056
3230
  return fname === normalizedPath;
3057
3231
  });
3058
3232
  return file ? file.data : null;
3059
3233
  },
3060
-
3061
- readFile: function(path) {
3234
+
3235
+ readFile: function (path) {
3062
3236
  // Alias for read() to match LittleFS interface
3063
3237
  return this.read(path);
3064
3238
  },
3065
-
3066
- write: function(path, data) {
3239
+
3240
+ write: function (path, data) {
3067
3241
  // Determine the filename format used in original files
3068
3242
  // Check if original files have leading slash
3069
- const hasLeadingSlash = this._files.length > 0 && this._files[0].name.startsWith('/');
3070
-
3243
+ const hasLeadingSlash =
3244
+ this._files.length > 0 && this._files[0].name.startsWith("/");
3245
+
3071
3246
  // Normalize path for comparison
3072
- const normalizedPath = path.startsWith('/') ? path.substring(1) : path;
3073
-
3247
+ const normalizedPath = path.startsWith("/") ? path.substring(1) : path;
3248
+
3074
3249
  // Store filename in the same format as original files
3075
- const storedName = hasLeadingSlash ? '/' + normalizedPath : normalizedPath;
3076
-
3250
+ const storedName = hasLeadingSlash
3251
+ ? "/" + normalizedPath
3252
+ : normalizedPath;
3253
+
3077
3254
  // Check if file already exists
3078
- const existingIndex = this._files.findIndex(f => {
3079
- const fname = f.name.startsWith('/') ? f.name.substring(1) : f.name;
3255
+ const existingIndex = this._files.findIndex((f) => {
3256
+ const fname = f.name.startsWith("/") ? f.name.substring(1) : f.name;
3080
3257
  return fname === normalizedPath;
3081
3258
  });
3082
-
3259
+
3083
3260
  // Update or add file
3084
3261
  if (existingIndex >= 0) {
3085
3262
  this._files[existingIndex] = {
3086
3263
  name: storedName,
3087
3264
  size: data.length,
3088
- data: data
3265
+ data: data,
3089
3266
  };
3090
3267
  } else {
3091
3268
  this._files.push({
3092
3269
  name: storedName,
3093
3270
  size: data.length,
3094
- data: data
3271
+ data: data,
3095
3272
  });
3096
3273
  }
3097
-
3274
+
3098
3275
  this._modified = true;
3099
3276
  },
3100
-
3101
- writeFile: function(path, data) {
3277
+
3278
+ writeFile: function (path, data) {
3102
3279
  // Alias for write() to match LittleFS interface
3103
3280
  return this.write(path, data);
3104
3281
  },
3105
-
3106
- addFile: function(path, data) {
3282
+
3283
+ addFile: function (path, data) {
3107
3284
  // Alias for write() to match alternative interface
3108
3285
  return this.write(path, data);
3109
3286
  },
3110
-
3111
- remove: function(path) {
3287
+
3288
+ remove: function (path) {
3112
3289
  // Normalize path
3113
- const normalizedPath = path.startsWith('/') ? path.substring(1) : path;
3114
-
3290
+ const normalizedPath = path.startsWith("/") ? path.substring(1) : path;
3291
+
3115
3292
  // Find and remove file
3116
- const index = this._files.findIndex(f => {
3117
- const fname = f.name.startsWith('/') ? f.name.substring(1) : f.name;
3293
+ const index = this._files.findIndex((f) => {
3294
+ const fname = f.name.startsWith("/") ? f.name.substring(1) : f.name;
3118
3295
  return fname === normalizedPath;
3119
3296
  });
3120
-
3297
+
3121
3298
  if (index >= 0) {
3122
3299
  this._files.splice(index, 1);
3123
3300
  this._modified = true;
@@ -3125,77 +3302,81 @@ async function openSPIFFS(partition) {
3125
3302
  throw new Error(`File not found: ${path}`);
3126
3303
  }
3127
3304
  },
3128
-
3129
- deleteFile: function(path) {
3305
+
3306
+ deleteFile: function (path) {
3130
3307
  // Alias for remove() to match LittleFS interface
3131
3308
  return this.remove(path);
3132
3309
  },
3133
-
3134
- delete: function(path, options) {
3310
+
3311
+ delete: function (path, options) {
3135
3312
  // For compatibility with LittleFS delete method
3136
3313
  // SPIFFS doesn't have directories, so just delete the file
3137
3314
  return this.remove(path);
3138
3315
  },
3139
-
3140
- mkdir: function() {
3141
- throw new Error('SPIFFS does not support directories. Files are stored in a flat structure.');
3316
+
3317
+ mkdir: function () {
3318
+ throw new Error(
3319
+ "SPIFFS does not support directories. Files are stored in a flat structure.",
3320
+ );
3142
3321
  },
3143
-
3144
- toImage: function() {
3322
+
3323
+ toImage: function () {
3145
3324
  // If not modified, return original data
3146
3325
  if (!this._modified) {
3147
3326
  return this._originalData || new Uint8Array(this._partition.size);
3148
3327
  }
3149
-
3328
+
3150
3329
  // Create new SPIFFS filesystem with all files
3151
3330
  const fs = new SpiffsFS(this._partition.size, this._config);
3152
-
3331
+
3153
3332
  // Add all files - preserve original filename format
3154
3333
  for (const file of this._files) {
3155
3334
  // Use the filename exactly as stored in _files
3156
3335
  // This preserves whether it has a leading slash or not
3157
3336
  const fileName = file.name;
3158
-
3337
+
3159
3338
  // Log for debugging
3160
- console.log(`Adding file to SPIFFS: "${fileName}" (${file.data.length} bytes)`);
3161
-
3339
+ console.log(
3340
+ `Adding file to SPIFFS: "${fileName}" (${file.data.length} bytes)`,
3341
+ );
3342
+
3162
3343
  fs.createFile(fileName, file.data);
3163
3344
  }
3164
-
3345
+
3165
3346
  // Generate binary image
3166
3347
  const image = fs.toBinary();
3167
3348
  console.log(`Generated SPIFFS image: ${image.length} bytes`);
3168
3349
  return image;
3169
- }
3350
+ },
3170
3351
  };
3171
-
3352
+
3172
3353
  // Store filesystem instance
3173
3354
  currentLittleFS = spiffsWrapper;
3174
3355
  currentLittleFSPartition = partition;
3175
- currentLittleFSPath = '/';
3356
+ currentLittleFSPath = "/";
3176
3357
  currentLittleFSBlockSize = config.blockSize;
3177
- currentFilesystemType = 'spiffs';
3178
-
3358
+ currentFilesystemType = "spiffs";
3359
+
3179
3360
  // Update UI
3180
3361
  littlefsPartitionName.textContent = partition.name;
3181
3362
  littlefsPartitionSize.textContent = formatSize(partition.size);
3182
- littlefsDiskVersion.textContent = 'SPIFFS';
3183
-
3363
+ littlefsDiskVersion.textContent = "SPIFFS";
3364
+
3184
3365
  // Show manager
3185
- littlefsManager.classList.remove('hidden');
3186
-
3366
+ littlefsManager.classList.remove("hidden");
3367
+
3187
3368
  // Enable write operations for SPIFFS (but not mkdir since SPIFFS is flat)
3188
3369
  butLittlefsUpload.disabled = false;
3189
3370
  butLittlefsMkdir.disabled = true; // SPIFFS doesn't support directories
3190
3371
  butLittlefsWrite.disabled = false;
3191
-
3372
+
3192
3373
  // Load files
3193
3374
  refreshLittleFS();
3194
-
3195
- logMsg('SPIFFS filesystem opened successfully');
3375
+
3376
+ logMsg("SPIFFS filesystem opened successfully");
3196
3377
  } catch (e) {
3197
3378
  errorMsg(`Failed to open SPIFFS: ${e.message || e}`);
3198
- debugMsg('SPIFFS open error details: ' + e);
3379
+ debugMsg("SPIFFS open error details: " + e);
3199
3380
  resetLittleFSState();
3200
3381
  }
3201
3382
  }
@@ -3216,15 +3397,15 @@ function littlefsEstimateFileFootprint(size) {
3216
3397
  function littlefsEstimateUsage(entries) {
3217
3398
  const block = currentLittleFSBlockSize || 4096;
3218
3399
  let total = block * 2; // root metadata copies
3219
-
3400
+
3220
3401
  for (const entry of entries || []) {
3221
- if (entry.type === 'dir') {
3402
+ if (entry.type === "dir") {
3222
3403
  total += block;
3223
3404
  } else {
3224
3405
  total += littlefsEstimateFileFootprint(entry.size || 0);
3225
3406
  }
3226
3407
  }
3227
-
3408
+
3228
3409
  return total;
3229
3410
  }
3230
3411
 
@@ -3233,149 +3414,154 @@ function littlefsEstimateUsage(entries) {
3233
3414
  */
3234
3415
  function refreshLittleFS() {
3235
3416
  if (!currentLittleFS) return;
3236
-
3417
+
3237
3418
  try {
3238
3419
  // Calculate usage based on all files (like ESPConnect)
3239
- const allFiles = currentLittleFS.list('/');
3420
+ const allFiles = currentLittleFS.list("/");
3240
3421
  const usedBytes = littlefsEstimateUsage(allFiles);
3241
3422
  const totalBytes = currentLittleFSPartition.size;
3242
3423
  const usedPercent = Math.round((usedBytes / totalBytes) * 100);
3243
-
3244
- littlefsUsageBar.style.width = usedPercent + '%';
3424
+
3425
+ littlefsUsageBar.style.width = usedPercent + "%";
3245
3426
  littlefsUsageText.textContent = `Used: ${formatSize(usedBytes)} / ${formatSize(totalBytes)} (${usedPercent}%)`;
3246
-
3427
+
3247
3428
  // Update breadcrumb
3248
- littlefsBreadcrumb.textContent = currentLittleFSPath || '/';
3249
- butLittlefsUp.disabled = currentLittleFSPath === '/' || !currentLittleFSPath;
3250
-
3429
+ littlefsBreadcrumb.textContent = currentLittleFSPath || "/";
3430
+ butLittlefsUp.disabled =
3431
+ currentLittleFSPath === "/" || !currentLittleFSPath;
3432
+
3251
3433
  // List files - the list() function behavior differs between filesystems
3252
3434
  let entries;
3253
-
3254
- if (currentFilesystemType === 'fatfs') {
3435
+
3436
+ if (currentFilesystemType === "fatfs") {
3255
3437
  // FatFS returns ALL files recursively from root, so we always list from root and filter
3256
- const allEntries = currentLittleFS.list('/');
3257
- const isRoot = currentLittleFSPath === '/';
3258
-
3438
+ const allEntries = currentLittleFS.list("/");
3439
+ const isRoot = currentLittleFSPath === "/";
3440
+
3259
3441
  // Filter to show only direct children
3260
- entries = allEntries.filter(entry => {
3442
+ entries = allEntries.filter((entry) => {
3261
3443
  // Remove /fatfs prefix from entry path for comparison
3262
3444
  let entryPath = entry.path;
3263
- if (entryPath.startsWith('/fatfs/')) {
3445
+ if (entryPath.startsWith("/fatfs/")) {
3264
3446
  entryPath = entryPath.slice(6);
3265
- } else if (entryPath === '/fatfs') {
3266
- entryPath = '/';
3447
+ } else if (entryPath === "/fatfs") {
3448
+ entryPath = "/";
3267
3449
  }
3268
-
3450
+
3269
3451
  if (isRoot) {
3270
3452
  // In root: only show top-level entries
3271
3453
  const withoutLeadingSlash = entryPath.slice(1);
3272
- return withoutLeadingSlash && !withoutLeadingSlash.includes('/');
3454
+ return withoutLeadingSlash && !withoutLeadingSlash.includes("/");
3273
3455
  } else {
3274
3456
  // In subdirectory: entry must be direct child of current path
3275
- const expectedPrefix = currentLittleFSPath + '/';
3457
+ const expectedPrefix = currentLittleFSPath + "/";
3276
3458
  if (!entryPath.startsWith(expectedPrefix)) {
3277
3459
  return false;
3278
3460
  }
3279
3461
  const relativePath = entryPath.slice(expectedPrefix.length);
3280
- return relativePath && !relativePath.includes('/');
3462
+ return relativePath && !relativePath.includes("/");
3281
3463
  }
3282
3464
  });
3283
-
3465
+
3284
3466
  // Add name attribute for FatFS entries
3285
- entries = entries.map(entry => ({
3467
+ entries = entries.map((entry) => ({
3286
3468
  ...entry,
3287
- name: entry.path.split('/').pop() || entry.path
3469
+ name: entry.path.split("/").pop() || entry.path,
3288
3470
  }));
3289
3471
  } else {
3290
3472
  // LittleFS and SPIFFS return only direct children
3291
3473
  entries = currentLittleFS.list(currentLittleFSPath);
3292
3474
  }
3293
-
3475
+
3294
3476
  // Clear table
3295
- littlefsFileList.innerHTML = '';
3296
-
3477
+ littlefsFileList.innerHTML = "";
3478
+
3297
3479
  if (entries.length === 0) {
3298
- const row = document.createElement('tr');
3299
- row.innerHTML = '<td colspan="4" class="empty-state">No files in this directory</td>';
3480
+ const row = document.createElement("tr");
3481
+ row.innerHTML =
3482
+ '<td colspan="4" class="empty-state">No files in this directory</td>';
3300
3483
  littlefsFileList.appendChild(row);
3301
3484
  return;
3302
3485
  }
3303
-
3486
+
3304
3487
  // Sort: directories first, then files
3305
3488
  entries.sort((a, b) => {
3306
- if (a.type === 'dir' && b.type !== 'dir') return -1;
3307
- if (a.type !== 'dir' && b.type === 'dir') return 1;
3489
+ if (a.type === "dir" && b.type !== "dir") return -1;
3490
+ if (a.type !== "dir" && b.type === "dir") return 1;
3308
3491
  return a.path.localeCompare(b.path);
3309
3492
  });
3310
-
3493
+
3311
3494
  // Add rows
3312
- entries.forEach(entry => {
3313
- const row = document.createElement('tr');
3314
-
3495
+ entries.forEach((entry) => {
3496
+ const row = document.createElement("tr");
3497
+
3315
3498
  // Name
3316
- const nameCell = document.createElement('td');
3317
- nameCell.setAttribute('data-label', 'Name');
3318
- const nameDiv = document.createElement('div');
3319
- nameDiv.className = 'file-name' + (entry.type === 'dir' ? ' clickable' : '');
3320
-
3321
- const icon = document.createElement('span');
3322
- icon.className = 'file-icon';
3323
- icon.textContent = entry.type === 'dir' ? '📁' : '📄';
3324
-
3499
+ const nameCell = document.createElement("td");
3500
+ nameCell.setAttribute("data-label", "Name");
3501
+ const nameDiv = document.createElement("div");
3502
+ nameDiv.className =
3503
+ "file-name" + (entry.type === "dir" ? " clickable" : "");
3504
+
3505
+ const icon = document.createElement("span");
3506
+ icon.className = "file-icon";
3507
+ icon.textContent = entry.type === "dir" ? "📁" : "📄";
3508
+
3325
3509
  // Use entry.name instead of parsing the path
3326
- const name = entry.name || entry.path.split('/').filter(Boolean).pop() || '/';
3327
- const nameText = document.createElement('span');
3510
+ const name =
3511
+ entry.name || entry.path.split("/").filter(Boolean).pop() || "/";
3512
+ const nameText = document.createElement("span");
3328
3513
  nameText.textContent = name;
3329
-
3514
+
3330
3515
  nameDiv.appendChild(icon);
3331
3516
  nameDiv.appendChild(nameText);
3332
-
3333
- if (entry.type === 'dir') {
3517
+
3518
+ if (entry.type === "dir") {
3334
3519
  nameDiv.onclick = () => navigateLittleFS(entry.path);
3335
3520
  }
3336
-
3521
+
3337
3522
  nameCell.appendChild(nameDiv);
3338
3523
  row.appendChild(nameCell);
3339
-
3524
+
3340
3525
  // Type
3341
- const typeCell = document.createElement('td');
3342
- typeCell.setAttribute('data-label', 'Type');
3343
- typeCell.textContent = entry.type === 'dir' ? 'Directory' : 'File';
3526
+ const typeCell = document.createElement("td");
3527
+ typeCell.setAttribute("data-label", "Type");
3528
+ typeCell.textContent = entry.type === "dir" ? "Directory" : "File";
3344
3529
  row.appendChild(typeCell);
3345
-
3530
+
3346
3531
  // Size
3347
- const sizeCell = document.createElement('td');
3348
- sizeCell.setAttribute('data-label', 'Size');
3349
- sizeCell.textContent = entry.type === 'file' ? formatSize(entry.size) : '-';
3532
+ const sizeCell = document.createElement("td");
3533
+ sizeCell.setAttribute("data-label", "Size");
3534
+ sizeCell.textContent =
3535
+ entry.type === "file" ? formatSize(entry.size) : "-";
3350
3536
  row.appendChild(sizeCell);
3351
-
3537
+
3352
3538
  // Actions
3353
- const actionsCell = document.createElement('td');
3354
- actionsCell.setAttribute('data-label', 'Actions');
3355
- const actionsDiv = document.createElement('div');
3356
- actionsDiv.className = 'file-actions';
3357
-
3358
- if (entry.type === 'file') {
3359
- const downloadBtn = document.createElement('button');
3360
- downloadBtn.textContent = 'Download';
3539
+ const actionsCell = document.createElement("td");
3540
+ actionsCell.setAttribute("data-label", "Actions");
3541
+ const actionsDiv = document.createElement("div");
3542
+ actionsDiv.className = "file-actions";
3543
+
3544
+ if (entry.type === "file") {
3545
+ const downloadBtn = document.createElement("button");
3546
+ downloadBtn.textContent = "Download";
3361
3547
  downloadBtn.onclick = () => downloadLittleFSFile(entry.path);
3362
3548
  actionsDiv.appendChild(downloadBtn);
3363
-
3364
- const viewBtn = document.createElement('button');
3365
- viewBtn.textContent = 'View';
3549
+
3550
+ const viewBtn = document.createElement("button");
3551
+ viewBtn.textContent = "View";
3366
3552
  viewBtn.onclick = () => viewLittleFSFile(entry.path);
3367
3553
  actionsDiv.appendChild(viewBtn);
3368
3554
  }
3369
-
3370
- const deleteBtn = document.createElement('button');
3371
- deleteBtn.textContent = 'Delete';
3372
- deleteBtn.className = 'delete-btn';
3555
+
3556
+ const deleteBtn = document.createElement("button");
3557
+ deleteBtn.textContent = "Delete";
3558
+ deleteBtn.className = "delete-btn";
3373
3559
  deleteBtn.onclick = () => deleteLittleFSFile(entry.path, entry.type);
3374
3560
  actionsDiv.appendChild(deleteBtn);
3375
-
3561
+
3376
3562
  actionsCell.appendChild(actionsDiv);
3377
3563
  row.appendChild(actionsCell);
3378
-
3564
+
3379
3565
  littlefsFileList.appendChild(row);
3380
3566
  });
3381
3567
  } catch (e) {
@@ -3389,17 +3575,17 @@ function refreshLittleFS() {
3389
3575
  function navigateLittleFS(path) {
3390
3576
  // Remove /fatfs prefix if present (for FatFS compatibility)
3391
3577
  let normalizedPath = path;
3392
- if (normalizedPath.startsWith('/fatfs/')) {
3578
+ if (normalizedPath.startsWith("/fatfs/")) {
3393
3579
  normalizedPath = normalizedPath.slice(6); // Remove '/fatfs' keeping the /
3394
- } else if (normalizedPath === '/fatfs') {
3395
- normalizedPath = '/';
3580
+ } else if (normalizedPath === "/fatfs") {
3581
+ normalizedPath = "/";
3396
3582
  }
3397
-
3583
+
3398
3584
  // Remove trailing slash except for root
3399
- if (normalizedPath !== '/' && normalizedPath.endsWith('/')) {
3585
+ if (normalizedPath !== "/" && normalizedPath.endsWith("/")) {
3400
3586
  normalizedPath = normalizedPath.slice(0, -1);
3401
3587
  }
3402
-
3588
+
3403
3589
  currentLittleFSPath = normalizedPath;
3404
3590
  refreshLittleFS();
3405
3591
  }
@@ -3408,14 +3594,14 @@ function navigateLittleFS(path) {
3408
3594
  * Navigate up one directory
3409
3595
  */
3410
3596
  function clickLittlefsUp() {
3411
- if (currentLittleFSPath === '/' || !currentLittleFSPath) return;
3412
-
3597
+ if (currentLittleFSPath === "/" || !currentLittleFSPath) return;
3598
+
3413
3599
  // Split path and remove last segment
3414
- const parts = currentLittleFSPath.split('/').filter(Boolean);
3600
+ const parts = currentLittleFSPath.split("/").filter(Boolean);
3415
3601
  parts.pop();
3416
-
3602
+
3417
3603
  // Reconstruct path
3418
- currentLittleFSPath = parts.length ? '/' + parts.join('/') : '/';
3604
+ currentLittleFSPath = parts.length ? "/" + parts.join("/") : "/";
3419
3605
  refreshLittleFS();
3420
3606
  }
3421
3607
 
@@ -3432,21 +3618,25 @@ function clickLittlefsRefresh() {
3432
3618
  */
3433
3619
  async function clickLittlefsBackup() {
3434
3620
  if (!currentLittleFS || !currentLittleFSPartition) return;
3435
-
3621
+
3436
3622
  try {
3437
3623
  logMsg(`Creating ${getFilesystemDisplayName()} backup image...`);
3438
3624
  const image = currentLittleFS.toImage();
3439
-
3625
+
3440
3626
  // Create filename with chip type and MAC address
3441
- const chipInfo = currentChipName ? currentChipName.replace(/\s+/g, '_') : 'ESP';
3442
- const macInfo = currentMacAddr ? currentMacAddr.replace(/:/g, '') : '';
3443
- const fsType = currentFilesystemType || 'filesystem';
3444
- const filename = `${chipInfo}${macInfo ? '_' + macInfo : ''}_${currentLittleFSPartition.name}_${fsType}_backup.bin`;
3627
+ const chipInfo = currentChipName
3628
+ ? currentChipName.replace(/\s+/g, "_")
3629
+ : "ESP";
3630
+ const macInfo = currentMacAddr ? currentMacAddr.replace(/:/g, "") : "";
3631
+ const fsType = currentFilesystemType || "filesystem";
3632
+ const filename = `${chipInfo}${macInfo ? "_" + macInfo : ""}_${currentLittleFSPartition.name}_${fsType}_backup.bin`;
3445
3633
  await saveDataToFile(image, filename);
3446
-
3634
+
3447
3635
  logMsg(`${getFilesystemDisplayName()} backup saved as "${filename}"`);
3448
3636
  } catch (e) {
3449
- errorMsg(`Failed to backup ${getFilesystemDisplayName()}: ${e.message || e}`);
3637
+ errorMsg(
3638
+ `Failed to backup ${getFilesystemDisplayName()}: ${e.message || e}`,
3639
+ );
3450
3640
  }
3451
3641
  }
3452
3642
 
@@ -3455,27 +3645,29 @@ async function clickLittlefsBackup() {
3455
3645
  */
3456
3646
  async function clickLittlefsWrite() {
3457
3647
  if (!currentLittleFS || !currentLittleFSPartition) return;
3458
-
3648
+
3459
3649
  const confirmed = confirm(
3460
3650
  `Write modified filesystem to flash?\n\n` +
3461
- `Partition: ${currentLittleFSPartition.name}\n` +
3462
- `Offset: 0x${currentLittleFSPartition.offset.toString(16)}\n` +
3463
- `Size: ${formatSize(currentLittleFSPartition.size)}\n\n` +
3464
- `This will overwrite the current filesystem on the device!`
3651
+ `Partition: ${currentLittleFSPartition.name}\n` +
3652
+ `Offset: 0x${currentLittleFSPartition.offset.toString(16)}\n` +
3653
+ `Size: ${formatSize(currentLittleFSPartition.size)}\n\n` +
3654
+ `This will overwrite the current filesystem on the device!`,
3465
3655
  );
3466
-
3656
+
3467
3657
  if (!confirmed) return;
3468
-
3658
+
3469
3659
  try {
3470
3660
  logMsg(`Creating ${getFilesystemDisplayName()} image...`);
3471
3661
  const image = currentLittleFS.toImage();
3472
3662
  logMsg(`Image created: ${formatSize(image.length)}`);
3473
-
3663
+
3474
3664
  if (image.length > currentLittleFSPartition.size) {
3475
- errorMsg(`Image size (${formatSize(image.length)}) exceeds partition size (${formatSize(currentLittleFSPartition.size)})`);
3665
+ errorMsg(
3666
+ `Image size (${formatSize(image.length)}) exceeds partition size (${formatSize(currentLittleFSPartition.size)})`,
3667
+ );
3476
3668
  return;
3477
3669
  }
3478
-
3670
+
3479
3671
  // Disable buttons during write
3480
3672
  butLittlefsRefresh.disabled = true;
3481
3673
  butLittlefsBackup.disabled = true;
@@ -3483,19 +3675,24 @@ async function clickLittlefsWrite() {
3483
3675
  butLittlefsClose.disabled = true;
3484
3676
  butLittlefsUpload.disabled = true;
3485
3677
  butLittlefsMkdir.disabled = true;
3486
-
3487
- logMsg(`Writing ${formatSize(image.length)} to partition "${currentLittleFSPartition.name}" at 0x${currentLittleFSPartition.offset.toString(16)}...`);
3488
-
3678
+
3679
+ logMsg(
3680
+ `Writing ${formatSize(image.length)} to partition "${currentLittleFSPartition.name}" at 0x${currentLittleFSPartition.offset.toString(16)}...`,
3681
+ );
3682
+
3489
3683
  // Use the LittleFS usage bar as progress indicator
3490
3684
  const usageBar = document.getElementById("littlefsUsageBar");
3491
3685
  const usageText = document.getElementById("littlefsUsageText");
3492
3686
  const originalUsageBarWidth = usageBar.style.width;
3493
3687
  const originalUsageText = usageText.textContent;
3494
-
3688
+
3495
3689
  // Convert Uint8Array to ArrayBuffer (CRITICAL: flashData expects ArrayBuffer, not Uint8Array)
3496
3690
  // This matches the ESPConnect implementation
3497
- const imageBuffer = image.buffer.slice(image.byteOffset, image.byteOffset + image.byteLength);
3498
-
3691
+ const imageBuffer = image.buffer.slice(
3692
+ image.byteOffset,
3693
+ image.byteOffset + image.byteLength,
3694
+ );
3695
+
3499
3696
  // Write the image to flash with progress indication
3500
3697
  await espStub.flashData(
3501
3698
  imageBuffer,
@@ -3504,18 +3701,19 @@ async function clickLittlefsWrite() {
3504
3701
  usageBar.style.width = percent + "%";
3505
3702
  usageText.textContent = `Writing: ${formatSize(bytesWritten)} / ${formatSize(totalBytes)} (${percent}%)`;
3506
3703
  },
3507
- currentLittleFSPartition.offset
3704
+ currentLittleFSPartition.offset,
3508
3705
  );
3509
-
3706
+
3510
3707
  // Restore original usage display
3511
3708
  usageBar.style.width = originalUsageBarWidth;
3512
3709
  usageText.textContent = originalUsageText;
3513
-
3710
+
3514
3711
  logMsg(`${getFilesystemDisplayName()} successfully written to flash!`);
3515
3712
  logMsg(`To use the new filesystem, reset your device.`);
3516
-
3517
3713
  } catch (e) {
3518
- errorMsg(`Failed to write ${getFilesystemDisplayName()} to flash: ${e.message || e}`);
3714
+ errorMsg(
3715
+ `Failed to write ${getFilesystemDisplayName()} to flash: ${e.message || e}`,
3716
+ );
3519
3717
  } finally {
3520
3718
  // Re-enable buttons
3521
3719
  butLittlefsRefresh.disabled = false;
@@ -3524,7 +3722,7 @@ async function clickLittlefsWrite() {
3524
3722
  butLittlefsClose.disabled = false;
3525
3723
  butLittlefsUpload.disabled = !littlefsFileInput.files.length;
3526
3724
  // Re-enable mkdir only if not SPIFFS
3527
- butLittlefsMkdir.disabled = (currentFilesystemType === 'spiffs');
3725
+ butLittlefsMkdir.disabled = currentFilesystemType === "spiffs";
3528
3726
  }
3529
3727
  }
3530
3728
 
@@ -3532,12 +3730,12 @@ async function clickLittlefsWrite() {
3532
3730
  * Close LittleFS manager
3533
3731
  */
3534
3732
  function clickLittlefsClose() {
3535
- const fsName = getFilesystemDisplayName() || 'Filesystem';
3536
-
3733
+ const fsName = getFilesystemDisplayName() || "Filesystem";
3734
+
3537
3735
  if (currentLittleFS) {
3538
3736
  try {
3539
3737
  // Only call destroy if it exists (LittleFS has it, FatFS/SPIFFS don't)
3540
- if (typeof currentLittleFS.destroy === 'function') {
3738
+ if (typeof currentLittleFS.destroy === "function") {
3541
3739
  currentLittleFS.destroy();
3542
3740
  }
3543
3741
  } catch (e) {
@@ -3545,11 +3743,11 @@ function clickLittlefsClose() {
3545
3743
  }
3546
3744
  currentLittleFS = null;
3547
3745
  }
3548
-
3746
+
3549
3747
  currentLittleFSPartition = null;
3550
- currentLittleFSPath = '/';
3748
+ currentLittleFSPath = "/";
3551
3749
  currentFilesystemType = null;
3552
- littlefsManager.classList.add('hidden');
3750
+ littlefsManager.classList.add("hidden");
3553
3751
  logMsg(`${fsName} manager closed`);
3554
3752
  }
3555
3753
 
@@ -3558,24 +3756,24 @@ function clickLittlefsClose() {
3558
3756
  */
3559
3757
  async function clickLittlefsUpload() {
3560
3758
  if (!currentLittleFS || !littlefsFileInput.files.length) return;
3561
-
3759
+
3562
3760
  const file = littlefsFileInput.files[0];
3563
-
3761
+
3564
3762
  try {
3565
3763
  logMsg(`Uploading file "${file.name}"...`);
3566
-
3764
+
3567
3765
  const data = await file.arrayBuffer();
3568
3766
  const uint8Data = new Uint8Array(data);
3569
-
3767
+
3570
3768
  // Construct target path
3571
3769
  let targetPath = currentLittleFSPath;
3572
- if (!targetPath.endsWith('/')) targetPath += '/';
3770
+ if (!targetPath.endsWith("/")) targetPath += "/";
3573
3771
  targetPath += file.name;
3574
-
3772
+
3575
3773
  // Ensure parent directories exist
3576
- const segments = targetPath.split('/').filter(Boolean);
3774
+ const segments = targetPath.split("/").filter(Boolean);
3577
3775
  if (segments.length > 1) {
3578
- let built = '';
3776
+ let built = "";
3579
3777
  for (let i = 0; i < segments.length - 1; i++) {
3580
3778
  built += `/${segments[i]}`;
3581
3779
  try {
@@ -3585,25 +3783,25 @@ async function clickLittlefsUpload() {
3585
3783
  }
3586
3784
  }
3587
3785
  }
3588
-
3786
+
3589
3787
  // Write file to LittleFS - EXACTLY like ESPConnect
3590
- if (typeof currentLittleFS.writeFile === 'function') {
3788
+ if (typeof currentLittleFS.writeFile === "function") {
3591
3789
  currentLittleFS.writeFile(targetPath, uint8Data);
3592
- } else if (typeof currentLittleFS.addFile === 'function') {
3790
+ } else if (typeof currentLittleFS.addFile === "function") {
3593
3791
  currentLittleFS.addFile(targetPath, uint8Data);
3594
3792
  }
3595
-
3793
+
3596
3794
  // Verify by reading back
3597
3795
  const readBack = currentLittleFS.readFile(targetPath);
3598
3796
  logMsg(`File written: ${readBack.length} bytes at ${targetPath}`);
3599
-
3797
+
3600
3798
  // Clear input
3601
- littlefsFileInput.value = '';
3799
+ littlefsFileInput.value = "";
3602
3800
  butLittlefsUpload.disabled = true;
3603
-
3801
+
3604
3802
  // Refresh list
3605
3803
  refreshLittleFS();
3606
-
3804
+
3607
3805
  logMsg(`File "${file.name}" uploaded successfully`);
3608
3806
  } catch (e) {
3609
3807
  errorMsg(`Failed to upload file: ${e.message || e}`);
@@ -3615,30 +3813,32 @@ async function clickLittlefsUpload() {
3615
3813
  */
3616
3814
  async function clickLittlefsMkdir() {
3617
3815
  if (!currentLittleFS) return;
3618
-
3816
+
3619
3817
  // Check if mkdir is supported (SPIFFS doesn't support directories)
3620
- if (currentFilesystemType === 'spiffs') {
3621
- errorMsg('SPIFFS does not support directories. Files are stored in a flat structure.');
3818
+ if (currentFilesystemType === "spiffs") {
3819
+ errorMsg(
3820
+ "SPIFFS does not support directories. Files are stored in a flat structure.",
3821
+ );
3622
3822
  return;
3623
3823
  }
3624
-
3824
+
3625
3825
  let dirName;
3626
3826
  if (isElectron) {
3627
- dirName = await window.electronAPI.showPrompt('Enter directory name:');
3827
+ dirName = await window.electronAPI.showPrompt("Enter directory name:");
3628
3828
  } else {
3629
- dirName = prompt('Enter directory name:');
3829
+ dirName = prompt("Enter directory name:");
3630
3830
  }
3631
-
3831
+
3632
3832
  if (!dirName || !dirName.trim()) return;
3633
-
3833
+
3634
3834
  try {
3635
3835
  let targetPath = currentLittleFSPath;
3636
- if (!targetPath.endsWith('/')) targetPath += '/';
3836
+ if (!targetPath.endsWith("/")) targetPath += "/";
3637
3837
  targetPath += dirName.trim();
3638
-
3838
+
3639
3839
  currentLittleFS.mkdir(targetPath);
3640
3840
  refreshLittleFS();
3641
-
3841
+
3642
3842
  logMsg(`Directory "${dirName}" created successfully`);
3643
3843
  } catch (e) {
3644
3844
  errorMsg(`Failed to create directory: ${e.message || e}`);
@@ -3650,15 +3850,15 @@ async function clickLittlefsMkdir() {
3650
3850
  */
3651
3851
  async function downloadLittleFSFile(path) {
3652
3852
  if (!currentLittleFS) return;
3653
-
3853
+
3654
3854
  try {
3655
3855
  logMsg(`Downloading file "${path}"...`);
3656
-
3856
+
3657
3857
  const data = currentLittleFS.readFile(path);
3658
- const filename = path.split('/').filter(Boolean).pop() || 'file.bin';
3659
-
3858
+ const filename = path.split("/").filter(Boolean).pop() || "file.bin";
3859
+
3660
3860
  await saveDataToFile(data, filename);
3661
-
3861
+
3662
3862
  logMsg(`File "${filename}" downloaded successfully`);
3663
3863
  } catch (e) {
3664
3864
  errorMsg(`Failed to download file: ${e.message || e}`);
@@ -3670,21 +3870,23 @@ async function downloadLittleFSFile(path) {
3670
3870
  */
3671
3871
  function deleteLittleFSFile(path, type) {
3672
3872
  if (!currentLittleFS) return;
3673
-
3674
- const name = path.split('/').filter(Boolean).pop() || path;
3873
+
3874
+ const name = path.split("/").filter(Boolean).pop() || path;
3675
3875
  const confirmed = confirm(`Delete ${type} "${name}"?`);
3676
-
3876
+
3677
3877
  if (!confirmed) return;
3678
-
3878
+
3679
3879
  try {
3680
- if (type === 'dir') {
3880
+ if (type === "dir") {
3681
3881
  currentLittleFS.delete(path, { recursive: true });
3682
3882
  } else {
3683
3883
  currentLittleFS.deleteFile(path);
3684
3884
  }
3685
-
3885
+
3686
3886
  refreshLittleFS();
3687
- logMsg(`${type === 'dir' ? 'Directory' : 'File'} "${name}" deleted successfully`);
3887
+ logMsg(
3888
+ `${type === "dir" ? "Directory" : "File"} "${name}" deleted successfully`,
3889
+ );
3688
3890
  } catch (e) {
3689
3891
  errorMsg(`Failed to delete ${type}: ${e.message || e}`);
3690
3892
  }
@@ -3695,28 +3897,28 @@ function deleteLittleFSFile(path, type) {
3695
3897
  */
3696
3898
  async function viewLittleFSFile(path) {
3697
3899
  if (!currentLittleFS) return;
3698
-
3900
+
3699
3901
  try {
3700
3902
  logMsg(`Loading file "${path}"...`);
3701
-
3903
+
3702
3904
  const data = currentLittleFS.readFile(path);
3703
- const filename = path.split('/').filter(Boolean).pop() || 'file';
3704
-
3905
+ const filename = path.split("/").filter(Boolean).pop() || "file";
3906
+
3705
3907
  // Store current file data
3706
3908
  currentViewedFile = path;
3707
3909
  currentViewedFileData = data;
3708
-
3910
+
3709
3911
  // Update modal info
3710
3912
  fileViewerTitle.textContent = filename;
3711
3913
  fileViewerPath.textContent = path;
3712
3914
  fileViewerSize.textContent = formatSize(data.length);
3713
-
3915
+
3714
3916
  // Show text view by default
3715
- switchViewerTab('text');
3716
-
3917
+ switchViewerTab("text");
3918
+
3717
3919
  // Show modal
3718
- fileViewerModal.classList.remove('hidden');
3719
-
3920
+ fileViewerModal.classList.remove("hidden");
3921
+
3720
3922
  logMsg(`File "${filename}" loaded successfully`);
3721
3923
  } catch (e) {
3722
3924
  errorMsg(`Failed to view file: ${e.message || e}`);
@@ -3727,7 +3929,7 @@ async function viewLittleFSFile(path) {
3727
3929
  * Close file viewer modal
3728
3930
  */
3729
3931
  function closeFileViewer() {
3730
- fileViewerModal.classList.add('hidden');
3932
+ fileViewerModal.classList.add("hidden");
3731
3933
  currentViewedFile = null;
3732
3934
  currentViewedFileData = null;
3733
3935
  }
@@ -3737,8 +3939,9 @@ function closeFileViewer() {
3737
3939
  */
3738
3940
  async function downloadFromViewer() {
3739
3941
  if (!currentViewedFile || !currentViewedFileData) return;
3740
-
3741
- const filename = currentViewedFile.split('/').filter(Boolean).pop() || 'file.bin';
3942
+
3943
+ const filename =
3944
+ currentViewedFile.split("/").filter(Boolean).pop() || "file.bin";
3742
3945
  await saveDataToFile(currentViewedFileData, filename);
3743
3946
  logMsg(`File "${filename}" downloaded from viewer`);
3744
3947
  }
@@ -3748,15 +3951,15 @@ async function downloadFromViewer() {
3748
3951
  */
3749
3952
  function switchViewerTab(mode) {
3750
3953
  if (!currentViewedFileData) return;
3751
-
3954
+
3752
3955
  // Update tab buttons
3753
- if (mode === 'text') {
3754
- tabText.classList.add('active');
3755
- tabHex.classList.remove('active');
3956
+ if (mode === "text") {
3957
+ tabText.classList.add("active");
3958
+ tabHex.classList.remove("active");
3756
3959
  displayTextView(currentViewedFileData);
3757
3960
  } else {
3758
- tabHex.classList.add('active');
3759
- tabText.classList.remove('active');
3961
+ tabHex.classList.add("active");
3962
+ tabText.classList.remove("active");
3760
3963
  displayHexView(currentViewedFileData);
3761
3964
  }
3762
3965
  }
@@ -3767,13 +3970,13 @@ function switchViewerTab(mode) {
3767
3970
  function displayTextView(data) {
3768
3971
  try {
3769
3972
  // Try to decode as UTF-8
3770
- const decoder = new TextDecoder('utf-8', { fatal: false });
3973
+ const decoder = new TextDecoder("utf-8", { fatal: false });
3771
3974
  const text = decoder.decode(data);
3772
-
3975
+
3773
3976
  fileViewerText.textContent = text;
3774
- fileViewerText.className = '';
3977
+ fileViewerText.className = "";
3775
3978
  } catch (e) {
3776
- fileViewerText.textContent = 'Unable to display as text. Try Hex view.';
3979
+ fileViewerText.textContent = "Unable to display as text. Try Hex view.";
3777
3980
  }
3778
3981
  }
3779
3982
 
@@ -3783,52 +3986,52 @@ function displayTextView(data) {
3783
3986
  function displayHexView(data) {
3784
3987
  const lines = [];
3785
3988
  const bytesPerLine = 16;
3786
-
3989
+
3787
3990
  for (let i = 0; i < data.length; i += bytesPerLine) {
3788
- const offset = i.toString(16).padStart(8, '0').toUpperCase();
3789
-
3991
+ const offset = i.toString(16).padStart(8, "0").toUpperCase();
3992
+
3790
3993
  // Hex bytes
3791
3994
  const hexBytes = [];
3792
3995
  const asciiChars = [];
3793
-
3996
+
3794
3997
  for (let j = 0; j < bytesPerLine; j++) {
3795
3998
  if (i + j < data.length) {
3796
3999
  const byte = data[i + j];
3797
- hexBytes.push(byte.toString(16).padStart(2, '0').toUpperCase());
3798
-
4000
+ hexBytes.push(byte.toString(16).padStart(2, "0").toUpperCase());
4001
+
3799
4002
  // ASCII representation (printable characters only)
3800
4003
  if (byte >= 32 && byte <= 126) {
3801
4004
  asciiChars.push(String.fromCharCode(byte));
3802
4005
  } else {
3803
- asciiChars.push('.');
4006
+ asciiChars.push(".");
3804
4007
  }
3805
4008
  } else {
3806
- hexBytes.push(' ');
3807
- asciiChars.push(' ');
4009
+ hexBytes.push(" ");
4010
+ asciiChars.push(" ");
3808
4011
  }
3809
4012
  }
3810
-
4013
+
3811
4014
  // Format: offset | hex bytes (grouped by 8) | ascii
3812
- const hexPart1 = hexBytes.slice(0, 8).join(' ');
3813
- const hexPart2 = hexBytes.slice(8, 16).join(' ');
3814
- const hexPart = hexPart1 + ' ' + hexPart2;
3815
- const asciiPart = asciiChars.join('');
3816
-
4015
+ const hexPart1 = hexBytes.slice(0, 8).join(" ");
4016
+ const hexPart2 = hexBytes.slice(8, 16).join(" ");
4017
+ const hexPart = hexPart1 + " " + hexPart2;
4018
+ const asciiPart = asciiChars.join("");
4019
+
3817
4020
  lines.push(
3818
4021
  `<div class="hex-line">` +
3819
- `<span class="hex-offset">${offset}</span>` +
3820
- `<span class="hex-bytes">${hexPart}</span>` +
3821
- `<span class="hex-ascii">${asciiPart}</span>` +
3822
- `</div>`
4022
+ `<span class="hex-offset">${offset}</span>` +
4023
+ `<span class="hex-bytes">${hexPart}</span>` +
4024
+ `<span class="hex-ascii">${asciiPart}</span>` +
4025
+ `</div>`,
3823
4026
  );
3824
4027
  }
3825
-
3826
- fileViewerText.innerHTML = `<div class="hex-view">${lines.join('')}</div>`;
3827
- fileViewerText.className = 'hex-view';
4028
+
4029
+ fileViewerText.innerHTML = `<div class="hex-view">${lines.join("")}</div>`;
4030
+ fileViewerText.className = "hex-view";
3828
4031
  }
3829
4032
 
3830
4033
  // Close modal when clicking outside
3831
- fileViewerModal.addEventListener('click', (e) => {
4034
+ fileViewerModal.addEventListener("click", (e) => {
3832
4035
  if (e.target === fileViewerModal) {
3833
4036
  closeFileViewer();
3834
4037
  }