esp32tool 1.6.5 → 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/apple-touch-icon.png +0 -0
- package/css/style.css +325 -25
- package/dist/util/console-color.d.ts +5 -1
- package/dist/util/console-color.js +293 -15
- package/dist/util/timestamp-transformer.js +29 -8
- package/electron/cli-main.cjs +19 -19
- package/electron/main.cjs +167 -148
- package/electron/preload.js +16 -18
- package/icons/icon-128.png +0 -0
- package/icons/icon-144.png +0 -0
- package/icons/icon-152.png +0 -0
- package/icons/icon-192.png +0 -0
- package/icons/icon-384.png +0 -0
- package/icons/icon-512.png +0 -0
- package/icons/icon-72.png +0 -0
- package/icons/icon-96.png +0 -0
- package/js/console.js +21 -12
- package/js/hex-editor.js +216 -163
- package/js/improv.js +59 -21
- package/js/nvs-editor.js +1189 -182
- package/js/script.js +1048 -845
- package/js/util/console-color.js +293 -15
- package/js/util/timestamp-transformer.js +29 -8
- package/js/webusb-serial.js +1075 -950
- package/package.cli.json +2 -2
- package/package.json +15 -16
- package/screenshots/desktop.png +0 -0
- package/screenshots/mobile.png +0 -0
- package/src/util/console-color.ts +290 -15
- package/src/util/timestamp-transformer.ts +32 -8
- package/sw.js +1 -1
package/js/script.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Import WebUSB serial support for Android compatibility
|
|
2
|
-
import { WebUSBSerial, requestSerialPort } from
|
|
3
|
-
import { ESP32ToolConsole } from
|
|
4
|
-
import { HexEditor } from
|
|
5
|
-
import { NVSEditor } from
|
|
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
|
|
50
|
+
if (!currentFilesystemType) return "Filesystem";
|
|
51
51
|
switch (currentFilesystemType) {
|
|
52
|
-
case
|
|
53
|
-
|
|
54
|
-
case
|
|
55
|
-
|
|
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 ===
|
|
71
|
+
if (typeof currentLittleFS.destroy === "function") {
|
|
68
72
|
currentLittleFS.destroy();
|
|
69
73
|
}
|
|
70
74
|
} catch (e) {
|
|
71
|
-
debugMsg(
|
|
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(
|
|
87
|
-
|
|
90
|
+
littlefsManager.classList.add("hidden");
|
|
91
|
+
|
|
88
92
|
// Clear partition list
|
|
89
|
-
partitionList.innerHTML =
|
|
90
|
-
partitionList.classList.add(
|
|
91
|
-
|
|
93
|
+
partitionList.innerHTML = "";
|
|
94
|
+
partitionList.classList.add("hidden");
|
|
95
|
+
|
|
92
96
|
// Show the Read Partition Table button again
|
|
93
|
-
butReadPartitions.classList.remove(
|
|
94
|
-
|
|
97
|
+
butReadPartitions.classList.remove("hidden");
|
|
98
|
+
|
|
95
99
|
// Close NVS editor if open
|
|
96
100
|
if (nvsEditorInstance) {
|
|
97
|
-
try {
|
|
101
|
+
try {
|
|
102
|
+
nvsEditorInstance.close();
|
|
103
|
+
} catch (_) {}
|
|
98
104
|
nvsEditorInstance = null;
|
|
99
105
|
}
|
|
100
|
-
nvseditorContainer.classList.add(
|
|
101
|
-
document.body.classList.remove(
|
|
102
|
-
|
|
106
|
+
nvseditorContainer.classList.add("hidden");
|
|
107
|
+
document.body.classList.remove("nvseditor-active");
|
|
108
|
+
|
|
103
109
|
// Hide Detect FS button
|
|
104
|
-
butDetectFS.classList.add(
|
|
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(
|
|
114
|
+
butOpenFSManager.classList.add("hidden");
|
|
109
115
|
}
|
|
110
|
-
|
|
116
|
+
|
|
111
117
|
// Hide ESP8266 info (if it exists)
|
|
112
|
-
const esp8266Info = document.getElementById(
|
|
118
|
+
const esp8266Info = document.getElementById("esp8266Info");
|
|
113
119
|
if (esp8266Info) {
|
|
114
|
-
esp8266Info.classList.add(
|
|
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(
|
|
133
|
+
|
|
134
|
+
logMsg("All cached data cleared");
|
|
129
135
|
}
|
|
130
136
|
|
|
131
|
-
const baudRates = [
|
|
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
|
|
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 =
|
|
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 =
|
|
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 !==
|
|
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,
|
|
308
|
-
blockSize: isWebUSB ? 248 : 3968,
|
|
309
|
-
maxInFlight: isWebUSB ? 248 : 15872
|
|
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(
|
|
319
|
-
document.body.classList.add(
|
|
327
|
+
document.body.classList.add("mobile-device");
|
|
328
|
+
document.body.classList.add("no-hover");
|
|
320
329
|
} else {
|
|
321
|
-
document.body.classList.remove(
|
|
322
|
-
document.body.classList.remove(
|
|
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(
|
|
350
|
-
window.addEventListener(
|
|
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(
|
|
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(
|
|
382
|
-
tabHex.addEventListener("click", () => switchViewerTab(
|
|
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) =>
|
|
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 = {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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(
|
|
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 !==
|
|
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 ===
|
|
663
|
+
|
|
664
|
+
if (unit === "KB") {
|
|
643
665
|
return value * 1024; // KB to bytes
|
|
644
|
-
} else if (unit ===
|
|
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(
|
|
657
|
-
|
|
678
|
+
console.log("[clickConnect] Function called");
|
|
679
|
+
|
|
658
680
|
if (espStub) {
|
|
659
|
-
console.log(
|
|
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(
|
|
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 ?
|
|
711
|
+
const platformMsg = `Platform: ${isAndroid ? "Android" : "Desktop"} (UA: ${userAgent.substring(0, 50)}...)`;
|
|
690
712
|
logMsg(platformMsg);
|
|
691
713
|
}
|
|
692
|
-
logMsg(`Using: ${isAndroid ?
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
821
|
-
butNVSEditor.classList.add(
|
|
822
|
-
|
|
843
|
+
butReadPartitions.classList.add("hidden");
|
|
844
|
+
butNVSEditor.classList.add("hidden");
|
|
845
|
+
|
|
823
846
|
// Show ESP8266 filesystem detection button
|
|
824
|
-
butDetectFS.classList.remove(
|
|
847
|
+
butDetectFS.classList.remove("hidden");
|
|
825
848
|
} else {
|
|
826
849
|
// Show partition table button for ESP32
|
|
827
|
-
butReadPartitions.classList.remove(
|
|
828
|
-
butNVSEditor.classList.remove(
|
|
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(
|
|
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(
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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(
|
|
1006
|
+
consoleContainer.removeEventListener("console-reset", consoleResetHandler);
|
|
980
1007
|
}
|
|
981
1008
|
consoleResetHandler = async () => {
|
|
982
|
-
if (
|
|
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(
|
|
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(
|
|
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 {
|
|
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(
|
|
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(
|
|
1029
|
-
|
|
1070
|
+
consoleContainer.addEventListener("console-reset", consoleResetHandler);
|
|
1071
|
+
|
|
1030
1072
|
// Listen for console close events
|
|
1031
1073
|
if (consoleCloseHandler) {
|
|
1032
|
-
consoleContainer.removeEventListener(
|
|
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(
|
|
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(
|
|
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 (
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1288
|
+
consoleContainer.removeEventListener(
|
|
1289
|
+
"console-reset",
|
|
1290
|
+
consoleResetHandler,
|
|
1291
|
+
);
|
|
1234
1292
|
consoleResetHandler = null;
|
|
1235
1293
|
}
|
|
1236
1294
|
if (consoleCloseHandler) {
|
|
1237
|
-
consoleContainer.removeEventListener(
|
|
1295
|
+
consoleContainer.removeEventListener(
|
|
1296
|
+
"console-close",
|
|
1297
|
+
consoleCloseHandler,
|
|
1298
|
+
);
|
|
1238
1299
|
consoleCloseHandler = null;
|
|
1239
1300
|
}
|
|
1240
1301
|
if (consoleBootloaderHandlerModule) {
|
|
1241
|
-
consoleContainer.removeEventListener(
|
|
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 =
|
|
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(
|
|
1356
|
-
const main = document.querySelector(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
1447
|
-
|
|
1511
|
+
const scannedLayout = esptoolMod.scanESP8266Filesystem(
|
|
1512
|
+
checkData,
|
|
1513
|
+
checkOffset,
|
|
1514
|
+
flashSizeBytes,
|
|
1515
|
+
);
|
|
1516
|
+
|
|
1448
1517
|
if (scannedLayout) {
|
|
1449
|
-
const fsType = esptoolMod.detectFilesystemFromImage(
|
|
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 !==
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 = [
|
|
1493
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
1514
|
-
const progressBar = readProgress.querySelector(
|
|
1600
|
+
readProgress.classList.remove("hidden");
|
|
1601
|
+
const progressBar = readProgress.querySelector("div");
|
|
1515
1602
|
if (progressBar) {
|
|
1516
|
-
progressBar.style.width =
|
|
1603
|
+
progressBar.style.width = "0%";
|
|
1517
1604
|
}
|
|
1518
|
-
|
|
1605
|
+
|
|
1519
1606
|
// Read the filesystem with real progress tracking
|
|
1520
|
-
logMsg(
|
|
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 =
|
|
1625
|
+
progressBar.style.width = "100%";
|
|
1537
1626
|
}
|
|
1538
|
-
await new Promise(resolve => setTimeout(resolve, 300));
|
|
1539
|
-
readProgress.classList.add(
|
|
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(
|
|
1634
|
+
butOpenFSManager.classList.add("hidden");
|
|
1546
1635
|
|
|
1547
1636
|
// Detect filesystem type
|
|
1548
|
-
const fsType = esptoolMod.detectFilesystemFromImage(
|
|
1637
|
+
const fsType = esptoolMod.detectFilesystemFromImage(
|
|
1638
|
+
fsData,
|
|
1639
|
+
currentChipName,
|
|
1640
|
+
);
|
|
1549
1641
|
logMsg(`Detected filesystem type: ${fsType.toUpperCase()}`);
|
|
1550
|
-
|
|
1551
|
-
if (fsType ===
|
|
1552
|
-
errorMsg(
|
|
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:
|
|
1651
|
+
name: "filesystem",
|
|
1560
1652
|
type: 0x01,
|
|
1561
|
-
subtype: fsType ===
|
|
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 ===
|
|
1662
|
+
if (fsType === "littlefs") {
|
|
1571
1663
|
await openLittleFS(partition);
|
|
1572
|
-
} else if (fsType ===
|
|
1664
|
+
} else if (fsType === "fatfs") {
|
|
1573
1665
|
await openFatFS(partition);
|
|
1574
|
-
} else if (fsType ===
|
|
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(
|
|
1671
|
+
debugMsg("Filesystem detection error details: " + e);
|
|
1581
1672
|
} finally {
|
|
1582
1673
|
// Hide progress bar
|
|
1583
|
-
readProgress.classList.add(
|
|
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(
|
|
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(
|
|
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
|
|
1766
|
-
|
|
1767
|
-
|
|
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(
|
|
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 !==
|
|
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(
|
|
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(
|
|
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(
|
|
1865
|
-
document.body.classList.add(
|
|
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(
|
|
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(
|
|
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(
|
|
1942
|
-
document.body.classList.remove(
|
|
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(
|
|
1949
|
-
document.body.classList.remove(
|
|
2045
|
+
hexeditorContainer.classList.add("hidden");
|
|
2046
|
+
document.body.classList.remove("hexeditor-active");
|
|
1950
2047
|
if (hexEditorInstance) {
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
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 (
|
|
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(
|
|
1976
|
-
document.body.classList.remove(
|
|
1977
|
-
try {
|
|
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(
|
|
1988
|
-
document.body.classList.add(
|
|
2092
|
+
nvseditorContainer.classList.remove("hidden");
|
|
2093
|
+
document.body.classList.add("nvseditor-active");
|
|
1989
2094
|
nvsEditorInstance.initProgressUI();
|
|
1990
|
-
nvsEditorInstance.showProgress(
|
|
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(
|
|
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(
|
|
2113
|
+
const nvsPartition = partitions.find(
|
|
2114
|
+
(p) => p.type === 0x01 && p.subtype === 0x02,
|
|
2115
|
+
);
|
|
2007
2116
|
if (!nvsPartition) {
|
|
2008
|
-
throw new Error(
|
|
2117
|
+
throw new Error("No NVS partition found in partition table");
|
|
2009
2118
|
}
|
|
2010
2119
|
|
|
2011
|
-
logMsg(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
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
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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(
|
|
2067
|
-
document.body.classList.remove(
|
|
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(
|
|
2073
|
-
nvseditorContainer.classList.add(
|
|
2074
|
-
document.body.classList.remove(
|
|
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 {
|
|
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(
|
|
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 ===
|
|
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(
|
|
2251
|
+
errorMsg(
|
|
2252
|
+
"No valid partition table found after " + MAX_ATTEMPTS + " attempts",
|
|
2253
|
+
);
|
|
2141
2254
|
return;
|
|
2142
2255
|
}
|
|
2143
2256
|
|
|
2144
|
-
logMsg(
|
|
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 =
|
|
2177
|
-
|
|
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 =
|
|
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",
|
|
2192
|
-
|
|
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",
|
|
2196
|
-
|
|
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 (
|
|
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
|
|
2317
|
-
|
|
2318
|
-
|
|
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 {
|
|
2578
|
+
try {
|
|
2579
|
+
nvsEditorInstance.close();
|
|
2580
|
+
} catch (_) {}
|
|
2437
2581
|
nvsEditorInstance = null;
|
|
2438
2582
|
}
|
|
2439
|
-
nvseditorContainer.classList.add(
|
|
2440
|
-
document.body.classList.remove(
|
|
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 ===
|
|
2701
|
+
|
|
2702
|
+
if (fsType === "littlefs") {
|
|
2560
2703
|
await openLittleFS(partition);
|
|
2561
|
-
} else if (fsType ===
|
|
2704
|
+
} else if (fsType === "fatfs") {
|
|
2562
2705
|
await openFatFS(partition);
|
|
2563
|
-
} else if (fsType ===
|
|
2706
|
+
} else if (fsType === "spiffs") {
|
|
2564
2707
|
await openSPIFFS(partition);
|
|
2565
2708
|
} else {
|
|
2566
|
-
errorMsg(
|
|
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(
|
|
2593
|
-
return
|
|
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 !==
|
|
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(
|
|
2613
|
-
return
|
|
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
|
|
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
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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) &
|
|
2781
|
-
const minor = diskVer &
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
2839
|
-
logMsg(
|
|
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(
|
|
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(
|
|
3053
|
+
logMsg(
|
|
3054
|
+
`Partition appears blank/unformatted. You can format and save to initialize it.`,
|
|
3055
|
+
);
|
|
2899
3056
|
} catch (createErr) {
|
|
2900
|
-
logMsg(
|
|
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(
|
|
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 =
|
|
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 =
|
|
2923
|
-
|
|
3083
|
+
littlefsDiskVersion.textContent = "FAT";
|
|
3084
|
+
|
|
2924
3085
|
// Show manager
|
|
2925
|
-
littlefsManager.classList.remove(
|
|
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(
|
|
3095
|
+
|
|
3096
|
+
logMsg("FatFS filesystem opened successfully");
|
|
2936
3097
|
} catch (e) {
|
|
2937
3098
|
errorMsg(`Failed to open FatFS: ${e.message || e}`);
|
|
2938
|
-
debugMsg(
|
|
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(
|
|
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(
|
|
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(
|
|
2976
|
-
logMsg(
|
|
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
|
|
2997
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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:
|
|
3036
|
-
type:
|
|
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(
|
|
3054
|
-
const file = this._files.find(f => {
|
|
3055
|
-
const fname = f.name.startsWith(
|
|
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 =
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
3183
|
-
|
|
3363
|
+
littlefsDiskVersion.textContent = "SPIFFS";
|
|
3364
|
+
|
|
3184
3365
|
// Show manager
|
|
3185
|
-
littlefsManager.classList.remove(
|
|
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(
|
|
3375
|
+
|
|
3376
|
+
logMsg("SPIFFS filesystem opened successfully");
|
|
3196
3377
|
} catch (e) {
|
|
3197
3378
|
errorMsg(`Failed to open SPIFFS: ${e.message || e}`);
|
|
3198
|
-
debugMsg(
|
|
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 ===
|
|
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 =
|
|
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 ===
|
|
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(
|
|
3445
|
+
if (entryPath.startsWith("/fatfs/")) {
|
|
3264
3446
|
entryPath = entryPath.slice(6);
|
|
3265
|
-
} else if (entryPath ===
|
|
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(
|
|
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(
|
|
3299
|
-
row.innerHTML =
|
|
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 ===
|
|
3307
|
-
if (a.type !==
|
|
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(
|
|
3314
|
-
|
|
3495
|
+
entries.forEach((entry) => {
|
|
3496
|
+
const row = document.createElement("tr");
|
|
3497
|
+
|
|
3315
3498
|
// Name
|
|
3316
|
-
const nameCell = document.createElement(
|
|
3317
|
-
nameCell.setAttribute(
|
|
3318
|
-
const nameDiv = document.createElement(
|
|
3319
|
-
nameDiv.className =
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
icon
|
|
3323
|
-
icon.
|
|
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 =
|
|
3327
|
-
|
|
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 ===
|
|
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(
|
|
3342
|
-
typeCell.setAttribute(
|
|
3343
|
-
typeCell.textContent = entry.type ===
|
|
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(
|
|
3348
|
-
sizeCell.setAttribute(
|
|
3349
|
-
sizeCell.textContent =
|
|
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(
|
|
3354
|
-
actionsCell.setAttribute(
|
|
3355
|
-
const actionsDiv = document.createElement(
|
|
3356
|
-
actionsDiv.className =
|
|
3357
|
-
|
|
3358
|
-
if (entry.type ===
|
|
3359
|
-
const downloadBtn = document.createElement(
|
|
3360
|
-
downloadBtn.textContent =
|
|
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(
|
|
3365
|
-
viewBtn.textContent =
|
|
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(
|
|
3371
|
-
deleteBtn.textContent =
|
|
3372
|
-
deleteBtn.className =
|
|
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(
|
|
3578
|
+
if (normalizedPath.startsWith("/fatfs/")) {
|
|
3393
3579
|
normalizedPath = normalizedPath.slice(6); // Remove '/fatfs' keeping the /
|
|
3394
|
-
} else if (normalizedPath ===
|
|
3395
|
-
normalizedPath =
|
|
3580
|
+
} else if (normalizedPath === "/fatfs") {
|
|
3581
|
+
normalizedPath = "/";
|
|
3396
3582
|
}
|
|
3397
|
-
|
|
3583
|
+
|
|
3398
3584
|
// Remove trailing slash except for root
|
|
3399
|
-
if (normalizedPath !==
|
|
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 ===
|
|
3412
|
-
|
|
3597
|
+
if (currentLittleFSPath === "/" || !currentLittleFSPath) return;
|
|
3598
|
+
|
|
3413
3599
|
// Split path and remove last segment
|
|
3414
|
-
const parts = currentLittleFSPath.split(
|
|
3600
|
+
const parts = currentLittleFSPath.split("/").filter(Boolean);
|
|
3415
3601
|
parts.pop();
|
|
3416
|
-
|
|
3602
|
+
|
|
3417
3603
|
// Reconstruct path
|
|
3418
|
-
currentLittleFSPath = parts.length ?
|
|
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
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
const
|
|
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(
|
|
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
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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() ||
|
|
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 ===
|
|
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(
|
|
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(
|
|
3770
|
+
if (!targetPath.endsWith("/")) targetPath += "/";
|
|
3573
3771
|
targetPath += file.name;
|
|
3574
|
-
|
|
3772
|
+
|
|
3575
3773
|
// Ensure parent directories exist
|
|
3576
|
-
const segments = targetPath.split(
|
|
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 ===
|
|
3788
|
+
if (typeof currentLittleFS.writeFile === "function") {
|
|
3591
3789
|
currentLittleFS.writeFile(targetPath, uint8Data);
|
|
3592
|
-
} else if (typeof currentLittleFS.addFile ===
|
|
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 ===
|
|
3621
|
-
errorMsg(
|
|
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(
|
|
3827
|
+
dirName = await window.electronAPI.showPrompt("Enter directory name:");
|
|
3628
3828
|
} else {
|
|
3629
|
-
dirName = prompt(
|
|
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(
|
|
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(
|
|
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(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
3716
|
-
|
|
3917
|
+
switchViewerTab("text");
|
|
3918
|
+
|
|
3717
3919
|
// Show modal
|
|
3718
|
-
fileViewerModal.classList.remove(
|
|
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(
|
|
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 =
|
|
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 ===
|
|
3754
|
-
tabText.classList.add(
|
|
3755
|
-
tabHex.classList.remove(
|
|
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(
|
|
3759
|
-
tabText.classList.remove(
|
|
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(
|
|
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 =
|
|
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,
|
|
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,
|
|
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 +
|
|
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
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
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(
|
|
3827
|
-
fileViewerText.className =
|
|
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(
|
|
4034
|
+
fileViewerModal.addEventListener("click", (e) => {
|
|
3832
4035
|
if (e.target === fileViewerModal) {
|
|
3833
4036
|
closeFileViewer();
|
|
3834
4037
|
}
|