esp32tool 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/css/dark.css +156 -0
- package/css/light.css +156 -0
- package/css/style.css +870 -0
- package/dist/const.d.ts +277 -0
- package/dist/const.js +511 -0
- package/dist/esp_loader.d.ts +222 -0
- package/dist/esp_loader.js +1466 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +15 -0
- package/dist/lib/spiffs/index.d.ts +15 -0
- package/dist/lib/spiffs/index.js +16 -0
- package/dist/lib/spiffs/spiffs.d.ts +26 -0
- package/dist/lib/spiffs/spiffs.js +132 -0
- package/dist/lib/spiffs/spiffsBlock.d.ts +36 -0
- package/dist/lib/spiffs/spiffsBlock.js +140 -0
- package/dist/lib/spiffs/spiffsConfig.d.ts +63 -0
- package/dist/lib/spiffs/spiffsConfig.js +79 -0
- package/dist/lib/spiffs/spiffsPage.d.ts +45 -0
- package/dist/lib/spiffs/spiffsPage.js +260 -0
- package/dist/lib/spiffs/spiffsReader.d.ts +19 -0
- package/dist/lib/spiffs/spiffsReader.js +192 -0
- package/dist/partition.d.ts +26 -0
- package/dist/partition.js +129 -0
- package/dist/struct.d.ts +2 -0
- package/dist/struct.js +91 -0
- package/dist/stubs/esp32.json +8 -0
- package/dist/stubs/esp32c2.json +8 -0
- package/dist/stubs/esp32c3.json +8 -0
- package/dist/stubs/esp32c5.json +8 -0
- package/dist/stubs/esp32c6.json +8 -0
- package/dist/stubs/esp32c61.json +8 -0
- package/dist/stubs/esp32h2.json +8 -0
- package/dist/stubs/esp32p4.json +8 -0
- package/dist/stubs/esp32p4r3.json +8 -0
- package/dist/stubs/esp32s2.json +8 -0
- package/dist/stubs/esp32s3.json +8 -0
- package/dist/stubs/esp8266.json +8 -0
- package/dist/stubs/index.d.ts +10 -0
- package/dist/stubs/index.js +56 -0
- package/dist/util.d.ts +14 -0
- package/dist/util.js +46 -0
- package/dist/wasm/filesystems.d.ts +33 -0
- package/dist/wasm/filesystems.js +114 -0
- package/dist/web/esp32-D955RjN9.js +16 -0
- package/dist/web/esp32c2-CJkxHDQi.js +16 -0
- package/dist/web/esp32c3-BhUHzH0o.js +16 -0
- package/dist/web/esp32c5-Chs0HtmA.js +16 -0
- package/dist/web/esp32c6-D6mPN6ut.js +16 -0
- package/dist/web/esp32c61-CQiYCWAs.js +16 -0
- package/dist/web/esp32h2-LsKJE9AS.js +16 -0
- package/dist/web/esp32p4-7nWC-HiD.js +16 -0
- package/dist/web/esp32p4r3-CwiPecZW.js +16 -0
- package/dist/web/esp32s2-CtqVheSJ.js +16 -0
- package/dist/web/esp32s3-CRbtB0QR.js +16 -0
- package/dist/web/esp8266-nEkNAo8K.js +16 -0
- package/dist/web/index.js +7265 -0
- package/electron/main.js +333 -0
- package/electron/preload.js +37 -0
- package/eslint.config.js +22 -0
- package/index.html +408 -0
- package/js/modules/esp32-D955RjN9.js +16 -0
- package/js/modules/esp32c2-CJkxHDQi.js +16 -0
- package/js/modules/esp32c3-BhUHzH0o.js +16 -0
- package/js/modules/esp32c5-Chs0HtmA.js +16 -0
- package/js/modules/esp32c6-D6mPN6ut.js +16 -0
- package/js/modules/esp32c61-CQiYCWAs.js +16 -0
- package/js/modules/esp32h2-LsKJE9AS.js +16 -0
- package/js/modules/esp32p4-7nWC-HiD.js +16 -0
- package/js/modules/esp32p4r3-CwiPecZW.js +16 -0
- package/js/modules/esp32s2-CtqVheSJ.js +16 -0
- package/js/modules/esp32s3-CRbtB0QR.js +16 -0
- package/js/modules/esp8266-nEkNAo8K.js +16 -0
- package/js/modules/esptool.js +7265 -0
- package/js/script.js +2237 -0
- package/js/utilities.js +182 -0
- package/license.md +11 -0
- package/package.json +61 -0
- package/script/build +12 -0
- package/script/develop +17 -0
- package/src/const.ts +599 -0
- package/src/esp_loader.ts +1907 -0
- package/src/index.ts +63 -0
- package/src/lib/spiffs/index.ts +22 -0
- package/src/lib/spiffs/spiffs.ts +175 -0
- package/src/lib/spiffs/spiffsBlock.ts +204 -0
- package/src/lib/spiffs/spiffsConfig.ts +140 -0
- package/src/lib/spiffs/spiffsPage.ts +357 -0
- package/src/lib/spiffs/spiffsReader.ts +280 -0
- package/src/partition.ts +155 -0
- package/src/struct.ts +108 -0
- package/src/stubs/README.md +3 -0
- package/src/stubs/esp32.json +8 -0
- package/src/stubs/esp32c2.json +8 -0
- package/src/stubs/esp32c3.json +8 -0
- package/src/stubs/esp32c5.json +8 -0
- package/src/stubs/esp32c6.json +8 -0
- package/src/stubs/esp32c61.json +8 -0
- package/src/stubs/esp32h2.json +8 -0
- package/src/stubs/esp32p4.json +8 -0
- package/src/stubs/esp32p4r3.json +8 -0
- package/src/stubs/esp32s2.json +8 -0
- package/src/stubs/esp32s3.json +8 -0
- package/src/stubs/esp8266.json +8 -0
- package/src/stubs/index.ts +86 -0
- package/src/util.ts +49 -0
- package/src/wasm/fatfs/fatfs.wasm +0 -0
- package/src/wasm/fatfs/index.d.ts +26 -0
- package/src/wasm/fatfs/index.js +343 -0
- package/src/wasm/filesystems.ts +156 -0
- package/src/wasm/littlefs/index.d.ts +83 -0
- package/src/wasm/littlefs/index.js +529 -0
- package/src/wasm/littlefs/littlefs.js +2 -0
- package/src/wasm/littlefs/littlefs.wasm +0 -0
- package/src/wasm/shared/types.ts +13 -0
package/js/script.js
ADDED
|
@@ -0,0 +1,2237 @@
|
|
|
1
|
+
let espStub;
|
|
2
|
+
let esp32s2ReconnectInProgress = false;
|
|
3
|
+
let currentLittleFS = null;
|
|
4
|
+
let currentLittleFSPartition = null;
|
|
5
|
+
let currentLittleFSPath = '/';
|
|
6
|
+
let currentLittleFSBlockSize = 4096;
|
|
7
|
+
let currentFilesystemType = null; // 'littlefs', 'fatfs', or 'spiffs'
|
|
8
|
+
let littlefsModulePromise = null; // Cache for LittleFS WASM module
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get display name for current filesystem type
|
|
12
|
+
*/
|
|
13
|
+
function getFilesystemDisplayName() {
|
|
14
|
+
if (!currentFilesystemType) return 'Filesystem';
|
|
15
|
+
switch (currentFilesystemType) {
|
|
16
|
+
case 'littlefs': return 'LittleFS';
|
|
17
|
+
case 'fatfs': return 'FatFS';
|
|
18
|
+
case 'spiffs': return 'SPIFFS';
|
|
19
|
+
default: return 'Filesystem';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Clear all cached data and state on disconnect
|
|
25
|
+
*/
|
|
26
|
+
function clearAllCachedData() {
|
|
27
|
+
// Close filesystem if open
|
|
28
|
+
if (currentLittleFS) {
|
|
29
|
+
try {
|
|
30
|
+
// Only call destroy if it exists (LittleFS has it, FatFS/SPIFFS don't)
|
|
31
|
+
if (typeof currentLittleFS.destroy === 'function') {
|
|
32
|
+
currentLittleFS.destroy();
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.error('Error destroying filesystem:', e);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Reset filesystem state
|
|
40
|
+
currentLittleFS = null;
|
|
41
|
+
currentLittleFSPartition = null;
|
|
42
|
+
currentLittleFSPath = '/';
|
|
43
|
+
currentLittleFSBlockSize = 4096;
|
|
44
|
+
currentFilesystemType = null;
|
|
45
|
+
|
|
46
|
+
// Hide filesystem manager
|
|
47
|
+
littlefsManager.classList.add('hidden');
|
|
48
|
+
|
|
49
|
+
// Clear partition list
|
|
50
|
+
partitionList.innerHTML = '';
|
|
51
|
+
partitionList.classList.add('hidden');
|
|
52
|
+
|
|
53
|
+
// Show the Read Partition Table button again
|
|
54
|
+
butReadPartitions.classList.remove('hidden');
|
|
55
|
+
|
|
56
|
+
// Clear file input
|
|
57
|
+
if (littlefsFileInput) {
|
|
58
|
+
littlefsFileInput.value = '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Reset buttons
|
|
62
|
+
butLittlefsUpload.disabled = true;
|
|
63
|
+
|
|
64
|
+
// Clear any cached module promises
|
|
65
|
+
littlefsModulePromise = null;
|
|
66
|
+
|
|
67
|
+
logMsg('All cached data cleared');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const baudRates = [2000000, 1500000, 921600, 500000, 460800, 230400, 153600, 128000, 115200];
|
|
71
|
+
const bufferSize = 512;
|
|
72
|
+
const colors = ["#00a7e9", "#f89521", "#be1e2d"];
|
|
73
|
+
const measurementPeriodId = "0001";
|
|
74
|
+
|
|
75
|
+
// Check if running in Electron
|
|
76
|
+
const isElectron = window.electronAPI && window.electronAPI.isElectron;
|
|
77
|
+
|
|
78
|
+
const maxLogLength = 100;
|
|
79
|
+
const log = document.getElementById("log");
|
|
80
|
+
const butConnect = document.getElementById("butConnect");
|
|
81
|
+
const baudRate = document.getElementById("baudRate");
|
|
82
|
+
const butClear = document.getElementById("butClear");
|
|
83
|
+
const butErase = document.getElementById("butErase");
|
|
84
|
+
const butProgram = document.getElementById("butProgram");
|
|
85
|
+
const butReadFlash = document.getElementById("butReadFlash");
|
|
86
|
+
const readOffset = document.getElementById("readOffset");
|
|
87
|
+
const readSize = document.getElementById("readSize");
|
|
88
|
+
const readProgress = document.getElementById("readProgress");
|
|
89
|
+
const butReadPartitions = document.getElementById("butReadPartitions");
|
|
90
|
+
const partitionList = document.getElementById("partitionList");
|
|
91
|
+
const littlefsManager = document.getElementById("littlefsManager");
|
|
92
|
+
const littlefsPartitionName = document.getElementById("littlefsPartitionName");
|
|
93
|
+
const littlefsPartitionSize = document.getElementById("littlefsPartitionSize");
|
|
94
|
+
const littlefsUsageBar = document.getElementById("littlefsUsageBar");
|
|
95
|
+
const littlefsUsageText = document.getElementById("littlefsUsageText");
|
|
96
|
+
const littlefsDiskVersion = document.getElementById("littlefsDiskVersion");
|
|
97
|
+
const littlefsFileList = document.getElementById("littlefsFileList");
|
|
98
|
+
const littlefsBreadcrumb = document.getElementById("littlefsBreadcrumb");
|
|
99
|
+
const butLittlefsUp = document.getElementById("butLittlefsUp");
|
|
100
|
+
const butLittlefsRefresh = document.getElementById("butLittlefsRefresh");
|
|
101
|
+
const butLittlefsBackup = document.getElementById("butLittlefsBackup");
|
|
102
|
+
const butLittlefsWrite = document.getElementById("butLittlefsWrite");
|
|
103
|
+
const butLittlefsClose = document.getElementById("butLittlefsClose");
|
|
104
|
+
const littlefsFileInput = document.getElementById("littlefsFileInput");
|
|
105
|
+
const butLittlefsUpload = document.getElementById("butLittlefsUpload");
|
|
106
|
+
const butLittlefsMkdir = document.getElementById("butLittlefsMkdir");
|
|
107
|
+
const autoscroll = document.getElementById("autoscroll");
|
|
108
|
+
const lightSS = document.getElementById("light");
|
|
109
|
+
const darkSS = document.getElementById("dark");
|
|
110
|
+
const darkMode = document.getElementById("darkmode");
|
|
111
|
+
const debugMode = document.getElementById("debugmode");
|
|
112
|
+
const showLog = document.getElementById("showlog");
|
|
113
|
+
const firmware = document.querySelectorAll(".upload .firmware input");
|
|
114
|
+
const progress = document.querySelectorAll(".upload .progress-bar");
|
|
115
|
+
const offsets = document.querySelectorAll(".upload .offset");
|
|
116
|
+
const appDiv = document.getElementById("app");
|
|
117
|
+
|
|
118
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
119
|
+
butConnect.addEventListener("click", () => {
|
|
120
|
+
clickConnect().catch(async (e) => {
|
|
121
|
+
console.error(e);
|
|
122
|
+
errorMsg(e.message || e);
|
|
123
|
+
if (espStub) {
|
|
124
|
+
await espStub.disconnect();
|
|
125
|
+
}
|
|
126
|
+
toggleUIConnected(false);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
butClear.addEventListener("click", clickClear);
|
|
130
|
+
butErase.addEventListener("click", clickErase);
|
|
131
|
+
butProgram.addEventListener("click", clickProgram);
|
|
132
|
+
butReadFlash.addEventListener("click", clickReadFlash);
|
|
133
|
+
butReadPartitions.addEventListener("click", clickReadPartitions);
|
|
134
|
+
butLittlefsRefresh.addEventListener("click", clickLittlefsRefresh);
|
|
135
|
+
butLittlefsBackup.addEventListener("click", clickLittlefsBackup);
|
|
136
|
+
butLittlefsWrite.addEventListener("click", clickLittlefsWrite);
|
|
137
|
+
butLittlefsClose.addEventListener("click", clickLittlefsClose);
|
|
138
|
+
butLittlefsUp.addEventListener("click", clickLittlefsUp);
|
|
139
|
+
butLittlefsUpload.addEventListener("click", clickLittlefsUpload);
|
|
140
|
+
butLittlefsMkdir.addEventListener("click", clickLittlefsMkdir);
|
|
141
|
+
littlefsFileInput.addEventListener("change", () => {
|
|
142
|
+
butLittlefsUpload.disabled = !littlefsFileInput.files.length;
|
|
143
|
+
});
|
|
144
|
+
for (let i = 0; i < firmware.length; i++) {
|
|
145
|
+
firmware[i].addEventListener("change", checkFirmware);
|
|
146
|
+
}
|
|
147
|
+
for (let i = 0; i < offsets.length; i++) {
|
|
148
|
+
offsets[i].addEventListener("change", checkProgrammable);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Initialize upload rows visibility - only show first row
|
|
152
|
+
updateUploadRowsVisibility();
|
|
153
|
+
|
|
154
|
+
autoscroll.addEventListener("click", clickAutoscroll);
|
|
155
|
+
baudRate.addEventListener("change", changeBaudRate);
|
|
156
|
+
darkMode.addEventListener("click", clickDarkMode);
|
|
157
|
+
debugMode.addEventListener("click", clickDebugMode);
|
|
158
|
+
showLog.addEventListener("click", clickShowLog);
|
|
159
|
+
window.addEventListener("error", function (event) {
|
|
160
|
+
console.log("Got an uncaught error: ", event.error);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Header auto-hide functionality
|
|
164
|
+
const header = document.querySelector(".header");
|
|
165
|
+
const main = document.querySelector(".main");
|
|
166
|
+
|
|
167
|
+
// Show header on mouse enter at top of page
|
|
168
|
+
main.addEventListener("mousemove", (e) => {
|
|
169
|
+
if (e.clientY < 5 && header.classList.contains("header-hidden")) {
|
|
170
|
+
header.classList.remove("header-hidden");
|
|
171
|
+
main.classList.remove("no-header-padding");
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Keep header visible when mouse is over it
|
|
176
|
+
header.addEventListener("mouseenter", () => {
|
|
177
|
+
header.classList.remove("header-hidden");
|
|
178
|
+
main.classList.remove("no-header-padding");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Hide header when mouse leaves (only if connected)
|
|
182
|
+
header.addEventListener("mouseleave", () => {
|
|
183
|
+
if (espStub && header.classList.contains("header-hidden") === false) {
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
if (!header.matches(":hover")) {
|
|
186
|
+
header.classList.add("header-hidden");
|
|
187
|
+
main.classList.add("no-header-padding");
|
|
188
|
+
}
|
|
189
|
+
}, 1000);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if ("serial" in navigator) {
|
|
194
|
+
const notSupported = document.getElementById("notSupported");
|
|
195
|
+
notSupported.classList.add("hidden");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
initBaudRate();
|
|
199
|
+
loadAllSettings();
|
|
200
|
+
updateTheme();
|
|
201
|
+
logMsg("ESP32Tool loaded.");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
function initBaudRate() {
|
|
205
|
+
for (let rate of baudRates) {
|
|
206
|
+
var option = document.createElement("option");
|
|
207
|
+
option.text = rate + " Baud";
|
|
208
|
+
option.value = rate;
|
|
209
|
+
baudRate.add(option);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function logMsg(text) {
|
|
214
|
+
log.innerHTML += text + "<br>";
|
|
215
|
+
|
|
216
|
+
// Remove old log content
|
|
217
|
+
if (log.textContent.split("\n").length > maxLogLength + 1) {
|
|
218
|
+
let logLines = log.innerHTML.replace(/(\n)/gm, "").split("<br>");
|
|
219
|
+
log.innerHTML = logLines.splice(-maxLogLength).join("<br>\n");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (autoscroll.checked) {
|
|
223
|
+
log.scrollTop = log.scrollHeight;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function debugMsg(...args) {
|
|
228
|
+
if (!debugMode.checked) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getStackTrace() {
|
|
233
|
+
let stack = new Error().stack;
|
|
234
|
+
//console.log(stack);
|
|
235
|
+
stack = stack.split("\n").map((v) => v.trim());
|
|
236
|
+
stack.shift();
|
|
237
|
+
stack.shift();
|
|
238
|
+
|
|
239
|
+
let trace = [];
|
|
240
|
+
for (let line of stack) {
|
|
241
|
+
line = line.replace("at ", "");
|
|
242
|
+
trace.push({
|
|
243
|
+
func: line.substr(0, line.indexOf("(") - 1),
|
|
244
|
+
pos: line.substring(line.indexOf(".js:") + 4, line.lastIndexOf(":")),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return trace;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let stack = getStackTrace();
|
|
252
|
+
stack.shift();
|
|
253
|
+
let top = stack.shift();
|
|
254
|
+
let prefix =
|
|
255
|
+
'<span class="debug-function">[' + top.func + ":" + top.pos + "]</span> ";
|
|
256
|
+
for (let arg of args) {
|
|
257
|
+
if (arg === undefined) {
|
|
258
|
+
logMsg(prefix + "undefined");
|
|
259
|
+
} else if (arg === null) {
|
|
260
|
+
logMsg(prefix + "null");
|
|
261
|
+
} else if (typeof arg == "string") {
|
|
262
|
+
logMsg(prefix + arg);
|
|
263
|
+
} else if (typeof arg == "number") {
|
|
264
|
+
logMsg(prefix + arg);
|
|
265
|
+
} else if (typeof arg == "boolean") {
|
|
266
|
+
logMsg(prefix + (arg ? "true" : "false"));
|
|
267
|
+
} else if (Array.isArray(arg)) {
|
|
268
|
+
logMsg(prefix + "[" + arg.map((value) => toHex(value)).join(", ") + "]");
|
|
269
|
+
} else if (typeof arg == "object" && arg instanceof Uint8Array) {
|
|
270
|
+
logMsg(
|
|
271
|
+
prefix +
|
|
272
|
+
"[" +
|
|
273
|
+
Array.from(arg)
|
|
274
|
+
.map((value) => toHex(value))
|
|
275
|
+
.join(", ") +
|
|
276
|
+
"]",
|
|
277
|
+
);
|
|
278
|
+
} else {
|
|
279
|
+
logMsg(prefix + "Unhandled type of argument:" + typeof arg);
|
|
280
|
+
console.log(arg);
|
|
281
|
+
}
|
|
282
|
+
prefix = ""; // Only show for first argument
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function errorMsg(text) {
|
|
287
|
+
logMsg('<span class="error-message">Error:</span> ' + text);
|
|
288
|
+
console.error(text);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* @name updateTheme
|
|
293
|
+
* Sets the theme to dark mode. Can be refactored later for more themes
|
|
294
|
+
*/
|
|
295
|
+
function updateTheme() {
|
|
296
|
+
// Disable all themes
|
|
297
|
+
document
|
|
298
|
+
.querySelectorAll("link[rel=stylesheet].alternate")
|
|
299
|
+
.forEach((styleSheet) => {
|
|
300
|
+
enableStyleSheet(styleSheet, false);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (darkMode.checked) {
|
|
304
|
+
enableStyleSheet(darkSS, true);
|
|
305
|
+
} else {
|
|
306
|
+
enableStyleSheet(lightSS, true);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function enableStyleSheet(node, enabled) {
|
|
311
|
+
node.disabled = !enabled;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function formatMacAddr(macAddr) {
|
|
315
|
+
return macAddr
|
|
316
|
+
.map((value) => value.toString(16).toUpperCase().padStart(2, "0"))
|
|
317
|
+
.join(":");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @name clickConnect
|
|
322
|
+
* Click handler for the connect/disconnect button.
|
|
323
|
+
*/
|
|
324
|
+
async function clickConnect() {
|
|
325
|
+
if (espStub) {
|
|
326
|
+
// Remove disconnect event listener to prevent it from firing during manual disconnect
|
|
327
|
+
if (espStub.handleDisconnect) {
|
|
328
|
+
espStub.removeEventListener("disconnect", espStub.handleDisconnect);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
await espStub.disconnect();
|
|
332
|
+
await espStub.port.close();
|
|
333
|
+
toggleUIConnected(false);
|
|
334
|
+
espStub = undefined;
|
|
335
|
+
|
|
336
|
+
// Clear all cached data and state
|
|
337
|
+
clearAllCachedData();
|
|
338
|
+
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const esploaderMod = await window.esptoolPackage;
|
|
343
|
+
|
|
344
|
+
let esploader = await esploaderMod.connect({
|
|
345
|
+
log: (...args) => logMsg(...args),
|
|
346
|
+
debug: (...args) => debugMsg(...args),
|
|
347
|
+
error: (...args) => errorMsg(...args),
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Store port info for ESP32-S2 detection
|
|
351
|
+
let portInfo = esploader.port?.getInfo ? esploader.port.getInfo() : {};
|
|
352
|
+
let isESP32S2 = portInfo.usbVendorId === 0x303a && portInfo.usbProductId === 0x0002;
|
|
353
|
+
|
|
354
|
+
// Handle ESP32-S2 Native USB reconnection requirement for BROWSER
|
|
355
|
+
// Only add listener if not already in reconnect mode and not in Electron
|
|
356
|
+
if (!esp32s2ReconnectInProgress && !isElectron) {
|
|
357
|
+
esploader.addEventListener("esp32s2-usb-reconnect", async () => {
|
|
358
|
+
// Prevent recursive calls
|
|
359
|
+
if (esp32s2ReconnectInProgress) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
esp32s2ReconnectInProgress = true;
|
|
364
|
+
logMsg("ESP32-S2 Native USB detected!");
|
|
365
|
+
toggleUIConnected(false);
|
|
366
|
+
espStub = undefined;
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
await esploader.port.close();
|
|
370
|
+
|
|
371
|
+
if (esploader.port.forget) {
|
|
372
|
+
await esploader.port.forget();
|
|
373
|
+
}
|
|
374
|
+
} catch (disconnectErr) {
|
|
375
|
+
// Ignore disconnect errors
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Show modal dialog
|
|
379
|
+
const modal = document.getElementById("esp32s2Modal");
|
|
380
|
+
const reconnectBtn = document.getElementById("butReconnectS2");
|
|
381
|
+
|
|
382
|
+
modal.classList.remove("hidden");
|
|
383
|
+
|
|
384
|
+
// Handle reconnect button click
|
|
385
|
+
const handleReconnect = async () => {
|
|
386
|
+
modal.classList.add("hidden");
|
|
387
|
+
reconnectBtn.removeEventListener("click", handleReconnect);
|
|
388
|
+
|
|
389
|
+
// Trigger port selection
|
|
390
|
+
try {
|
|
391
|
+
await clickConnect();
|
|
392
|
+
// Reset flag on successful connection
|
|
393
|
+
esp32s2ReconnectInProgress = false;
|
|
394
|
+
} catch (err) {
|
|
395
|
+
errorMsg("Failed to reconnect: " + err);
|
|
396
|
+
// Reset flag on error so user can try again
|
|
397
|
+
esp32s2ReconnectInProgress = false;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
reconnectBtn.addEventListener("click", handleReconnect);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
await esploader.initialize();
|
|
407
|
+
} catch (err) {
|
|
408
|
+
// Check if this is an ESP32-S2 that needs reconnection
|
|
409
|
+
if (isESP32S2 && isElectron && !esp32s2ReconnectInProgress) {
|
|
410
|
+
esp32s2ReconnectInProgress = true;
|
|
411
|
+
logMsg("ESP32-S2 Native USB detected - automatic reconnection...");
|
|
412
|
+
toggleUIConnected(false);
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
await esploader.port.close();
|
|
416
|
+
} catch (e) {
|
|
417
|
+
console.debug("Port close error:", e);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Wait for new port to appear
|
|
421
|
+
logMsg("Waiting for ESP32-S2 CDC port...");
|
|
422
|
+
|
|
423
|
+
const waitForNewPort = new Promise((resolve) => {
|
|
424
|
+
const checkInterval = setInterval(() => {
|
|
425
|
+
if (navigator.serial && navigator.serial.getPorts) {
|
|
426
|
+
navigator.serial.getPorts().then(ports => {
|
|
427
|
+
if (ports.length > 0) {
|
|
428
|
+
clearInterval(checkInterval);
|
|
429
|
+
resolve(ports[0]);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}, 50);
|
|
434
|
+
|
|
435
|
+
// Timeout after 500 ms
|
|
436
|
+
setTimeout(() => {
|
|
437
|
+
clearInterval(checkInterval);
|
|
438
|
+
resolve(null);
|
|
439
|
+
}, 500);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const newPort = await waitForNewPort;
|
|
443
|
+
|
|
444
|
+
if (!newPort) {
|
|
445
|
+
esp32s2ReconnectInProgress = false;
|
|
446
|
+
throw new Error("ESP32-S2 CDC port did not appear in time");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Additional small delay to ensure port is ready
|
|
450
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
451
|
+
|
|
452
|
+
// Open the new port and create ESPLoader directly
|
|
453
|
+
await newPort.open({ baudRate: 115200 });
|
|
454
|
+
logMsg("Connected successfully.");
|
|
455
|
+
|
|
456
|
+
esploader = new esploaderMod.ESPLoader(newPort, {
|
|
457
|
+
log: (...args) => logMsg(...args),
|
|
458
|
+
debug: (...args) => debugMsg(...args),
|
|
459
|
+
error: (...args) => errorMsg(...args),
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Initialize the new connection
|
|
463
|
+
await esploader.initialize();
|
|
464
|
+
|
|
465
|
+
esp32s2ReconnectInProgress = false;
|
|
466
|
+
logMsg("ESP32-S2 reconnection successful!");
|
|
467
|
+
} else {
|
|
468
|
+
// If ESP32-S2 reconnect is in progress (browser modal), suppress the error
|
|
469
|
+
if (esp32s2ReconnectInProgress) {
|
|
470
|
+
logMsg("Initialization interrupted for ESP32-S2 reconnection.");
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Not ESP32-S2 or reconnect already attempted
|
|
475
|
+
try {
|
|
476
|
+
await esploader.disconnect();
|
|
477
|
+
} catch (disconnectErr) {
|
|
478
|
+
// Ignore disconnect errors
|
|
479
|
+
}
|
|
480
|
+
throw err;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
logMsg("Connected to " + esploader.chipName);
|
|
485
|
+
logMsg("MAC Address: " + formatMacAddr(esploader.macAddr()));
|
|
486
|
+
|
|
487
|
+
espStub = await esploader.runStub();
|
|
488
|
+
toggleUIConnected(true);
|
|
489
|
+
toggleUIToolbar(true);
|
|
490
|
+
|
|
491
|
+
// Set detected flash size in the read size field
|
|
492
|
+
if (espStub.flashSize) {
|
|
493
|
+
const flashSizeBytes = parseInt(espStub.flashSize) * 1024 * 1024; // Convert MB to bytes
|
|
494
|
+
readSize.value = "0x" + flashSizeBytes.toString(16);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Set the selected baud rate
|
|
498
|
+
let baud = parseInt(baudRate.value);
|
|
499
|
+
if (baudRates.includes(baud)) {
|
|
500
|
+
await espStub.setBaudrate(baud);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Store disconnect handler so we can remove it later
|
|
504
|
+
const handleDisconnect = () => {
|
|
505
|
+
toggleUIConnected(false);
|
|
506
|
+
espStub = false;
|
|
507
|
+
};
|
|
508
|
+
espStub.handleDisconnect = handleDisconnect; // Store reference on espStub
|
|
509
|
+
espStub.addEventListener("disconnect", handleDisconnect);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* @name changeBaudRate
|
|
514
|
+
* Change handler for the Baud Rate selector.
|
|
515
|
+
*/
|
|
516
|
+
async function changeBaudRate() {
|
|
517
|
+
saveSetting("baudrate", baudRate.value);
|
|
518
|
+
if (espStub) {
|
|
519
|
+
let baud = parseInt(baudRate.value);
|
|
520
|
+
if (baudRates.includes(baud)) {
|
|
521
|
+
await espStub.setBaudrate(baud);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* @name clickAutoscroll
|
|
528
|
+
* Change handler for the Autoscroll checkbox.
|
|
529
|
+
*/
|
|
530
|
+
async function clickAutoscroll() {
|
|
531
|
+
saveSetting("autoscroll", autoscroll.checked);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* @name clickDarkMode
|
|
536
|
+
* Change handler for the Dark Mode checkbox.
|
|
537
|
+
*/
|
|
538
|
+
async function clickDarkMode() {
|
|
539
|
+
updateTheme();
|
|
540
|
+
saveSetting("darkmode", darkMode.checked);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* @name clickDebugMode
|
|
545
|
+
* Change handler for the Debug Mode checkbox.
|
|
546
|
+
*/
|
|
547
|
+
async function clickDebugMode() {
|
|
548
|
+
saveSetting("debugmode", debugMode.checked);
|
|
549
|
+
logMsg("Debug mode " + (debugMode.checked ? "enabled" : "disabled"));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* @name clickShowLog
|
|
554
|
+
* Change handler for the Show Log checkbox.
|
|
555
|
+
*/
|
|
556
|
+
async function clickShowLog() {
|
|
557
|
+
saveSetting("showlog", showLog.checked);
|
|
558
|
+
updateLogVisibility();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* @name updateLogVisibility
|
|
563
|
+
* Update log and log controls visibility
|
|
564
|
+
*/
|
|
565
|
+
function updateLogVisibility() {
|
|
566
|
+
const logControls = document.querySelector(".log-controls");
|
|
567
|
+
|
|
568
|
+
if (showLog.checked) {
|
|
569
|
+
log.classList.remove("hidden");
|
|
570
|
+
if (logControls) {
|
|
571
|
+
logControls.classList.remove("hidden");
|
|
572
|
+
}
|
|
573
|
+
} else {
|
|
574
|
+
log.classList.add("hidden");
|
|
575
|
+
if (logControls) {
|
|
576
|
+
logControls.classList.add("hidden");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* @name clickErase
|
|
583
|
+
* Click handler for the erase button.
|
|
584
|
+
*/
|
|
585
|
+
async function clickErase() {
|
|
586
|
+
let confirmed = false;
|
|
587
|
+
|
|
588
|
+
if (isElectron) {
|
|
589
|
+
confirmed = await window.electronAPI.showConfirm("This will erase the entire flash. Click OK to continue.");
|
|
590
|
+
} else {
|
|
591
|
+
confirmed = window.confirm("This will erase the entire flash. Click OK to continue.");
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (confirmed) {
|
|
595
|
+
baudRate.disabled = true;
|
|
596
|
+
butErase.disabled = true;
|
|
597
|
+
butProgram.disabled = true;
|
|
598
|
+
try {
|
|
599
|
+
logMsg("Erasing flash memory. Please wait...");
|
|
600
|
+
let stamp = Date.now();
|
|
601
|
+
await espStub.eraseFlash();
|
|
602
|
+
logMsg("Finished. Took " + (Date.now() - stamp) + "ms to erase.");
|
|
603
|
+
} catch (e) {
|
|
604
|
+
errorMsg(e);
|
|
605
|
+
} finally {
|
|
606
|
+
butErase.disabled = false;
|
|
607
|
+
baudRate.disabled = false;
|
|
608
|
+
butProgram.disabled = getValidFiles().length == 0;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* @name clickProgram
|
|
615
|
+
* Click handler for the program button.
|
|
616
|
+
*/
|
|
617
|
+
async function clickProgram() {
|
|
618
|
+
const readUploadedFileAsArrayBuffer = (inputFile) => {
|
|
619
|
+
const reader = new FileReader();
|
|
620
|
+
|
|
621
|
+
return new Promise((resolve, reject) => {
|
|
622
|
+
reader.onerror = () => {
|
|
623
|
+
reader.abort();
|
|
624
|
+
reject(new DOMException("Problem parsing input file."));
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
reader.onload = () => {
|
|
628
|
+
resolve(reader.result);
|
|
629
|
+
};
|
|
630
|
+
reader.readAsArrayBuffer(inputFile);
|
|
631
|
+
});
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
baudRate.disabled = true;
|
|
635
|
+
butErase.disabled = true;
|
|
636
|
+
butProgram.disabled = true;
|
|
637
|
+
for (let i = 0; i < firmware.length; i++) {
|
|
638
|
+
firmware[i].disabled = true;
|
|
639
|
+
offsets[i].disabled = true;
|
|
640
|
+
}
|
|
641
|
+
for (let file of getValidFiles()) {
|
|
642
|
+
progress[file].classList.remove("hidden");
|
|
643
|
+
let binfile = firmware[file].files[0];
|
|
644
|
+
let contents = await readUploadedFileAsArrayBuffer(binfile);
|
|
645
|
+
try {
|
|
646
|
+
let offset = parseInt(offsets[file].value, 16);
|
|
647
|
+
const progressBar = progress[file].querySelector("div");
|
|
648
|
+
await espStub.flashData(
|
|
649
|
+
contents,
|
|
650
|
+
(bytesWritten, totalBytes) => {
|
|
651
|
+
progressBar.style.width =
|
|
652
|
+
Math.floor((bytesWritten / totalBytes) * 100) + "%";
|
|
653
|
+
},
|
|
654
|
+
offset,
|
|
655
|
+
);
|
|
656
|
+
await sleep(100);
|
|
657
|
+
} catch (e) {
|
|
658
|
+
errorMsg(e);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
for (let i = 0; i < firmware.length; i++) {
|
|
662
|
+
firmware[i].disabled = false;
|
|
663
|
+
offsets[i].disabled = false;
|
|
664
|
+
progress[i].classList.add("hidden");
|
|
665
|
+
progress[i].querySelector("div").style.width = "0";
|
|
666
|
+
}
|
|
667
|
+
butErase.disabled = false;
|
|
668
|
+
baudRate.disabled = false;
|
|
669
|
+
butProgram.disabled = getValidFiles().length == 0;
|
|
670
|
+
logMsg("To run the new firmware, please reset your device.");
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function getValidFiles() {
|
|
674
|
+
// Get a list of file and offsets
|
|
675
|
+
// This will be used to check if we have valid stuff
|
|
676
|
+
// and will also return a list of files to program
|
|
677
|
+
let validFiles = [];
|
|
678
|
+
let offsetVals = [];
|
|
679
|
+
for (let i = 0; i < firmware.length; i++) {
|
|
680
|
+
let offs = parseInt(offsets[i].value, 16);
|
|
681
|
+
if (firmware[i].files.length > 0 && !offsetVals.includes(offs)) {
|
|
682
|
+
validFiles.push(i);
|
|
683
|
+
offsetVals.push(offs);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return validFiles;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* @name checkProgrammable
|
|
691
|
+
* Check if the conditions to program the device are sufficient
|
|
692
|
+
*/
|
|
693
|
+
async function checkProgrammable() {
|
|
694
|
+
butProgram.disabled = getValidFiles().length == 0;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* @name checkFirmware
|
|
699
|
+
* Handler for firmware upload changes
|
|
700
|
+
*/
|
|
701
|
+
async function checkFirmware(event) {
|
|
702
|
+
let filename = event.target.value.split("\\").pop();
|
|
703
|
+
let label = event.target.parentNode.querySelector("span");
|
|
704
|
+
let icon = event.target.parentNode.querySelector("svg");
|
|
705
|
+
if (filename != "") {
|
|
706
|
+
label.innerHTML = filename;
|
|
707
|
+
icon.classList.add("hidden");
|
|
708
|
+
} else {
|
|
709
|
+
label.innerHTML = "Choose a file…";
|
|
710
|
+
icon.classList.remove("hidden");
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
await checkProgrammable();
|
|
714
|
+
updateUploadRowsVisibility();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* @name updateUploadRowsVisibility
|
|
719
|
+
* Show/hide upload rows dynamically - only for flash write section
|
|
720
|
+
*/
|
|
721
|
+
function updateUploadRowsVisibility() {
|
|
722
|
+
const uploadRows = document.querySelectorAll(".upload");
|
|
723
|
+
let lastFilledIndex = -1;
|
|
724
|
+
|
|
725
|
+
// Find the last filled row
|
|
726
|
+
for (let i = 0; i < firmware.length; i++) {
|
|
727
|
+
if (firmware[i].files.length > 0) {
|
|
728
|
+
lastFilledIndex = i;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Show rows up to lastFilledIndex + 1 (next empty row), minimum 1 row
|
|
733
|
+
for (let i = 0; i < uploadRows.length; i++) {
|
|
734
|
+
if (i <= lastFilledIndex + 1) {
|
|
735
|
+
uploadRows[i].style.display = "flex";
|
|
736
|
+
} else {
|
|
737
|
+
uploadRows[i].style.display = "none";
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* @name clickReadFlash
|
|
744
|
+
* Click handler for the read flash button.
|
|
745
|
+
*/
|
|
746
|
+
async function clickReadFlash() {
|
|
747
|
+
const offset = parseInt(readOffset.value, 16);
|
|
748
|
+
const size = parseInt(readSize.value, 16);
|
|
749
|
+
|
|
750
|
+
if (isNaN(offset) || isNaN(size) || size <= 0) {
|
|
751
|
+
errorMsg("Invalid offset or size value");
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const defaultFilename = `flash_0x${offset.toString(16)}_0x${size.toString(16)}.bin`;
|
|
756
|
+
|
|
757
|
+
baudRate.disabled = true;
|
|
758
|
+
butErase.disabled = true;
|
|
759
|
+
butProgram.disabled = true;
|
|
760
|
+
butReadFlash.disabled = true;
|
|
761
|
+
readOffset.disabled = true;
|
|
762
|
+
readSize.disabled = true;
|
|
763
|
+
readProgress.classList.remove("hidden");
|
|
764
|
+
|
|
765
|
+
try {
|
|
766
|
+
const progressBar = readProgress.querySelector("div");
|
|
767
|
+
|
|
768
|
+
const data = await espStub.readFlash(
|
|
769
|
+
offset,
|
|
770
|
+
size,
|
|
771
|
+
(packet, progress, totalSize) => {
|
|
772
|
+
progressBar.style.width =
|
|
773
|
+
Math.floor((progress / totalSize) * 100) + "%";
|
|
774
|
+
}
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
logMsg(`Successfully read ${data.length} bytes from flash`);
|
|
778
|
+
|
|
779
|
+
// Save file using Electron API or browser download
|
|
780
|
+
await saveDataToFile(data, defaultFilename);
|
|
781
|
+
|
|
782
|
+
} catch (e) {
|
|
783
|
+
errorMsg("Failed to read flash: " + e);
|
|
784
|
+
} finally {
|
|
785
|
+
readProgress.classList.add("hidden");
|
|
786
|
+
readProgress.querySelector("div").style.width = "0";
|
|
787
|
+
butErase.disabled = false;
|
|
788
|
+
baudRate.disabled = false;
|
|
789
|
+
butProgram.disabled = getValidFiles().length == 0;
|
|
790
|
+
butReadFlash.disabled = false;
|
|
791
|
+
readOffset.disabled = false;
|
|
792
|
+
readSize.disabled = false;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* @name clickReadPartitions
|
|
798
|
+
* Click handler for the read partitions button.
|
|
799
|
+
*/
|
|
800
|
+
async function clickReadPartitions() {
|
|
801
|
+
const PARTITION_TABLE_OFFSET = 0x8000;
|
|
802
|
+
const PARTITION_TABLE_SIZE = 0x1000; // Read 4KB to get all partitions
|
|
803
|
+
|
|
804
|
+
butReadPartitions.disabled = true;
|
|
805
|
+
butErase.disabled = true;
|
|
806
|
+
butProgram.disabled = true;
|
|
807
|
+
butReadFlash.disabled = true;
|
|
808
|
+
|
|
809
|
+
try {
|
|
810
|
+
logMsg("Reading partition table from 0x8000...");
|
|
811
|
+
|
|
812
|
+
const data = await espStub.readFlash(PARTITION_TABLE_OFFSET, PARTITION_TABLE_SIZE);
|
|
813
|
+
|
|
814
|
+
const partitions = parsePartitionTable(data);
|
|
815
|
+
|
|
816
|
+
if (partitions.length === 0) {
|
|
817
|
+
errorMsg("No valid partition table found");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
logMsg(`Found ${partitions.length} partition(s)`);
|
|
822
|
+
|
|
823
|
+
// Display partitions
|
|
824
|
+
displayPartitions(partitions);
|
|
825
|
+
|
|
826
|
+
} catch (e) {
|
|
827
|
+
errorMsg("Failed to read partition table: " + e);
|
|
828
|
+
} finally {
|
|
829
|
+
butReadPartitions.disabled = false;
|
|
830
|
+
butErase.disabled = false;
|
|
831
|
+
butProgram.disabled = getValidFiles().length == 0;
|
|
832
|
+
butReadFlash.disabled = false;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Parse partition table from binary data
|
|
838
|
+
*/
|
|
839
|
+
function parsePartitionTable(data) {
|
|
840
|
+
const PARTITION_MAGIC = 0x50aa;
|
|
841
|
+
const PARTITION_ENTRY_SIZE = 32;
|
|
842
|
+
const partitions = [];
|
|
843
|
+
|
|
844
|
+
for (let i = 0; i < data.length; i += PARTITION_ENTRY_SIZE) {
|
|
845
|
+
const magic = data[i] | (data[i + 1] << 8);
|
|
846
|
+
|
|
847
|
+
if (magic !== PARTITION_MAGIC) {
|
|
848
|
+
break; // End of partition table
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const type = data[i + 2];
|
|
852
|
+
const subtype = data[i + 3];
|
|
853
|
+
const offset = data[i + 4] | (data[i + 5] << 8) | (data[i + 6] << 16) | (data[i + 7] << 24);
|
|
854
|
+
const size = data[i + 8] | (data[i + 9] << 8) | (data[i + 10] << 16) | (data[i + 11] << 24);
|
|
855
|
+
|
|
856
|
+
// Read name (16 bytes, null-terminated)
|
|
857
|
+
let name = "";
|
|
858
|
+
for (let j = 12; j < 28; j++) {
|
|
859
|
+
if (data[i + j] === 0) break;
|
|
860
|
+
name += String.fromCharCode(data[i + j]);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const flags = data[i + 28] | (data[i + 29] << 8) | (data[i + 30] << 16) | (data[i + 31] << 24);
|
|
864
|
+
|
|
865
|
+
// Get type names
|
|
866
|
+
const typeNames = { 0x00: "app", 0x01: "data" };
|
|
867
|
+
const appSubtypes = {
|
|
868
|
+
0x00: "factory", 0x10: "ota_0", 0x11: "ota_1", 0x12: "ota_2",
|
|
869
|
+
0x13: "ota_3", 0x14: "ota_4", 0x15: "ota_5", 0x20: "test"
|
|
870
|
+
};
|
|
871
|
+
const dataSubtypes = {
|
|
872
|
+
0x00: "ota", 0x01: "phy", 0x02: "nvs", 0x03: "coredump",
|
|
873
|
+
0x04: "nvs_keys", 0x05: "efuse", 0x81: "fat", 0x82: "spiffs"
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
const typeName = typeNames[type] || `0x${type.toString(16)}`;
|
|
877
|
+
let subtypeName = "";
|
|
878
|
+
if (type === 0x00) {
|
|
879
|
+
subtypeName = appSubtypes[subtype] || `0x${subtype.toString(16)}`;
|
|
880
|
+
} else if (type === 0x01) {
|
|
881
|
+
subtypeName = dataSubtypes[subtype] || `0x${subtype.toString(16)}`;
|
|
882
|
+
} else {
|
|
883
|
+
subtypeName = `0x${subtype.toString(16)}`;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
partitions.push({
|
|
887
|
+
name,
|
|
888
|
+
type,
|
|
889
|
+
subtype,
|
|
890
|
+
offset,
|
|
891
|
+
size,
|
|
892
|
+
flags,
|
|
893
|
+
typeName,
|
|
894
|
+
subtypeName
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return partitions;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Display partitions in the UI
|
|
903
|
+
*/
|
|
904
|
+
function displayPartitions(partitions) {
|
|
905
|
+
partitionList.innerHTML = "";
|
|
906
|
+
partitionList.classList.remove("hidden");
|
|
907
|
+
|
|
908
|
+
// Hide the Read Partition Table button after successful read
|
|
909
|
+
butReadPartitions.classList.add("hidden");
|
|
910
|
+
|
|
911
|
+
const table = document.createElement("table");
|
|
912
|
+
table.className = "partition-table-display";
|
|
913
|
+
|
|
914
|
+
// Header
|
|
915
|
+
const thead = document.createElement("thead");
|
|
916
|
+
const headerRow = document.createElement("tr");
|
|
917
|
+
["Name", "Type", "SubType", "Offset", "Size", "Action"].forEach(text => {
|
|
918
|
+
const th = document.createElement("th");
|
|
919
|
+
th.textContent = text;
|
|
920
|
+
headerRow.appendChild(th);
|
|
921
|
+
});
|
|
922
|
+
thead.appendChild(headerRow);
|
|
923
|
+
table.appendChild(thead);
|
|
924
|
+
|
|
925
|
+
// Body
|
|
926
|
+
const tbody = document.createElement("tbody");
|
|
927
|
+
partitions.forEach(partition => {
|
|
928
|
+
const row = document.createElement("tr");
|
|
929
|
+
|
|
930
|
+
// Name
|
|
931
|
+
const nameCell = document.createElement("td");
|
|
932
|
+
nameCell.textContent = partition.name;
|
|
933
|
+
row.appendChild(nameCell);
|
|
934
|
+
|
|
935
|
+
// Type
|
|
936
|
+
const typeCell = document.createElement("td");
|
|
937
|
+
typeCell.textContent = partition.typeName;
|
|
938
|
+
row.appendChild(typeCell);
|
|
939
|
+
|
|
940
|
+
// SubType
|
|
941
|
+
const subtypeCell = document.createElement("td");
|
|
942
|
+
subtypeCell.textContent = partition.subtypeName;
|
|
943
|
+
row.appendChild(subtypeCell);
|
|
944
|
+
|
|
945
|
+
// Offset
|
|
946
|
+
const offsetCell = document.createElement("td");
|
|
947
|
+
offsetCell.textContent = `0x${partition.offset.toString(16)}`;
|
|
948
|
+
row.appendChild(offsetCell);
|
|
949
|
+
|
|
950
|
+
// Size
|
|
951
|
+
const sizeCell = document.createElement("td");
|
|
952
|
+
sizeCell.textContent = formatSize(partition.size);
|
|
953
|
+
row.appendChild(sizeCell);
|
|
954
|
+
|
|
955
|
+
// Action
|
|
956
|
+
const actionCell = document.createElement("td");
|
|
957
|
+
const downloadBtn = document.createElement("button");
|
|
958
|
+
downloadBtn.textContent = "Download";
|
|
959
|
+
downloadBtn.className = "partition-download-btn";
|
|
960
|
+
downloadBtn.onclick = () => downloadPartition(partition);
|
|
961
|
+
actionCell.appendChild(downloadBtn);
|
|
962
|
+
|
|
963
|
+
// Add "Open FS" button for data partitions with filesystem
|
|
964
|
+
// 0x81 = FAT, 0x82 = SPIFFS (often contains LittleFS)
|
|
965
|
+
if (partition.type === 0x01 && (partition.subtype === 0x81 || partition.subtype === 0x82)) {
|
|
966
|
+
const fsBtn = document.createElement("button");
|
|
967
|
+
fsBtn.textContent = "Open FS";
|
|
968
|
+
fsBtn.className = "littlefs-fs-button";
|
|
969
|
+
fsBtn.onclick = () => openFilesystem(partition);
|
|
970
|
+
actionCell.appendChild(fsBtn);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
row.appendChild(actionCell);
|
|
974
|
+
|
|
975
|
+
tbody.appendChild(row);
|
|
976
|
+
});
|
|
977
|
+
table.appendChild(tbody);
|
|
978
|
+
|
|
979
|
+
partitionList.appendChild(table);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Download a partition
|
|
984
|
+
*/
|
|
985
|
+
async function downloadPartition(partition) {
|
|
986
|
+
const defaultFilename = `${partition.name}_0x${partition.offset.toString(16)}.bin`;
|
|
987
|
+
|
|
988
|
+
const partitionProgress = document.getElementById("partitionProgress");
|
|
989
|
+
const progressBar = partitionProgress.querySelector("div");
|
|
990
|
+
|
|
991
|
+
try {
|
|
992
|
+
partitionProgress.classList.remove("hidden");
|
|
993
|
+
progressBar.style.width = "0%";
|
|
994
|
+
|
|
995
|
+
logMsg(
|
|
996
|
+
`Downloading partition "${partition.name}" (${formatSize(partition.size)})...`
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
const data = await espStub.readFlash(
|
|
1000
|
+
partition.offset,
|
|
1001
|
+
partition.size,
|
|
1002
|
+
(packet, progress, totalSize) => {
|
|
1003
|
+
const percent = Math.floor((progress / totalSize) * 100);
|
|
1004
|
+
progressBar.style.width = percent + "%";
|
|
1005
|
+
}
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
// Save file using Electron API or browser download
|
|
1009
|
+
await saveDataToFile(data, defaultFilename);
|
|
1010
|
+
|
|
1011
|
+
logMsg(`Partition "${partition.name}" downloaded successfully`);
|
|
1012
|
+
} catch (e) {
|
|
1013
|
+
errorMsg(`Failed to download partition: ${e}`);
|
|
1014
|
+
} finally {
|
|
1015
|
+
partitionProgress.classList.add("hidden");
|
|
1016
|
+
progressBar.style.width = "0%";
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Format size in human-readable format
|
|
1022
|
+
*/
|
|
1023
|
+
function formatSize(bytes) {
|
|
1024
|
+
if (bytes < 1024) {
|
|
1025
|
+
return `${bytes} B`;
|
|
1026
|
+
} else if (bytes < 1024 * 1024) {
|
|
1027
|
+
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
1028
|
+
} else {
|
|
1029
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* @name clickClear
|
|
1035
|
+
* Click handler for the clear button.
|
|
1036
|
+
*/
|
|
1037
|
+
async function clickClear() {
|
|
1038
|
+
// reset(); Reset function wasnt declared.
|
|
1039
|
+
log.innerHTML = "";
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function convertJSON(chunk) {
|
|
1043
|
+
try {
|
|
1044
|
+
let jsonObj = JSON.parse(chunk);
|
|
1045
|
+
return jsonObj;
|
|
1046
|
+
} catch (e) {
|
|
1047
|
+
return chunk;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function toggleUIToolbar(show) {
|
|
1052
|
+
isConnected = show;
|
|
1053
|
+
for (let i = 0; i < progress.length; i++) {
|
|
1054
|
+
progress[i].classList.add("hidden");
|
|
1055
|
+
progress[i].querySelector("div").style.width = "0";
|
|
1056
|
+
}
|
|
1057
|
+
if (show) {
|
|
1058
|
+
appDiv.classList.add("connected");
|
|
1059
|
+
} else {
|
|
1060
|
+
appDiv.classList.remove("connected");
|
|
1061
|
+
}
|
|
1062
|
+
butErase.disabled = !show;
|
|
1063
|
+
butReadFlash.disabled = !show;
|
|
1064
|
+
butReadPartitions.disabled = !show;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function toggleUIConnected(connected) {
|
|
1068
|
+
let lbl = "Connect";
|
|
1069
|
+
const header = document.querySelector(".header");
|
|
1070
|
+
const main = document.querySelector(".main");
|
|
1071
|
+
|
|
1072
|
+
if (connected) {
|
|
1073
|
+
lbl = "Disconnect";
|
|
1074
|
+
// Auto-hide header after connection
|
|
1075
|
+
setTimeout(() => {
|
|
1076
|
+
header.classList.add("header-hidden");
|
|
1077
|
+
main.classList.add("no-header-padding");
|
|
1078
|
+
}, 2000); // Hide after 2 seconds
|
|
1079
|
+
} else {
|
|
1080
|
+
toggleUIToolbar(false);
|
|
1081
|
+
// Show header when disconnected
|
|
1082
|
+
header.classList.remove("header-hidden");
|
|
1083
|
+
main.classList.remove("no-header-padding");
|
|
1084
|
+
}
|
|
1085
|
+
butConnect.textContent = lbl;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function loadAllSettings() {
|
|
1089
|
+
// Load all saved settings or defaults
|
|
1090
|
+
autoscroll.checked = loadSetting("autoscroll", true);
|
|
1091
|
+
baudRate.value = loadSetting("baudrate", 2000000);
|
|
1092
|
+
darkMode.checked = loadSetting("darkmode", false);
|
|
1093
|
+
debugMode.checked = loadSetting("debugmode", true);
|
|
1094
|
+
showLog.checked = loadSetting("showlog", false);
|
|
1095
|
+
|
|
1096
|
+
// Apply show log setting
|
|
1097
|
+
updateLogVisibility();
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function loadSetting(setting, defaultValue) {
|
|
1101
|
+
let value = JSON.parse(window.localStorage.getItem(setting));
|
|
1102
|
+
if (value == null) {
|
|
1103
|
+
return defaultValue;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return value;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function saveSetting(setting, value) {
|
|
1110
|
+
window.localStorage.setItem(setting, JSON.stringify(value));
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function ucWords(text) {
|
|
1114
|
+
return text
|
|
1115
|
+
.replace("_", " ")
|
|
1116
|
+
.toLowerCase()
|
|
1117
|
+
.replace(/(?<= )[^\s]|^./g, (a) => a.toUpperCase());
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function sleep(ms) {
|
|
1121
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Save data to file - uses Electron API in desktop app, browser download otherwise
|
|
1126
|
+
*/
|
|
1127
|
+
async function saveDataToFile(data, defaultFilename) {
|
|
1128
|
+
if (isElectron) {
|
|
1129
|
+
// Use Electron's native save dialog
|
|
1130
|
+
const result = await window.electronAPI.saveFile(
|
|
1131
|
+
Array.from(data), // Convert Uint8Array to regular array for IPC
|
|
1132
|
+
defaultFilename
|
|
1133
|
+
);
|
|
1134
|
+
|
|
1135
|
+
if (result.success) {
|
|
1136
|
+
logMsg(`File saved: ${result.filePath}`);
|
|
1137
|
+
} else if (result.canceled) {
|
|
1138
|
+
logMsg("Save cancelled by user");
|
|
1139
|
+
} else {
|
|
1140
|
+
errorMsg(`Failed to save file: ${result.error}`);
|
|
1141
|
+
}
|
|
1142
|
+
} else {
|
|
1143
|
+
// Browser fallback - use download link
|
|
1144
|
+
const blob = new Blob([data], { type: "application/octet-stream" });
|
|
1145
|
+
const url = URL.createObjectURL(blob);
|
|
1146
|
+
const a = document.createElement("a");
|
|
1147
|
+
a.href = url;
|
|
1148
|
+
a.download = defaultFilename;
|
|
1149
|
+
document.body.appendChild(a);
|
|
1150
|
+
a.click();
|
|
1151
|
+
document.body.removeChild(a);
|
|
1152
|
+
URL.revokeObjectURL(url);
|
|
1153
|
+
logMsg(`Flash data downloaded as "${defaultFilename}"`);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Read file from disk - uses Electron API in desktop app
|
|
1159
|
+
*/
|
|
1160
|
+
async function readFileFromDisk() {
|
|
1161
|
+
if (isElectron) {
|
|
1162
|
+
const result = await window.electronAPI.openFile();
|
|
1163
|
+
|
|
1164
|
+
if (result.success) {
|
|
1165
|
+
return {
|
|
1166
|
+
data: new Uint8Array(result.data),
|
|
1167
|
+
filename: result.filename,
|
|
1168
|
+
filePath: result.filePath
|
|
1169
|
+
};
|
|
1170
|
+
} else if (result.canceled) {
|
|
1171
|
+
return null;
|
|
1172
|
+
} else {
|
|
1173
|
+
throw new Error(result.error);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Open and mount a filesystem partition
|
|
1182
|
+
*/
|
|
1183
|
+
async function openFilesystem(partition) {
|
|
1184
|
+
try {
|
|
1185
|
+
logMsg(`Detecting filesystem type for partition "${partition.name}"...`);
|
|
1186
|
+
|
|
1187
|
+
// Detect filesystem type
|
|
1188
|
+
const fsType = await detectFilesystemType(partition.offset, partition.size);
|
|
1189
|
+
logMsg(`Detected filesystem: ${fsType}`);
|
|
1190
|
+
|
|
1191
|
+
if (fsType === 'littlefs') {
|
|
1192
|
+
await openLittleFS(partition);
|
|
1193
|
+
} else if (fsType === 'fatfs') {
|
|
1194
|
+
await openFatFS(partition);
|
|
1195
|
+
} else if (fsType === 'spiffs') {
|
|
1196
|
+
await openSPIFFS(partition);
|
|
1197
|
+
} else {
|
|
1198
|
+
errorMsg('Unknown filesystem type. Cannot open partition.');
|
|
1199
|
+
}
|
|
1200
|
+
} catch (e) {
|
|
1201
|
+
errorMsg(`Failed to open filesystem: ${e.message || e}`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Detect filesystem type by reading partition header
|
|
1207
|
+
*
|
|
1208
|
+
* LittleFS Detection:
|
|
1209
|
+
* - LittleFS stores metadata blocks at the beginning of the partition
|
|
1210
|
+
* - The superblock contains the string "littlefs" in its metadata
|
|
1211
|
+
* - LittleFS uses a specific block structure with magic numbers
|
|
1212
|
+
*
|
|
1213
|
+
* FatFS Detection:
|
|
1214
|
+
* - FAT boot sector signature 0xAA55 at offset 510-511
|
|
1215
|
+
* - FAT signature string at offset 54 (FAT16) or 82 (FAT32)
|
|
1216
|
+
*
|
|
1217
|
+
* SPIFFS Detection:
|
|
1218
|
+
* - SPIFFS has a different structure without the "littlefs" string
|
|
1219
|
+
* - SPIFFS uses object headers with different magic numbers
|
|
1220
|
+
*
|
|
1221
|
+
* Detection Strategy:
|
|
1222
|
+
* 1. Read first 8KB of partition (covers multiple blocks)
|
|
1223
|
+
* 2. Search for "littlefs" string in ASCII representation
|
|
1224
|
+
* 3. Check for FAT boot signature
|
|
1225
|
+
* 4. Check for SPIFFS magic
|
|
1226
|
+
* 5. If found -> corresponding FS, otherwise -> SPIFFS
|
|
1227
|
+
*/
|
|
1228
|
+
async function detectFilesystemType(offset, size) {
|
|
1229
|
+
try {
|
|
1230
|
+
// Read first 8KB or entire partition if smaller
|
|
1231
|
+
const readSize = Math.min(8192, size);
|
|
1232
|
+
const data = await espStub.readFlash(offset, readSize);
|
|
1233
|
+
|
|
1234
|
+
if (data.length < 32) {
|
|
1235
|
+
logMsg('Partition too small, assuming SPIFFS');
|
|
1236
|
+
return 'spiffs';
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
1240
|
+
|
|
1241
|
+
// Method 1: Check for SPIFFS magic number FIRST (most reliable)
|
|
1242
|
+
// SPIFFS magic: 0x20140529 XORed with page size and optionally (blocksLim - blockIndex)
|
|
1243
|
+
// The magic is stored as objIdLen bytes (typically 2 bytes) in the last lookup page
|
|
1244
|
+
// Pattern: Always ends with 0x05 in the second byte (from 0x0529)
|
|
1245
|
+
|
|
1246
|
+
// Check at block boundaries (every 4KB) for SPIFFS magic
|
|
1247
|
+
for (let blockOffset = 0; blockOffset < Math.min(8192, data.length); blockOffset += 4096) {
|
|
1248
|
+
// SPIFFS stores magic near the end of the last lookup page
|
|
1249
|
+
// For 4KB blocks, check around offset 0xF0-0xFF
|
|
1250
|
+
for (let pageOffset = 0xE0; pageOffset < Math.min(0x100, data.length - blockOffset - 2); pageOffset += 2) {
|
|
1251
|
+
const offset = blockOffset + pageOffset;
|
|
1252
|
+
if (offset + 2 > data.length) break;
|
|
1253
|
+
|
|
1254
|
+
// Read as 16-bit (objIdLen=2, most common)
|
|
1255
|
+
const magic16 = view.getUint16(offset, true);
|
|
1256
|
+
|
|
1257
|
+
// SPIFFS magic pattern: second byte is always 0x05 (from base 0x0529)
|
|
1258
|
+
// First byte varies based on XOR with pageSize and block index
|
|
1259
|
+
if ((magic16 & 0xFF00) === 0x0500) {
|
|
1260
|
+
logMsg(`SPIFFS detected: Found SPIFFS magic pattern 0x${magic16.toString(16)} at offset 0x${offset.toString(16)}`);
|
|
1261
|
+
return 'spiffs';
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Method 2: Check for "littlefs" string in metadata (very reliable)
|
|
1267
|
+
// LittleFS stores this in the superblock metadata
|
|
1268
|
+
const decoder = new TextDecoder('ascii', { fatal: false });
|
|
1269
|
+
const dataStr = decoder.decode(data);
|
|
1270
|
+
|
|
1271
|
+
if (dataStr.includes('littlefs')) {
|
|
1272
|
+
logMsg('LittleFS detected: Found "littlefs" signature in partition data');
|
|
1273
|
+
return 'littlefs';
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Method 3: Check for FAT filesystem signatures
|
|
1277
|
+
if (data.length >= 512) {
|
|
1278
|
+
const bootSig = view.getUint16(510, true);
|
|
1279
|
+
|
|
1280
|
+
if (bootSig === 0xAA55) {
|
|
1281
|
+
// Check for FAT signature strings
|
|
1282
|
+
let fat16Sig = '';
|
|
1283
|
+
let fat32Sig = '';
|
|
1284
|
+
|
|
1285
|
+
if (data.length >= 62) {
|
|
1286
|
+
fat16Sig = String.fromCharCode(data[54], data[55], data[56], data[57], data[58]);
|
|
1287
|
+
}
|
|
1288
|
+
if (data.length >= 90) {
|
|
1289
|
+
fat32Sig = String.fromCharCode(data[82], data[83], data[84], data[85], data[86]);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
if (fat16Sig.startsWith('FAT') || fat32Sig.startsWith('FAT')) {
|
|
1293
|
+
logMsg('FatFS detected: Found FAT boot signature and FAT string');
|
|
1294
|
+
return 'fatfs';
|
|
1295
|
+
} else {
|
|
1296
|
+
logMsg('Boot signature found but no FAT string - might be empty/unformatted FAT partition');
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// Method 4: Check for LittleFS magic numbers (more specific than structure check)
|
|
1302
|
+
// LittleFS v2.x magic: 0x32736c66 ("lfs2" in little-endian)
|
|
1303
|
+
// LittleFS v1.x magic: 0x31736c66 ("lfs1" in little-endian)
|
|
1304
|
+
for (let i = 0; i < Math.min(8192, data.length - 4); i++) {
|
|
1305
|
+
const magic32 = view.getUint32(i, true);
|
|
1306
|
+
if (magic32 === 0x32736c66 || magic32 === 0x31736c66) {
|
|
1307
|
+
logMsg('LittleFS detected: Found LittleFS magic number 0x' + magic32.toString(16) + ' at offset ' + i);
|
|
1308
|
+
return 'littlefs';
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// Default: If no clear signature found, assume SPIFFS
|
|
1313
|
+
logMsg('No clear filesystem signature found, assuming SPIFFS');
|
|
1314
|
+
return 'spiffs';
|
|
1315
|
+
|
|
1316
|
+
} catch (err) {
|
|
1317
|
+
errorMsg(`Failed to detect filesystem type: ${err.message || err}`);
|
|
1318
|
+
return 'spiffs'; // Safe fallback
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Lazy-load and cache the LittleFS WASM module
|
|
1324
|
+
*/
|
|
1325
|
+
async function loadLittlefsModule() {
|
|
1326
|
+
if (!littlefsModulePromise) {
|
|
1327
|
+
// Use absolute path from root for better compatibility with GitHub Pages
|
|
1328
|
+
const basePath = window.location.pathname.endsWith('/')
|
|
1329
|
+
? window.location.pathname
|
|
1330
|
+
: window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1);
|
|
1331
|
+
const modulePath = `${basePath}src/wasm/littlefs/index.js`;
|
|
1332
|
+
|
|
1333
|
+
littlefsModulePromise = import(modulePath)
|
|
1334
|
+
.catch(error => {
|
|
1335
|
+
console.error('Failed to load LittleFS module from:', modulePath, error);
|
|
1336
|
+
littlefsModulePromise = null; // Reset on error so it can be retried
|
|
1337
|
+
throw error;
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
return littlefsModulePromise;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Reset LittleFS state
|
|
1345
|
+
*/
|
|
1346
|
+
function resetLittleFSState() {
|
|
1347
|
+
// Clean up existing filesystem instance
|
|
1348
|
+
if (currentLittleFS) {
|
|
1349
|
+
try {
|
|
1350
|
+
// Don't call destroy() - it can cause crashes
|
|
1351
|
+
// Just let garbage collection handle it
|
|
1352
|
+
} catch (e) {
|
|
1353
|
+
console.error('Error cleaning up LittleFS:', e);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
currentLittleFS = null;
|
|
1358
|
+
currentLittleFSPartition = null;
|
|
1359
|
+
currentLittleFSPath = '/';
|
|
1360
|
+
currentLittleFSBlockSize = 4096;
|
|
1361
|
+
|
|
1362
|
+
// Hide UI - safely check if elements exist
|
|
1363
|
+
try {
|
|
1364
|
+
if (littlefsManager) {
|
|
1365
|
+
littlefsManager.classList.add('hidden');
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Clear file list
|
|
1369
|
+
if (littlefsFileList) {
|
|
1370
|
+
littlefsFileList.innerHTML = '';
|
|
1371
|
+
}
|
|
1372
|
+
} catch (e) {
|
|
1373
|
+
console.error('Error resetting LittleFS UI:', e);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Open LittleFS partition
|
|
1379
|
+
*/
|
|
1380
|
+
async function openLittleFS(partition) {
|
|
1381
|
+
try {
|
|
1382
|
+
logMsg(`Reading LittleFS partition "${partition.name}" (${formatSize(partition.size)})...`);
|
|
1383
|
+
|
|
1384
|
+
// Read entire partition
|
|
1385
|
+
const partitionProgress = document.getElementById("partitionProgress");
|
|
1386
|
+
const progressBar = partitionProgress.querySelector("div");
|
|
1387
|
+
partitionProgress.classList.remove("hidden");
|
|
1388
|
+
|
|
1389
|
+
const data = await espStub.readFlash(
|
|
1390
|
+
partition.offset,
|
|
1391
|
+
partition.size,
|
|
1392
|
+
(packet, progress, totalSize) => {
|
|
1393
|
+
const percent = Math.floor((progress / totalSize) * 100);
|
|
1394
|
+
progressBar.style.width = percent + "%";
|
|
1395
|
+
}
|
|
1396
|
+
);
|
|
1397
|
+
|
|
1398
|
+
partitionProgress.classList.add("hidden");
|
|
1399
|
+
progressBar.style.width = "0%";
|
|
1400
|
+
|
|
1401
|
+
logMsg('Mounting LittleFS filesystem...');
|
|
1402
|
+
|
|
1403
|
+
// Try to mount with different block sizes
|
|
1404
|
+
const blockSizes = [4096, 2048, 1024, 512];
|
|
1405
|
+
let fs = null;
|
|
1406
|
+
let blockSize = 0;
|
|
1407
|
+
|
|
1408
|
+
// Use cached module loader
|
|
1409
|
+
const module = await loadLittlefsModule();
|
|
1410
|
+
const { createLittleFSFromImage, formatDiskVersion } = module;
|
|
1411
|
+
|
|
1412
|
+
for (const bs of blockSizes) {
|
|
1413
|
+
try {
|
|
1414
|
+
const blockCount = Math.floor(partition.size / bs);
|
|
1415
|
+
fs = await createLittleFSFromImage(data, {
|
|
1416
|
+
blockSize: bs,
|
|
1417
|
+
blockCount: blockCount,
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
// Try to list root to verify it works
|
|
1421
|
+
fs.list('/');
|
|
1422
|
+
blockSize = bs;
|
|
1423
|
+
logMsg(`Successfully mounted LittleFS with block size ${bs}`);
|
|
1424
|
+
break;
|
|
1425
|
+
} catch (err) {
|
|
1426
|
+
// Try next block size
|
|
1427
|
+
// Don't call destroy() - just let it be garbage collected
|
|
1428
|
+
fs = null;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (!fs) {
|
|
1433
|
+
throw new Error('Failed to mount LittleFS with any block size');
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// Store filesystem instance
|
|
1437
|
+
currentLittleFS = fs;
|
|
1438
|
+
currentLittleFSPartition = partition;
|
|
1439
|
+
currentLittleFSPath = '/';
|
|
1440
|
+
currentLittleFSBlockSize = blockSize;
|
|
1441
|
+
currentFilesystemType = 'littlefs';
|
|
1442
|
+
|
|
1443
|
+
// Update UI
|
|
1444
|
+
littlefsPartitionName.textContent = partition.name;
|
|
1445
|
+
littlefsPartitionSize.textContent = formatSize(partition.size);
|
|
1446
|
+
|
|
1447
|
+
// Get disk version
|
|
1448
|
+
try {
|
|
1449
|
+
const diskVer = fs.getDiskVersion();
|
|
1450
|
+
const major = (diskVer >> 16) & 0xFFFF;
|
|
1451
|
+
const minor = diskVer & 0xFFFF;
|
|
1452
|
+
littlefsDiskVersion.textContent = `v${major}.${minor}`;
|
|
1453
|
+
} catch (e) {
|
|
1454
|
+
littlefsDiskVersion.textContent = '';
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// Show manager
|
|
1458
|
+
littlefsManager.classList.remove('hidden');
|
|
1459
|
+
|
|
1460
|
+
// Enable all operations for LittleFS (including directories)
|
|
1461
|
+
butLittlefsUpload.disabled = false;
|
|
1462
|
+
butLittlefsMkdir.disabled = false;
|
|
1463
|
+
butLittlefsWrite.disabled = false;
|
|
1464
|
+
|
|
1465
|
+
// Load files
|
|
1466
|
+
refreshLittleFS();
|
|
1467
|
+
|
|
1468
|
+
logMsg('LittleFS filesystem opened successfully');
|
|
1469
|
+
} catch (e) {
|
|
1470
|
+
errorMsg(`Failed to open LittleFS: ${e.message || e}`);
|
|
1471
|
+
// Don't call destroy() - just reset state
|
|
1472
|
+
resetLittleFSState();
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* Open FatFS partition
|
|
1478
|
+
*/
|
|
1479
|
+
async function openFatFS(partition) {
|
|
1480
|
+
try {
|
|
1481
|
+
logMsg(`Reading FatFS partition "${partition.name}" (${formatSize(partition.size)})...`);
|
|
1482
|
+
|
|
1483
|
+
// Read entire partition
|
|
1484
|
+
const partitionProgress = document.getElementById("partitionProgress");
|
|
1485
|
+
const progressBar = partitionProgress.querySelector("div");
|
|
1486
|
+
partitionProgress.classList.remove("hidden");
|
|
1487
|
+
|
|
1488
|
+
const data = await espStub.readFlash(
|
|
1489
|
+
partition.offset,
|
|
1490
|
+
partition.size,
|
|
1491
|
+
(packet, progress, totalSize) => {
|
|
1492
|
+
const percent = Math.floor((progress / totalSize) * 100);
|
|
1493
|
+
progressBar.style.width = percent + "%";
|
|
1494
|
+
}
|
|
1495
|
+
);
|
|
1496
|
+
|
|
1497
|
+
partitionProgress.classList.add("hidden");
|
|
1498
|
+
progressBar.style.width = "0%";
|
|
1499
|
+
|
|
1500
|
+
logMsg('Mounting FatFS filesystem...');
|
|
1501
|
+
logMsg(`Partition size: ${formatSize(partition.size)} (${partition.size} bytes)`);
|
|
1502
|
+
|
|
1503
|
+
// Load FatFS module
|
|
1504
|
+
const basePath = window.location.pathname.endsWith('/')
|
|
1505
|
+
? window.location.pathname
|
|
1506
|
+
: window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1);
|
|
1507
|
+
const modulePath = `${basePath}src/wasm/fatfs/index.js`;
|
|
1508
|
+
const module = await import(modulePath);
|
|
1509
|
+
const { createFatFSFromImage, createFatFS } = module;
|
|
1510
|
+
|
|
1511
|
+
// Use 4096 block size (ESP32 standard)
|
|
1512
|
+
let blockSize = 4096;
|
|
1513
|
+
let blockCount = Math.max(1, Math.floor(partition.size / blockSize));
|
|
1514
|
+
if (blockCount <= 0) {
|
|
1515
|
+
blockCount = 1;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
let fs = null;
|
|
1519
|
+
|
|
1520
|
+
// First try to mount existing FatFS from image
|
|
1521
|
+
try {
|
|
1522
|
+
logMsg(`Trying to mount FatFS with block size ${blockSize} (${blockCount} blocks)...`);
|
|
1523
|
+
|
|
1524
|
+
fs = await createFatFSFromImage(data, {
|
|
1525
|
+
blockSize: blockSize,
|
|
1526
|
+
blockCount: blockCount,
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
logMsg(`FatFS instance created, attempting to list files...`);
|
|
1530
|
+
const files = fs.list();
|
|
1531
|
+
logMsg(`Successfully listed ${files.length} files/directories`);
|
|
1532
|
+
logMsg(`Successfully mounted FatFS`);
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
logMsg(`Failed to mount existing FatFS: ${err.message || err}`);
|
|
1535
|
+
|
|
1536
|
+
// If mounting fails, create a new empty formatted filesystem
|
|
1537
|
+
// Note: This does NOT use the image data - it creates a blank filesystem
|
|
1538
|
+
if (createFatFS) {
|
|
1539
|
+
try {
|
|
1540
|
+
logMsg(`Creating new blank FatFS (not using image data)...`);
|
|
1541
|
+
fs = await createFatFS({
|
|
1542
|
+
blockSize: blockSize,
|
|
1543
|
+
blockCount: blockCount,
|
|
1544
|
+
formatOnInit: true,
|
|
1545
|
+
});
|
|
1546
|
+
logMsg(`Created new formatted FatFS`);
|
|
1547
|
+
logMsg(`Partition appears blank/unformatted. You can format and save to initialize it.`);
|
|
1548
|
+
} catch (createErr) {
|
|
1549
|
+
logMsg(`Failed to create new FatFS: ${createErr.message || createErr}`);
|
|
1550
|
+
throw err; // Throw original error
|
|
1551
|
+
}
|
|
1552
|
+
} else {
|
|
1553
|
+
throw err;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
if (!fs) {
|
|
1558
|
+
throw new Error('Failed to mount FatFS with any block size. The partition may not contain a valid FAT filesystem or may be corrupted.');
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// Store filesystem instance and block size
|
|
1562
|
+
currentLittleFS = fs;
|
|
1563
|
+
currentLittleFSPartition = partition;
|
|
1564
|
+
currentLittleFSPath = '/';
|
|
1565
|
+
currentLittleFSBlockSize = blockSize;
|
|
1566
|
+
currentFilesystemType = 'fatfs';
|
|
1567
|
+
|
|
1568
|
+
// Update UI
|
|
1569
|
+
littlefsPartitionName.textContent = partition.name;
|
|
1570
|
+
littlefsPartitionSize.textContent = formatSize(partition.size);
|
|
1571
|
+
littlefsDiskVersion.textContent = 'FAT';
|
|
1572
|
+
|
|
1573
|
+
// Show manager
|
|
1574
|
+
littlefsManager.classList.remove('hidden');
|
|
1575
|
+
|
|
1576
|
+
// Enable all operations for FatFS (including directories)
|
|
1577
|
+
butLittlefsUpload.disabled = false;
|
|
1578
|
+
butLittlefsMkdir.disabled = false;
|
|
1579
|
+
butLittlefsWrite.disabled = false;
|
|
1580
|
+
|
|
1581
|
+
// Load files
|
|
1582
|
+
refreshLittleFS();
|
|
1583
|
+
|
|
1584
|
+
logMsg('FatFS filesystem opened successfully');
|
|
1585
|
+
} catch (e) {
|
|
1586
|
+
errorMsg(`Failed to open FatFS: ${e.message || e}`);
|
|
1587
|
+
console.error('FatFS open error:', e);
|
|
1588
|
+
resetLittleFSState();
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* Open SPIFFS partition
|
|
1594
|
+
*/
|
|
1595
|
+
async function openSPIFFS(partition) {
|
|
1596
|
+
try {
|
|
1597
|
+
logMsg(`Reading SPIFFS partition "${partition.name}" (${formatSize(partition.size)})...`);
|
|
1598
|
+
|
|
1599
|
+
// Read entire partition
|
|
1600
|
+
const partitionProgress = document.getElementById("partitionProgress");
|
|
1601
|
+
const progressBar = partitionProgress.querySelector("div");
|
|
1602
|
+
partitionProgress.classList.remove("hidden");
|
|
1603
|
+
|
|
1604
|
+
const data = await espStub.readFlash(
|
|
1605
|
+
partition.offset,
|
|
1606
|
+
partition.size,
|
|
1607
|
+
(packet, progress, totalSize) => {
|
|
1608
|
+
const percent = Math.floor((progress / totalSize) * 100);
|
|
1609
|
+
progressBar.style.width = percent + "%";
|
|
1610
|
+
}
|
|
1611
|
+
);
|
|
1612
|
+
|
|
1613
|
+
partitionProgress.classList.add("hidden");
|
|
1614
|
+
progressBar.style.width = "0%";
|
|
1615
|
+
|
|
1616
|
+
logMsg('Parsing SPIFFS filesystem...');
|
|
1617
|
+
logMsg(`Partition size: ${formatSize(partition.size)} (${partition.size} bytes)`);
|
|
1618
|
+
|
|
1619
|
+
// Import SPIFFS module
|
|
1620
|
+
const basePath = window.location.pathname.endsWith('/')
|
|
1621
|
+
? window.location.pathname
|
|
1622
|
+
: window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1);
|
|
1623
|
+
const modulePath = `${basePath}js/modules/esptool.js`;
|
|
1624
|
+
|
|
1625
|
+
const { SpiffsFS, SpiffsReader, SpiffsBuildConfig, DEFAULT_SPIFFS_CONFIG } = await import(modulePath);
|
|
1626
|
+
|
|
1627
|
+
// Create build config with partition size
|
|
1628
|
+
const config = new SpiffsBuildConfig({
|
|
1629
|
+
...DEFAULT_SPIFFS_CONFIG,
|
|
1630
|
+
imgSize: partition.size,
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
// Create reader and parse existing files
|
|
1634
|
+
const reader = new SpiffsReader(data, config);
|
|
1635
|
+
reader.parse();
|
|
1636
|
+
|
|
1637
|
+
// Get file list
|
|
1638
|
+
const files = reader.listFiles();
|
|
1639
|
+
logMsg(`Found ${files.length} files in SPIFFS`);
|
|
1640
|
+
|
|
1641
|
+
// Create a wrapper object that mimics LittleFS interface with full read/write support
|
|
1642
|
+
const spiffsWrapper = {
|
|
1643
|
+
_reader: reader,
|
|
1644
|
+
_files: files,
|
|
1645
|
+
_partition: partition,
|
|
1646
|
+
_config: config,
|
|
1647
|
+
_originalData: data, // Store original image data
|
|
1648
|
+
_modified: false,
|
|
1649
|
+
|
|
1650
|
+
list: function(path = '/') {
|
|
1651
|
+
// Normalize path
|
|
1652
|
+
const normalizedPath = path === '/' ? '' : path.replace(/^\//, '').replace(/\/$/, '');
|
|
1653
|
+
|
|
1654
|
+
// Get all files with proper path property for UI compatibility
|
|
1655
|
+
const allFiles = this._files.map(f => {
|
|
1656
|
+
const fileName = f.name.startsWith('/') ? f.name.substring(1) : f.name;
|
|
1657
|
+
return {
|
|
1658
|
+
name: fileName,
|
|
1659
|
+
path: '/' + fileName, // Add path property for UI
|
|
1660
|
+
type: 'file',
|
|
1661
|
+
size: f.size,
|
|
1662
|
+
_data: f.data
|
|
1663
|
+
};
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
// If root, return all files
|
|
1667
|
+
if (!normalizedPath) {
|
|
1668
|
+
return allFiles;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// Filter by path prefix
|
|
1672
|
+
const prefix = normalizedPath + '/';
|
|
1673
|
+
return allFiles.filter(f => f.name.startsWith(prefix));
|
|
1674
|
+
},
|
|
1675
|
+
|
|
1676
|
+
read: function(path) {
|
|
1677
|
+
const normalizedPath = path.startsWith('/') ? path.substring(1) : path;
|
|
1678
|
+
const file = this._files.find(f => {
|
|
1679
|
+
const fname = f.name.startsWith('/') ? f.name.substring(1) : f.name;
|
|
1680
|
+
return fname === normalizedPath;
|
|
1681
|
+
});
|
|
1682
|
+
return file ? file.data : null;
|
|
1683
|
+
},
|
|
1684
|
+
|
|
1685
|
+
readFile: function(path) {
|
|
1686
|
+
// Alias for read() to match LittleFS interface
|
|
1687
|
+
return this.read(path);
|
|
1688
|
+
},
|
|
1689
|
+
|
|
1690
|
+
write: function(path, data) {
|
|
1691
|
+
// Determine the filename format used in original files
|
|
1692
|
+
// Check if original files have leading slash
|
|
1693
|
+
const hasLeadingSlash = this._files.length > 0 && this._files[0].name.startsWith('/');
|
|
1694
|
+
|
|
1695
|
+
// Normalize path for comparison
|
|
1696
|
+
const normalizedPath = path.startsWith('/') ? path.substring(1) : path;
|
|
1697
|
+
|
|
1698
|
+
// Store filename in the same format as original files
|
|
1699
|
+
const storedName = hasLeadingSlash ? '/' + normalizedPath : normalizedPath;
|
|
1700
|
+
|
|
1701
|
+
// Check if file already exists
|
|
1702
|
+
const existingIndex = this._files.findIndex(f => {
|
|
1703
|
+
const fname = f.name.startsWith('/') ? f.name.substring(1) : f.name;
|
|
1704
|
+
return fname === normalizedPath;
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
// Update or add file
|
|
1708
|
+
if (existingIndex >= 0) {
|
|
1709
|
+
this._files[existingIndex] = {
|
|
1710
|
+
name: storedName,
|
|
1711
|
+
size: data.length,
|
|
1712
|
+
data: data
|
|
1713
|
+
};
|
|
1714
|
+
} else {
|
|
1715
|
+
this._files.push({
|
|
1716
|
+
name: storedName,
|
|
1717
|
+
size: data.length,
|
|
1718
|
+
data: data
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
this._modified = true;
|
|
1723
|
+
},
|
|
1724
|
+
|
|
1725
|
+
writeFile: function(path, data) {
|
|
1726
|
+
// Alias for write() to match LittleFS interface
|
|
1727
|
+
return this.write(path, data);
|
|
1728
|
+
},
|
|
1729
|
+
|
|
1730
|
+
addFile: function(path, data) {
|
|
1731
|
+
// Alias for write() to match alternative interface
|
|
1732
|
+
return this.write(path, data);
|
|
1733
|
+
},
|
|
1734
|
+
|
|
1735
|
+
remove: function(path) {
|
|
1736
|
+
// Normalize path
|
|
1737
|
+
const normalizedPath = path.startsWith('/') ? path.substring(1) : path;
|
|
1738
|
+
|
|
1739
|
+
// Find and remove file
|
|
1740
|
+
const index = this._files.findIndex(f => {
|
|
1741
|
+
const fname = f.name.startsWith('/') ? f.name.substring(1) : f.name;
|
|
1742
|
+
return fname === normalizedPath;
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
if (index >= 0) {
|
|
1746
|
+
this._files.splice(index, 1);
|
|
1747
|
+
this._modified = true;
|
|
1748
|
+
} else {
|
|
1749
|
+
throw new Error(`File not found: ${path}`);
|
|
1750
|
+
}
|
|
1751
|
+
},
|
|
1752
|
+
|
|
1753
|
+
deleteFile: function(path) {
|
|
1754
|
+
// Alias for remove() to match LittleFS interface
|
|
1755
|
+
return this.remove(path);
|
|
1756
|
+
},
|
|
1757
|
+
|
|
1758
|
+
delete: function(path, options) {
|
|
1759
|
+
// For compatibility with LittleFS delete method
|
|
1760
|
+
// SPIFFS doesn't have directories, so just delete the file
|
|
1761
|
+
return this.remove(path);
|
|
1762
|
+
},
|
|
1763
|
+
|
|
1764
|
+
mkdir: function() {
|
|
1765
|
+
throw new Error('SPIFFS does not support directories. Files are stored in a flat structure.');
|
|
1766
|
+
},
|
|
1767
|
+
|
|
1768
|
+
toImage: function() {
|
|
1769
|
+
// If not modified, return original data
|
|
1770
|
+
if (!this._modified) {
|
|
1771
|
+
return this._originalData || new Uint8Array(this._partition.size);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// Create new SPIFFS filesystem with all files
|
|
1775
|
+
const fs = new SpiffsFS(this._partition.size, this._config);
|
|
1776
|
+
|
|
1777
|
+
// Add all files - preserve original filename format
|
|
1778
|
+
for (const file of this._files) {
|
|
1779
|
+
// Use the filename exactly as stored in _files
|
|
1780
|
+
// This preserves whether it has a leading slash or not
|
|
1781
|
+
const fileName = file.name;
|
|
1782
|
+
|
|
1783
|
+
// Log for debugging
|
|
1784
|
+
console.log(`Adding file to SPIFFS: "${fileName}" (${file.data.length} bytes)`);
|
|
1785
|
+
|
|
1786
|
+
fs.createFile(fileName, file.data);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// Generate binary image
|
|
1790
|
+
const image = fs.toBinary();
|
|
1791
|
+
console.log(`Generated SPIFFS image: ${image.length} bytes`);
|
|
1792
|
+
return image;
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
|
|
1796
|
+
// Store filesystem instance
|
|
1797
|
+
currentLittleFS = spiffsWrapper;
|
|
1798
|
+
currentLittleFSPartition = partition;
|
|
1799
|
+
currentLittleFSPath = '/';
|
|
1800
|
+
currentLittleFSBlockSize = config.blockSize;
|
|
1801
|
+
currentFilesystemType = 'spiffs';
|
|
1802
|
+
|
|
1803
|
+
// Update UI
|
|
1804
|
+
littlefsPartitionName.textContent = partition.name;
|
|
1805
|
+
littlefsPartitionSize.textContent = formatSize(partition.size);
|
|
1806
|
+
littlefsDiskVersion.textContent = 'SPIFFS';
|
|
1807
|
+
|
|
1808
|
+
// Show manager
|
|
1809
|
+
littlefsManager.classList.remove('hidden');
|
|
1810
|
+
|
|
1811
|
+
// Enable write operations for SPIFFS (but not mkdir since SPIFFS is flat)
|
|
1812
|
+
butLittlefsUpload.disabled = false;
|
|
1813
|
+
butLittlefsMkdir.disabled = true; // SPIFFS doesn't support directories
|
|
1814
|
+
butLittlefsWrite.disabled = false;
|
|
1815
|
+
|
|
1816
|
+
// Load files
|
|
1817
|
+
refreshLittleFS();
|
|
1818
|
+
|
|
1819
|
+
logMsg('SPIFFS filesystem opened successfully');
|
|
1820
|
+
logMsg('Note: SPIFFS is a flat filesystem - directories are not supported.');
|
|
1821
|
+
} catch (e) {
|
|
1822
|
+
errorMsg(`Failed to open SPIFFS: ${e.message || e}`);
|
|
1823
|
+
console.error('SPIFFS open error:', e);
|
|
1824
|
+
resetLittleFSState();
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* Estimate LittleFS storage footprint for a single file (data + metadata block)
|
|
1830
|
+
*/
|
|
1831
|
+
function littlefsEstimateFileFootprint(size) {
|
|
1832
|
+
const block = currentLittleFSBlockSize || 4096;
|
|
1833
|
+
const dataBytes = Math.max(1, Math.ceil(size / block)) * block;
|
|
1834
|
+
const metadataBytes = block; // per-file metadata block
|
|
1835
|
+
return dataBytes + metadataBytes;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/**
|
|
1839
|
+
* Estimate total LittleFS usage for a set of entries
|
|
1840
|
+
*/
|
|
1841
|
+
function littlefsEstimateUsage(entries) {
|
|
1842
|
+
const block = currentLittleFSBlockSize || 4096;
|
|
1843
|
+
let total = block * 2; // root metadata copies
|
|
1844
|
+
|
|
1845
|
+
for (const entry of entries || []) {
|
|
1846
|
+
if (entry.type === 'dir') {
|
|
1847
|
+
total += block;
|
|
1848
|
+
} else {
|
|
1849
|
+
total += littlefsEstimateFileFootprint(entry.size || 0);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
return total;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/**
|
|
1857
|
+
* Refresh LittleFS file list
|
|
1858
|
+
*/
|
|
1859
|
+
function refreshLittleFS() {
|
|
1860
|
+
if (!currentLittleFS) return;
|
|
1861
|
+
|
|
1862
|
+
try {
|
|
1863
|
+
// Calculate usage based on all files (like ESPConnect)
|
|
1864
|
+
const allFiles = currentLittleFS.list('/');
|
|
1865
|
+
const usedBytes = littlefsEstimateUsage(allFiles);
|
|
1866
|
+
const totalBytes = currentLittleFSPartition.size;
|
|
1867
|
+
const usedPercent = Math.round((usedBytes / totalBytes) * 100);
|
|
1868
|
+
|
|
1869
|
+
littlefsUsageBar.style.width = usedPercent + '%';
|
|
1870
|
+
littlefsUsageText.textContent = `Used: ${formatSize(usedBytes)} / ${formatSize(totalBytes)} (${usedPercent}%)`;
|
|
1871
|
+
|
|
1872
|
+
// Update breadcrumb
|
|
1873
|
+
littlefsBreadcrumb.textContent = currentLittleFSPath || '/';
|
|
1874
|
+
butLittlefsUp.disabled = currentLittleFSPath === '/' || !currentLittleFSPath;
|
|
1875
|
+
|
|
1876
|
+
// List files
|
|
1877
|
+
const entries = currentLittleFS.list(currentLittleFSPath);
|
|
1878
|
+
|
|
1879
|
+
// Clear table
|
|
1880
|
+
littlefsFileList.innerHTML = '';
|
|
1881
|
+
|
|
1882
|
+
if (entries.length === 0) {
|
|
1883
|
+
const row = document.createElement('tr');
|
|
1884
|
+
row.innerHTML = '<td colspan="4" class="empty-state">No files in this directory</td>';
|
|
1885
|
+
littlefsFileList.appendChild(row);
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// Sort: directories first, then files
|
|
1890
|
+
entries.sort((a, b) => {
|
|
1891
|
+
if (a.type === 'dir' && b.type !== 'dir') return -1;
|
|
1892
|
+
if (a.type !== 'dir' && b.type === 'dir') return 1;
|
|
1893
|
+
return a.path.localeCompare(b.path);
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
// Add rows
|
|
1897
|
+
entries.forEach(entry => {
|
|
1898
|
+
const row = document.createElement('tr');
|
|
1899
|
+
|
|
1900
|
+
// Name
|
|
1901
|
+
const nameCell = document.createElement('td');
|
|
1902
|
+
const nameDiv = document.createElement('div');
|
|
1903
|
+
nameDiv.className = 'file-name' + (entry.type === 'dir' ? ' clickable' : '');
|
|
1904
|
+
|
|
1905
|
+
const icon = document.createElement('span');
|
|
1906
|
+
icon.className = 'file-icon';
|
|
1907
|
+
icon.textContent = entry.type === 'dir' ? '📁' : '📄';
|
|
1908
|
+
|
|
1909
|
+
const name = entry.path.split('/').filter(Boolean).pop() || '/';
|
|
1910
|
+
const nameText = document.createElement('span');
|
|
1911
|
+
nameText.textContent = name;
|
|
1912
|
+
|
|
1913
|
+
nameDiv.appendChild(icon);
|
|
1914
|
+
nameDiv.appendChild(nameText);
|
|
1915
|
+
|
|
1916
|
+
if (entry.type === 'dir') {
|
|
1917
|
+
nameDiv.onclick = () => navigateLittleFS(entry.path);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
nameCell.appendChild(nameDiv);
|
|
1921
|
+
row.appendChild(nameCell);
|
|
1922
|
+
|
|
1923
|
+
// Type
|
|
1924
|
+
const typeCell = document.createElement('td');
|
|
1925
|
+
typeCell.textContent = entry.type === 'dir' ? 'Directory' : 'File';
|
|
1926
|
+
row.appendChild(typeCell);
|
|
1927
|
+
|
|
1928
|
+
// Size
|
|
1929
|
+
const sizeCell = document.createElement('td');
|
|
1930
|
+
sizeCell.textContent = entry.type === 'file' ? formatSize(entry.size) : '-';
|
|
1931
|
+
row.appendChild(sizeCell);
|
|
1932
|
+
|
|
1933
|
+
// Actions
|
|
1934
|
+
const actionsCell = document.createElement('td');
|
|
1935
|
+
const actionsDiv = document.createElement('div');
|
|
1936
|
+
actionsDiv.className = 'file-actions';
|
|
1937
|
+
|
|
1938
|
+
if (entry.type === 'file') {
|
|
1939
|
+
const downloadBtn = document.createElement('button');
|
|
1940
|
+
downloadBtn.textContent = 'Download';
|
|
1941
|
+
downloadBtn.onclick = () => downloadLittleFSFile(entry.path);
|
|
1942
|
+
actionsDiv.appendChild(downloadBtn);
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
const deleteBtn = document.createElement('button');
|
|
1946
|
+
deleteBtn.textContent = 'Delete';
|
|
1947
|
+
deleteBtn.className = 'delete-btn';
|
|
1948
|
+
deleteBtn.onclick = () => deleteLittleFSFile(entry.path, entry.type);
|
|
1949
|
+
actionsDiv.appendChild(deleteBtn);
|
|
1950
|
+
|
|
1951
|
+
actionsCell.appendChild(actionsDiv);
|
|
1952
|
+
row.appendChild(actionsCell);
|
|
1953
|
+
|
|
1954
|
+
littlefsFileList.appendChild(row);
|
|
1955
|
+
});
|
|
1956
|
+
} catch (e) {
|
|
1957
|
+
errorMsg(`Failed to refresh file list: ${e.message || e}`);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
/**
|
|
1962
|
+
* Navigate to a directory in LittleFS
|
|
1963
|
+
*/
|
|
1964
|
+
function navigateLittleFS(path) {
|
|
1965
|
+
currentLittleFSPath = path;
|
|
1966
|
+
refreshLittleFS();
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* Navigate up one directory
|
|
1971
|
+
*/
|
|
1972
|
+
function clickLittlefsUp() {
|
|
1973
|
+
if (currentLittleFSPath === '/' || !currentLittleFSPath) return;
|
|
1974
|
+
|
|
1975
|
+
const parts = currentLittleFSPath.split('/').filter(Boolean);
|
|
1976
|
+
parts.pop();
|
|
1977
|
+
currentLittleFSPath = '/' + parts.join('/');
|
|
1978
|
+
if (currentLittleFSPath !== '/' && !currentLittleFSPath.endsWith('/')) {
|
|
1979
|
+
currentLittleFSPath += '/';
|
|
1980
|
+
}
|
|
1981
|
+
refreshLittleFS();
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
/**
|
|
1985
|
+
* Refresh button handler
|
|
1986
|
+
*/
|
|
1987
|
+
function clickLittlefsRefresh() {
|
|
1988
|
+
refreshLittleFS();
|
|
1989
|
+
logMsg(`${getFilesystemDisplayName()} file list refreshed`);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
/**
|
|
1993
|
+
* Backup LittleFS image
|
|
1994
|
+
*/
|
|
1995
|
+
async function clickLittlefsBackup() {
|
|
1996
|
+
if (!currentLittleFS || !currentLittleFSPartition) return;
|
|
1997
|
+
|
|
1998
|
+
try {
|
|
1999
|
+
logMsg(`Creating ${getFilesystemDisplayName()} backup image...`);
|
|
2000
|
+
const image = currentLittleFS.toImage();
|
|
2001
|
+
|
|
2002
|
+
const fsType = currentFilesystemType || 'filesystem';
|
|
2003
|
+
const filename = `${currentLittleFSPartition.name}_${fsType}_backup.bin`;
|
|
2004
|
+
await saveDataToFile(image, filename);
|
|
2005
|
+
|
|
2006
|
+
logMsg(`${getFilesystemDisplayName()} backup saved as "${filename}"`);
|
|
2007
|
+
} catch (e) {
|
|
2008
|
+
errorMsg(`Failed to backup ${getFilesystemDisplayName()}: ${e.message || e}`);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
/**
|
|
2013
|
+
* Write LittleFS image to flash
|
|
2014
|
+
*/
|
|
2015
|
+
async function clickLittlefsWrite() {
|
|
2016
|
+
if (!currentLittleFS || !currentLittleFSPartition) return;
|
|
2017
|
+
|
|
2018
|
+
const confirmed = confirm(
|
|
2019
|
+
`Write modified filesystem to flash?\n\n` +
|
|
2020
|
+
`Partition: ${currentLittleFSPartition.name}\n` +
|
|
2021
|
+
`Offset: 0x${currentLittleFSPartition.offset.toString(16)}\n` +
|
|
2022
|
+
`Size: ${formatSize(currentLittleFSPartition.size)}\n\n` +
|
|
2023
|
+
`This will overwrite the current filesystem on the device!`
|
|
2024
|
+
);
|
|
2025
|
+
|
|
2026
|
+
if (!confirmed) return;
|
|
2027
|
+
|
|
2028
|
+
try {
|
|
2029
|
+
logMsg(`Creating ${getFilesystemDisplayName()} image...`);
|
|
2030
|
+
const image = currentLittleFS.toImage();
|
|
2031
|
+
logMsg(`Image created: ${formatSize(image.length)}`);
|
|
2032
|
+
|
|
2033
|
+
if (image.length > currentLittleFSPartition.size) {
|
|
2034
|
+
errorMsg(`Image size (${formatSize(image.length)}) exceeds partition size (${formatSize(currentLittleFSPartition.size)})`);
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// Disable buttons during write
|
|
2039
|
+
butLittlefsRefresh.disabled = true;
|
|
2040
|
+
butLittlefsBackup.disabled = true;
|
|
2041
|
+
butLittlefsWrite.disabled = true;
|
|
2042
|
+
butLittlefsClose.disabled = true;
|
|
2043
|
+
butLittlefsUpload.disabled = true;
|
|
2044
|
+
butLittlefsMkdir.disabled = true;
|
|
2045
|
+
|
|
2046
|
+
logMsg(`Writing ${formatSize(image.length)} to partition "${currentLittleFSPartition.name}" at 0x${currentLittleFSPartition.offset.toString(16)}...`);
|
|
2047
|
+
|
|
2048
|
+
// Use the LittleFS usage bar as progress indicator
|
|
2049
|
+
const usageBar = document.getElementById("littlefsUsageBar");
|
|
2050
|
+
const usageText = document.getElementById("littlefsUsageText");
|
|
2051
|
+
const originalUsageBarWidth = usageBar.style.width;
|
|
2052
|
+
const originalUsageText = usageText.textContent;
|
|
2053
|
+
|
|
2054
|
+
// Convert Uint8Array to ArrayBuffer (CRITICAL: flashData expects ArrayBuffer, not Uint8Array)
|
|
2055
|
+
// This matches the ESPConnect implementation
|
|
2056
|
+
const imageBuffer = image.buffer.slice(image.byteOffset, image.byteOffset + image.byteLength);
|
|
2057
|
+
|
|
2058
|
+
// Write the image to flash with progress indication
|
|
2059
|
+
await espStub.flashData(
|
|
2060
|
+
imageBuffer,
|
|
2061
|
+
(bytesWritten, totalBytes) => {
|
|
2062
|
+
const percent = Math.floor((bytesWritten / totalBytes) * 100);
|
|
2063
|
+
usageBar.style.width = percent + "%";
|
|
2064
|
+
usageText.textContent = `Writing: ${formatSize(bytesWritten)} / ${formatSize(totalBytes)} (${percent}%)`;
|
|
2065
|
+
},
|
|
2066
|
+
currentLittleFSPartition.offset
|
|
2067
|
+
);
|
|
2068
|
+
|
|
2069
|
+
// Restore original usage display
|
|
2070
|
+
usageBar.style.width = originalUsageBarWidth;
|
|
2071
|
+
usageText.textContent = originalUsageText;
|
|
2072
|
+
|
|
2073
|
+
logMsg(`${getFilesystemDisplayName()} successfully written to flash!`);
|
|
2074
|
+
logMsg(`To use the new filesystem, reset your device.`);
|
|
2075
|
+
|
|
2076
|
+
} catch (e) {
|
|
2077
|
+
errorMsg(`Failed to write ${getFilesystemDisplayName()} to flash: ${e.message || e}`);
|
|
2078
|
+
} finally {
|
|
2079
|
+
// Re-enable buttons
|
|
2080
|
+
butLittlefsRefresh.disabled = false;
|
|
2081
|
+
butLittlefsBackup.disabled = false;
|
|
2082
|
+
butLittlefsWrite.disabled = false;
|
|
2083
|
+
butLittlefsClose.disabled = false;
|
|
2084
|
+
butLittlefsUpload.disabled = !littlefsFileInput.files.length;
|
|
2085
|
+
butLittlefsMkdir.disabled = false;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
/**
|
|
2090
|
+
* Close LittleFS manager
|
|
2091
|
+
*/
|
|
2092
|
+
function clickLittlefsClose() {
|
|
2093
|
+
const fsName = getFilesystemDisplayName() || 'Filesystem';
|
|
2094
|
+
|
|
2095
|
+
if (currentLittleFS) {
|
|
2096
|
+
try {
|
|
2097
|
+
// Only call destroy if it exists (LittleFS has it, FatFS/SPIFFS don't)
|
|
2098
|
+
if (typeof currentLittleFS.destroy === 'function') {
|
|
2099
|
+
currentLittleFS.destroy();
|
|
2100
|
+
}
|
|
2101
|
+
} catch (e) {
|
|
2102
|
+
console.error(`Error destroying ${fsName}:`, e);
|
|
2103
|
+
}
|
|
2104
|
+
currentLittleFS = null;
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
currentLittleFSPartition = null;
|
|
2108
|
+
currentLittleFSPath = '/';
|
|
2109
|
+
currentFilesystemType = null;
|
|
2110
|
+
littlefsManager.classList.add('hidden');
|
|
2111
|
+
logMsg(`${fsName} manager closed`);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
/**
|
|
2115
|
+
* Upload file to LittleFS
|
|
2116
|
+
*/
|
|
2117
|
+
async function clickLittlefsUpload() {
|
|
2118
|
+
if (!currentLittleFS || !littlefsFileInput.files.length) return;
|
|
2119
|
+
|
|
2120
|
+
const file = littlefsFileInput.files[0];
|
|
2121
|
+
|
|
2122
|
+
try {
|
|
2123
|
+
logMsg(`Uploading file "${file.name}"...`);
|
|
2124
|
+
|
|
2125
|
+
const data = await file.arrayBuffer();
|
|
2126
|
+
const uint8Data = new Uint8Array(data);
|
|
2127
|
+
|
|
2128
|
+
// Construct target path
|
|
2129
|
+
let targetPath = currentLittleFSPath;
|
|
2130
|
+
if (!targetPath.endsWith('/')) targetPath += '/';
|
|
2131
|
+
targetPath += file.name;
|
|
2132
|
+
|
|
2133
|
+
// Ensure parent directories exist
|
|
2134
|
+
const segments = targetPath.split('/').filter(Boolean);
|
|
2135
|
+
if (segments.length > 1) {
|
|
2136
|
+
let built = '';
|
|
2137
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
2138
|
+
built += `/${segments[i]}`;
|
|
2139
|
+
try {
|
|
2140
|
+
currentLittleFS.mkdir(built);
|
|
2141
|
+
} catch (e) {
|
|
2142
|
+
// Ignore if directory already exists
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
// Write file to LittleFS - EXACTLY like ESPConnect
|
|
2148
|
+
if (typeof currentLittleFS.writeFile === 'function') {
|
|
2149
|
+
currentLittleFS.writeFile(targetPath, uint8Data);
|
|
2150
|
+
} else if (typeof currentLittleFS.addFile === 'function') {
|
|
2151
|
+
currentLittleFS.addFile(targetPath, uint8Data);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// Verify by reading back
|
|
2155
|
+
const readBack = currentLittleFS.readFile(targetPath);
|
|
2156
|
+
logMsg(`File written: ${readBack.length} bytes at ${targetPath}`);
|
|
2157
|
+
|
|
2158
|
+
// Clear input
|
|
2159
|
+
littlefsFileInput.value = '';
|
|
2160
|
+
butLittlefsUpload.disabled = true;
|
|
2161
|
+
|
|
2162
|
+
// Refresh list
|
|
2163
|
+
refreshLittleFS();
|
|
2164
|
+
|
|
2165
|
+
logMsg(`File "${file.name}" uploaded successfully`);
|
|
2166
|
+
} catch (e) {
|
|
2167
|
+
errorMsg(`Failed to upload file: ${e.message || e}`);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
/**
|
|
2172
|
+
* Create new directory
|
|
2173
|
+
*/
|
|
2174
|
+
function clickLittlefsMkdir() {
|
|
2175
|
+
if (!currentLittleFS) return;
|
|
2176
|
+
|
|
2177
|
+
const dirName = prompt('Enter directory name:');
|
|
2178
|
+
if (!dirName || !dirName.trim()) return;
|
|
2179
|
+
|
|
2180
|
+
try {
|
|
2181
|
+
let targetPath = currentLittleFSPath;
|
|
2182
|
+
if (!targetPath.endsWith('/')) targetPath += '/';
|
|
2183
|
+
targetPath += dirName.trim();
|
|
2184
|
+
|
|
2185
|
+
currentLittleFS.mkdir(targetPath);
|
|
2186
|
+
refreshLittleFS();
|
|
2187
|
+
|
|
2188
|
+
logMsg(`Directory "${dirName}" created successfully`);
|
|
2189
|
+
} catch (e) {
|
|
2190
|
+
errorMsg(`Failed to create directory: ${e.message || e}`);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
/**
|
|
2195
|
+
* Download file from LittleFS
|
|
2196
|
+
*/
|
|
2197
|
+
async function downloadLittleFSFile(path) {
|
|
2198
|
+
if (!currentLittleFS) return;
|
|
2199
|
+
|
|
2200
|
+
try {
|
|
2201
|
+
logMsg(`Downloading file "${path}"...`);
|
|
2202
|
+
|
|
2203
|
+
const data = currentLittleFS.readFile(path);
|
|
2204
|
+
const filename = path.split('/').filter(Boolean).pop() || 'file.bin';
|
|
2205
|
+
|
|
2206
|
+
await saveDataToFile(data, filename);
|
|
2207
|
+
|
|
2208
|
+
logMsg(`File "${filename}" downloaded successfully`);
|
|
2209
|
+
} catch (e) {
|
|
2210
|
+
errorMsg(`Failed to download file: ${e.message || e}`);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
/**
|
|
2215
|
+
* Delete file or directory from LittleFS
|
|
2216
|
+
*/
|
|
2217
|
+
function deleteLittleFSFile(path, type) {
|
|
2218
|
+
if (!currentLittleFS) return;
|
|
2219
|
+
|
|
2220
|
+
const name = path.split('/').filter(Boolean).pop() || path;
|
|
2221
|
+
const confirmed = confirm(`Delete ${type} "${name}"?`);
|
|
2222
|
+
|
|
2223
|
+
if (!confirmed) return;
|
|
2224
|
+
|
|
2225
|
+
try {
|
|
2226
|
+
if (type === 'dir') {
|
|
2227
|
+
currentLittleFS.delete(path, { recursive: true });
|
|
2228
|
+
} else {
|
|
2229
|
+
currentLittleFS.deleteFile(path);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
refreshLittleFS();
|
|
2233
|
+
logMsg(`${type === 'dir' ? 'Directory' : 'File'} "${name}" deleted successfully`);
|
|
2234
|
+
} catch (e) {
|
|
2235
|
+
errorMsg(`Failed to delete ${type}: ${e.message || e}`);
|
|
2236
|
+
}
|
|
2237
|
+
}
|