tasmota-esp-web-tools 9.0.2 → 10.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/dist/components/ewt-littlefs-manager.d.ts +43 -0
- package/dist/components/ewt-littlefs-manager.js +819 -0
- package/dist/flash.js +4 -30
- package/dist/install-dialog.d.ts +11 -0
- package/dist/install-dialog.js +336 -0
- package/dist/partition.d.ts +26 -0
- package/dist/partition.js +129 -0
- package/dist/util/esp32s2-reconnect.d.ts +22 -0
- package/dist/util/esp32s2-reconnect.js +45 -0
- package/dist/util/partition.d.ts +4 -0
- package/dist/util/partition.js +59 -0
- package/{js/modules/index-BK6drzlv.js → dist/web/index-t2Vsxqjj.js} +1 -1
- package/dist/web/install-button.js +1 -1
- package/dist/web/{install-dialog-Xo7JovVA.js → install-dialog-CVebVk1R.js} +515 -36
- package/dist/web/{styles-ntwzvT31.js → styles-CWxbqh-J.js} +1 -1
- package/dist/web/wasm/littlefs/index.d.ts +91 -0
- package/dist/web/wasm/littlefs/index.js +611 -0
- package/dist/web/wasm/littlefs/littlefs.js +505 -0
- package/dist/web/wasm/littlefs/littlefs.wasm +0 -0
- package/{dist/web/index-BK6drzlv.js → js/modules/index-t2Vsxqjj.js} +1 -1
- package/js/modules/install-button.js +1 -1
- package/js/modules/{install-dialog-Xo7JovVA.js → install-dialog-CVebVk1R.js} +515 -36
- package/js/modules/{styles-ntwzvT31.js → styles-CWxbqh-J.js} +1 -1
- package/js/modules/wasm/littlefs/index.d.ts +91 -0
- package/js/modules/wasm/littlefs/index.js +611 -0
- package/js/modules/wasm/littlefs/littlefs.js +505 -0
- package/js/modules/wasm/littlefs/littlefs.wasm +0 -0
- package/package.json +3 -2
- package/.devcontainer/Dockerfile +0 -16
- package/.devcontainer/devcontainer.json +0 -21
- package/.github/dependabot.yml +0 -10
- package/.github/workflows/build_upload.yml +0 -78
- package/.github/workflows/ci.yml +0 -24
- package/.prettierignore +0 -1
- package/BAUD_RATE_CONFIGURATION.md +0 -291
- package/BAUD_RATE_IMPROVEMENT.md +0 -195
- package/CHANGELOG_IMPROVEMENTS.md +0 -246
- package/ESP32_S2_USB_RECONNECT.md +0 -108
- package/FINAL_SUMMARY.md +0 -153
- package/example-baud-rate.html +0 -276
- package/manifest-example-p4-variants.json +0 -61
- package/rollup.config.mjs +0 -36
- package/script/build +0 -8
- package/script/develop +0 -17
- package/src/components/ewt-button.ts +0 -25
- package/src/components/ewt-checkbox.ts +0 -14
- package/src/components/ewt-circular-progress.ts +0 -14
- package/src/components/ewt-console.ts +0 -166
- package/src/components/ewt-dialog.ts +0 -22
- package/src/components/ewt-formfield.ts +0 -14
- package/src/components/ewt-icon-button.ts +0 -14
- package/src/components/ewt-list-item.ts +0 -14
- package/src/components/ewt-select.ts +0 -23
- package/src/components/ewt-textfield.ts +0 -23
- package/src/components/svg.ts +0 -27
- package/src/connect.ts +0 -55
- package/src/const.ts +0 -122
- package/src/flash.ts +0 -361
- package/src/install-button.ts +0 -126
- package/src/install-dialog.ts +0 -1114
- package/src/no-port-picked/index.ts +0 -10
- package/src/no-port-picked/no-port-picked-dialog.ts +0 -167
- package/src/pages/ewt-page-message.ts +0 -39
- package/src/pages/ewt-page-progress.ts +0 -44
- package/src/styles.ts +0 -34
- package/src/util/chip-family-name.ts +0 -46
- package/src/util/console-color.ts +0 -284
- package/src/util/file-download.ts +0 -17
- package/src/util/fire-event.ts +0 -20
- package/src/util/line-break-transformer.ts +0 -20
- package/src/util/manifest.ts +0 -18
- package/src/util/sleep.ts +0 -2
- package/static/logos/2smart.png +0 -0
- package/static/logos/canairio.png +0 -0
- package/static/logos/clockwise.png +0 -0
- package/static/logos/espeasy.png +0 -0
- package/static/logos/esphome.svg +0 -1
- package/static/logos/luciferin_logo.png +0 -0
- package/static/logos/squeezelite-esp32.png +0 -0
- package/static/logos/tasmota.svg +0 -1
- package/static/logos/wled.png +0 -0
- package/static/screenshots/dashboard.png +0 -0
- package/static/screenshots/logs.png +0 -0
- package/static/social.png +0 -0
- package/tsconfig.json +0 -20
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { LitElement, html, css } from "lit";
|
|
3
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
4
|
+
import "./ewt-button";
|
|
5
|
+
import "./ewt-textfield";
|
|
6
|
+
// Dynamic import for LittleFS WASM module
|
|
7
|
+
let _wasmBasePath = null;
|
|
8
|
+
let _littleFSModule = null;
|
|
9
|
+
async function loadLittleFS() {
|
|
10
|
+
// Cache the module to avoid reloading
|
|
11
|
+
if (_littleFSModule) {
|
|
12
|
+
return _littleFSModule;
|
|
13
|
+
}
|
|
14
|
+
// Determine WASM base path from the current script location
|
|
15
|
+
if (!_wasmBasePath) {
|
|
16
|
+
const scriptUrl = new URL(import.meta.url);
|
|
17
|
+
// Remove the filename to get the directory
|
|
18
|
+
const scriptDir = scriptUrl.href.substring(0, scriptUrl.href.lastIndexOf("/") + 1);
|
|
19
|
+
_wasmBasePath = scriptDir + "wasm/littlefs/";
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
// Try to import from the calculated path
|
|
23
|
+
const indexUrl = _wasmBasePath + "index.js";
|
|
24
|
+
console.log("[LittleFS] Loading module from:", indexUrl);
|
|
25
|
+
_littleFSModule = await import(/* @vite-ignore */ indexUrl);
|
|
26
|
+
return _littleFSModule;
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
console.error("[LittleFS] Failed to load from calculated path:", _wasmBasePath, err);
|
|
30
|
+
// Fallback to relative import (for local development)
|
|
31
|
+
try {
|
|
32
|
+
_littleFSModule = await import("../wasm/littlefs/index.js");
|
|
33
|
+
return _littleFSModule;
|
|
34
|
+
}
|
|
35
|
+
catch (fallbackErr) {
|
|
36
|
+
console.error("[LittleFS] Fallback import also failed:", fallbackErr);
|
|
37
|
+
throw new Error(`Failed to load LittleFS module: ${err}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let EwtLittleFSManager = class EwtLittleFSManager extends LitElement {
|
|
42
|
+
constructor() {
|
|
43
|
+
super(...arguments);
|
|
44
|
+
this.logger = console;
|
|
45
|
+
this._currentPath = "/";
|
|
46
|
+
this._files = [];
|
|
47
|
+
this._fs = null;
|
|
48
|
+
this._blockSize = 4096;
|
|
49
|
+
this._usage = { capacityBytes: 0, usedBytes: 0, freeBytes: 0 };
|
|
50
|
+
this._diskVersion = "";
|
|
51
|
+
this._busy = false;
|
|
52
|
+
this._selectedFile = null;
|
|
53
|
+
this._flashProgress = 0; // 0-100 for flash progress, -1 when not flashing
|
|
54
|
+
this._isFlashing = false;
|
|
55
|
+
}
|
|
56
|
+
async connectedCallback() {
|
|
57
|
+
super.connectedCallback();
|
|
58
|
+
await this._openFilesystem();
|
|
59
|
+
}
|
|
60
|
+
disconnectedCallback() {
|
|
61
|
+
super.disconnectedCallback();
|
|
62
|
+
this._cleanup();
|
|
63
|
+
}
|
|
64
|
+
async _openFilesystem() {
|
|
65
|
+
try {
|
|
66
|
+
this._busy = true;
|
|
67
|
+
this.logger.log(`Reading LittleFS partition "${this.partition.name}" (${this._formatSize(this.partition.size)})...`);
|
|
68
|
+
// Read entire partition
|
|
69
|
+
const data = await this.espStub.readFlash(this.partition.offset, this.partition.size);
|
|
70
|
+
this.logger.log("Mounting LittleFS filesystem...");
|
|
71
|
+
// Load LittleFS module dynamically
|
|
72
|
+
const { createLittleFSFromImage, formatDiskVersion } = await loadLittleFS();
|
|
73
|
+
// Try to mount with different block sizes
|
|
74
|
+
const blockSizes = [4096, 2048, 1024, 512];
|
|
75
|
+
let fs = null;
|
|
76
|
+
let blockSize = 0;
|
|
77
|
+
for (const bs of blockSizes) {
|
|
78
|
+
try {
|
|
79
|
+
const blockCount = Math.floor(this.partition.size / bs);
|
|
80
|
+
// Pass WASM URL if available
|
|
81
|
+
const options = {
|
|
82
|
+
blockSize: bs,
|
|
83
|
+
blockCount: blockCount,
|
|
84
|
+
};
|
|
85
|
+
if (_wasmBasePath) {
|
|
86
|
+
options.wasmURL = new URL("littlefs.wasm", _wasmBasePath).href;
|
|
87
|
+
}
|
|
88
|
+
fs = await createLittleFSFromImage(data, options);
|
|
89
|
+
// Try to list root to verify it works
|
|
90
|
+
fs.list("/");
|
|
91
|
+
blockSize = bs;
|
|
92
|
+
this.logger.log(`Successfully mounted LittleFS with block size ${bs}`);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
// Try next block size
|
|
97
|
+
fs = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!fs) {
|
|
101
|
+
throw new Error("Failed to mount LittleFS with any block size");
|
|
102
|
+
}
|
|
103
|
+
this._fs = fs;
|
|
104
|
+
this._blockSize = blockSize;
|
|
105
|
+
// Get disk version
|
|
106
|
+
try {
|
|
107
|
+
const diskVer = fs.getDiskVersion();
|
|
108
|
+
this._diskVersion = formatDiskVersion(diskVer);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
this._diskVersion = "";
|
|
112
|
+
}
|
|
113
|
+
this._refreshFiles();
|
|
114
|
+
this.logger.log("LittleFS filesystem opened successfully");
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
this.logger.error(`Failed to open LittleFS: ${e.message || e}`);
|
|
118
|
+
if (this.onClose) {
|
|
119
|
+
this.onClose();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
this._busy = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
_refreshFiles() {
|
|
127
|
+
if (!this._fs)
|
|
128
|
+
return;
|
|
129
|
+
try {
|
|
130
|
+
// Calculate usage
|
|
131
|
+
const allFiles = this._fs.list("/");
|
|
132
|
+
const usedBytes = this._estimateUsage(allFiles);
|
|
133
|
+
const totalBytes = this.partition.size;
|
|
134
|
+
this._usage = {
|
|
135
|
+
capacityBytes: totalBytes,
|
|
136
|
+
usedBytes: usedBytes,
|
|
137
|
+
freeBytes: totalBytes - usedBytes,
|
|
138
|
+
};
|
|
139
|
+
// List files in current directory
|
|
140
|
+
const entries = this._fs.list(this._currentPath);
|
|
141
|
+
// Sort: directories first, then files
|
|
142
|
+
entries.sort((a, b) => {
|
|
143
|
+
if (a.type === "dir" && b.type !== "dir")
|
|
144
|
+
return -1;
|
|
145
|
+
if (a.type !== "dir" && b.type === "dir")
|
|
146
|
+
return 1;
|
|
147
|
+
return a.path.localeCompare(b.path);
|
|
148
|
+
});
|
|
149
|
+
this._files = entries;
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
this.logger.error(`Failed to refresh file list: ${e.message || e}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
_estimateUsage(entries) {
|
|
156
|
+
const block = this._blockSize || 4096;
|
|
157
|
+
let total = block * 2; // root metadata copies
|
|
158
|
+
for (const entry of entries || []) {
|
|
159
|
+
if (entry.type === "dir") {
|
|
160
|
+
total += block;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const dataBytes = Math.max(1, Math.ceil((entry.size || 0) / block)) * block;
|
|
164
|
+
const metadataBytes = block;
|
|
165
|
+
total += dataBytes + metadataBytes;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return total;
|
|
169
|
+
}
|
|
170
|
+
_formatSize(bytes) {
|
|
171
|
+
if (bytes < 1024) {
|
|
172
|
+
return `${bytes} B`;
|
|
173
|
+
}
|
|
174
|
+
else if (bytes < 1024 * 1024) {
|
|
175
|
+
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
_navigateUp() {
|
|
182
|
+
if (this._currentPath === "/" || !this._currentPath)
|
|
183
|
+
return;
|
|
184
|
+
const parts = this._currentPath.split("/").filter(Boolean);
|
|
185
|
+
parts.pop();
|
|
186
|
+
this._currentPath = "/" + parts.join("/");
|
|
187
|
+
if (this._currentPath !== "/" && !this._currentPath.endsWith("/")) {
|
|
188
|
+
this._currentPath += "/";
|
|
189
|
+
}
|
|
190
|
+
this._refreshFiles();
|
|
191
|
+
}
|
|
192
|
+
_navigateTo(path) {
|
|
193
|
+
this._currentPath = path;
|
|
194
|
+
this._refreshFiles();
|
|
195
|
+
}
|
|
196
|
+
async _uploadFile() {
|
|
197
|
+
if (!this._fs || !this._selectedFile)
|
|
198
|
+
return;
|
|
199
|
+
try {
|
|
200
|
+
this._busy = true;
|
|
201
|
+
this.logger.log(`Uploading file "${this._selectedFile.name}"...`);
|
|
202
|
+
const data = await this._selectedFile.arrayBuffer();
|
|
203
|
+
const uint8Data = new Uint8Array(data);
|
|
204
|
+
// Construct target path
|
|
205
|
+
let targetPath = this._currentPath;
|
|
206
|
+
if (!targetPath.endsWith("/"))
|
|
207
|
+
targetPath += "/";
|
|
208
|
+
targetPath += this._selectedFile.name;
|
|
209
|
+
// Ensure parent directories exist
|
|
210
|
+
const segments = targetPath.split("/").filter(Boolean);
|
|
211
|
+
if (segments.length > 1) {
|
|
212
|
+
let built = "";
|
|
213
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
214
|
+
built += `/${segments[i]}`;
|
|
215
|
+
try {
|
|
216
|
+
this._fs.mkdir(built);
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
// Ignore if directory already exists
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Write file
|
|
224
|
+
if (typeof this._fs.writeFile === "function") {
|
|
225
|
+
this._fs.writeFile(targetPath, uint8Data);
|
|
226
|
+
}
|
|
227
|
+
else if (typeof this._fs.addFile === "function") {
|
|
228
|
+
this._fs.addFile(targetPath, uint8Data);
|
|
229
|
+
}
|
|
230
|
+
// Verify by reading back
|
|
231
|
+
const readBack = this._fs.readFile(targetPath);
|
|
232
|
+
this.logger.log(`✓ File written: ${readBack.length} bytes at ${targetPath}`);
|
|
233
|
+
// Clear input
|
|
234
|
+
const uploadedFileName = this._selectedFile.name;
|
|
235
|
+
this._selectedFile = null;
|
|
236
|
+
this._refreshFiles();
|
|
237
|
+
this.logger.log(`File "${uploadedFileName}" uploaded successfully`);
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
this.logger.error(`Failed to upload file: ${e.message || e}`);
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
this._busy = false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
_createFolder() {
|
|
247
|
+
if (!this._fs)
|
|
248
|
+
return;
|
|
249
|
+
const dirName = prompt("Enter directory name:");
|
|
250
|
+
if (!dirName || !dirName.trim())
|
|
251
|
+
return;
|
|
252
|
+
try {
|
|
253
|
+
let targetPath = this._currentPath;
|
|
254
|
+
if (!targetPath.endsWith("/"))
|
|
255
|
+
targetPath += "/";
|
|
256
|
+
targetPath += dirName.trim();
|
|
257
|
+
this._fs.mkdir(targetPath);
|
|
258
|
+
this._refreshFiles();
|
|
259
|
+
this.logger.log(`Directory "${dirName}" created successfully`);
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
this.logger.error(`Failed to create directory: ${e.message || e}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async _downloadFile(path) {
|
|
266
|
+
if (!this._fs)
|
|
267
|
+
return;
|
|
268
|
+
try {
|
|
269
|
+
this.logger.log(`Downloading file "${path}"...`);
|
|
270
|
+
const data = this._fs.readFile(path);
|
|
271
|
+
const filename = path.split("/").filter(Boolean).pop() || "file.bin";
|
|
272
|
+
// Create download
|
|
273
|
+
const blob = new Blob([data], { type: "application/octet-stream" });
|
|
274
|
+
const url = URL.createObjectURL(blob);
|
|
275
|
+
const a = document.createElement("a");
|
|
276
|
+
a.href = url;
|
|
277
|
+
a.download = filename;
|
|
278
|
+
document.body.appendChild(a);
|
|
279
|
+
a.click();
|
|
280
|
+
document.body.removeChild(a);
|
|
281
|
+
URL.revokeObjectURL(url);
|
|
282
|
+
this.logger.log(`File "${filename}" downloaded successfully`);
|
|
283
|
+
}
|
|
284
|
+
catch (e) {
|
|
285
|
+
this.logger.error(`Failed to download file: ${e.message || e}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
_deleteFile(path, type) {
|
|
289
|
+
if (!this._fs)
|
|
290
|
+
return;
|
|
291
|
+
const name = path.split("/").filter(Boolean).pop() || path;
|
|
292
|
+
const confirmed = confirm(`Delete ${type} "${name}"?`);
|
|
293
|
+
if (!confirmed)
|
|
294
|
+
return;
|
|
295
|
+
try {
|
|
296
|
+
if (type === "dir") {
|
|
297
|
+
this._fs.delete(path, { recursive: true });
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
this._fs.deleteFile(path);
|
|
301
|
+
}
|
|
302
|
+
this._refreshFiles();
|
|
303
|
+
this.logger.log(`${type === "dir" ? "Directory" : "File"} "${name}" deleted successfully`);
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
this.logger.error(`Failed to delete ${type}: ${e.message || e}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async _backupImage() {
|
|
310
|
+
if (!this._fs)
|
|
311
|
+
return;
|
|
312
|
+
try {
|
|
313
|
+
this.logger.log("Creating LittleFS backup image...");
|
|
314
|
+
const image = this._fs.toImage();
|
|
315
|
+
const filename = `${this.partition.name}_littlefs_backup.bin`;
|
|
316
|
+
// Create download
|
|
317
|
+
const blob = new Blob([image], { type: "application/octet-stream" });
|
|
318
|
+
const url = URL.createObjectURL(blob);
|
|
319
|
+
const a = document.createElement("a");
|
|
320
|
+
a.href = url;
|
|
321
|
+
a.download = filename;
|
|
322
|
+
document.body.appendChild(a);
|
|
323
|
+
a.click();
|
|
324
|
+
document.body.removeChild(a);
|
|
325
|
+
URL.revokeObjectURL(url);
|
|
326
|
+
this.logger.log(`LittleFS backup saved as "${filename}"`);
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
this.logger.error(`Failed to backup LittleFS: ${e.message || e}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async _writeToFlash() {
|
|
333
|
+
if (!this._fs)
|
|
334
|
+
return;
|
|
335
|
+
const confirmed = confirm(`Write modified LittleFS to flash?\n\n` +
|
|
336
|
+
`Partition: ${this.partition.name}\n` +
|
|
337
|
+
`Offset: 0x${this.partition.offset.toString(16)}\n` +
|
|
338
|
+
`Size: ${this._formatSize(this.partition.size)}\n\n` +
|
|
339
|
+
`This will overwrite the current filesystem on the device!`);
|
|
340
|
+
if (!confirmed)
|
|
341
|
+
return;
|
|
342
|
+
try {
|
|
343
|
+
this._busy = true;
|
|
344
|
+
this._isFlashing = true;
|
|
345
|
+
this._flashProgress = 0;
|
|
346
|
+
this.logger.log("Creating LittleFS image...");
|
|
347
|
+
const image = this._fs.toImage();
|
|
348
|
+
this.logger.log(`Image created: ${this._formatSize(image.length)}`);
|
|
349
|
+
if (image.length > this.partition.size) {
|
|
350
|
+
this.logger.error(`Image size (${this._formatSize(image.length)}) exceeds partition size (${this._formatSize(this.partition.size)})`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
this.logger.log(`Writing ${this._formatSize(image.length)} to partition "${this.partition.name}" at 0x${this.partition.offset.toString(16)}...`);
|
|
354
|
+
// Convert Uint8Array to ArrayBuffer
|
|
355
|
+
const imageBuffer = image.buffer.slice(image.byteOffset, image.byteOffset + image.byteLength);
|
|
356
|
+
// Write the image to flash with progress callback
|
|
357
|
+
await this.espStub.flashData(imageBuffer, (bytesWritten, totalBytes) => {
|
|
358
|
+
const percent = Math.floor((bytesWritten / totalBytes) * 100);
|
|
359
|
+
this._flashProgress = percent;
|
|
360
|
+
this.logger.log(`Writing: ${percent}%`);
|
|
361
|
+
}, this.partition.offset);
|
|
362
|
+
this.logger.log(`✓ LittleFS successfully written to flash!`);
|
|
363
|
+
this.logger.log(`To use the new filesystem, reset your device.`);
|
|
364
|
+
}
|
|
365
|
+
catch (e) {
|
|
366
|
+
this.logger.error(`Failed to write LittleFS to flash: ${e.message || e}`);
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
this._busy = false;
|
|
370
|
+
this._isFlashing = false;
|
|
371
|
+
this._flashProgress = 0;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
_cleanup() {
|
|
375
|
+
if (this._fs) {
|
|
376
|
+
try {
|
|
377
|
+
// Don't call destroy() - just let garbage collection handle it
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
console.error("Error cleaning up LittleFS:", e);
|
|
381
|
+
}
|
|
382
|
+
this._fs = null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
_handleFileSelect(e) {
|
|
386
|
+
var _a;
|
|
387
|
+
const input = e.target;
|
|
388
|
+
this._selectedFile = ((_a = input.files) === null || _a === void 0 ? void 0 : _a[0]) || null;
|
|
389
|
+
}
|
|
390
|
+
render() {
|
|
391
|
+
const usedPercent = Math.round((this._usage.usedBytes / this._usage.capacityBytes) * 100);
|
|
392
|
+
return html `
|
|
393
|
+
<div class="littlefs-manager">
|
|
394
|
+
<h3>LittleFS Filesystem Manager</h3>
|
|
395
|
+
|
|
396
|
+
<div class="littlefs-info">
|
|
397
|
+
<div class="littlefs-partition-info">
|
|
398
|
+
<strong>Partition:</strong> ${this.partition.name}
|
|
399
|
+
<span class="littlefs-size"
|
|
400
|
+
>(${this._formatSize(this.partition.size)})</span
|
|
401
|
+
>
|
|
402
|
+
</div>
|
|
403
|
+
<div class="littlefs-usage">
|
|
404
|
+
<div class="usage-bar">
|
|
405
|
+
<div
|
|
406
|
+
class="usage-fill ${this._isFlashing ? "flashing" : ""}"
|
|
407
|
+
style="width: ${this._isFlashing
|
|
408
|
+
? this._flashProgress
|
|
409
|
+
: usedPercent}%"
|
|
410
|
+
></div>
|
|
411
|
+
</div>
|
|
412
|
+
<div class="usage-text">
|
|
413
|
+
${this._isFlashing
|
|
414
|
+
? html `<span class="flash-status">
|
|
415
|
+
⚡ Writing to flash: ${this._flashProgress}%
|
|
416
|
+
</span>`
|
|
417
|
+
: html `<span
|
|
418
|
+
>Used: ${this._formatSize(this._usage.usedBytes)} /
|
|
419
|
+
${this._formatSize(this._usage.capacityBytes)}
|
|
420
|
+
(${usedPercent}%)</span
|
|
421
|
+
>
|
|
422
|
+
${this._diskVersion
|
|
423
|
+
? html `<span class="disk-version"
|
|
424
|
+
>${this._diskVersion}</span
|
|
425
|
+
>`
|
|
426
|
+
: ""}`}
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<div class="littlefs-controls">
|
|
432
|
+
<ewt-button
|
|
433
|
+
label="Refresh"
|
|
434
|
+
@click=${this._refreshFiles}
|
|
435
|
+
?disabled=${this._busy}
|
|
436
|
+
></ewt-button>
|
|
437
|
+
<ewt-button
|
|
438
|
+
label="Backup Image"
|
|
439
|
+
@click=${this._backupImage}
|
|
440
|
+
?disabled=${this._busy}
|
|
441
|
+
></ewt-button>
|
|
442
|
+
<ewt-button
|
|
443
|
+
label="Write to Flash"
|
|
444
|
+
@click=${this._writeToFlash}
|
|
445
|
+
?disabled=${this._busy}
|
|
446
|
+
></ewt-button>
|
|
447
|
+
<ewt-button
|
|
448
|
+
label="Close"
|
|
449
|
+
@click=${() => {
|
|
450
|
+
this._cleanup();
|
|
451
|
+
if (this.onClose)
|
|
452
|
+
this.onClose();
|
|
453
|
+
}}
|
|
454
|
+
?disabled=${this._busy}
|
|
455
|
+
></ewt-button>
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
<div class="littlefs-breadcrumb">
|
|
459
|
+
<ewt-button
|
|
460
|
+
label="↑ Up"
|
|
461
|
+
@click=${this._navigateUp}
|
|
462
|
+
?disabled=${this._currentPath === "/" || this._busy}
|
|
463
|
+
></ewt-button>
|
|
464
|
+
<span>${this._currentPath || "/"}</span>
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<div class="littlefs-file-upload">
|
|
468
|
+
<input
|
|
469
|
+
type="file"
|
|
470
|
+
@change=${this._handleFileSelect}
|
|
471
|
+
?disabled=${this._busy}
|
|
472
|
+
/>
|
|
473
|
+
<ewt-button
|
|
474
|
+
label="Upload File"
|
|
475
|
+
@click=${this._uploadFile}
|
|
476
|
+
?disabled=${!this._selectedFile || this._busy}
|
|
477
|
+
></ewt-button>
|
|
478
|
+
<ewt-button
|
|
479
|
+
label="New Folder"
|
|
480
|
+
@click=${this._createFolder}
|
|
481
|
+
?disabled=${this._busy}
|
|
482
|
+
></ewt-button>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
<div class="littlefs-files">
|
|
486
|
+
<table class="file-table">
|
|
487
|
+
<thead>
|
|
488
|
+
<tr>
|
|
489
|
+
<th>Name</th>
|
|
490
|
+
<th>Type</th>
|
|
491
|
+
<th>Size</th>
|
|
492
|
+
<th>Actions</th>
|
|
493
|
+
</tr>
|
|
494
|
+
</thead>
|
|
495
|
+
<tbody>
|
|
496
|
+
${this._files.length === 0
|
|
497
|
+
? html `
|
|
498
|
+
<tr>
|
|
499
|
+
<td colspan="4" class="empty-state">
|
|
500
|
+
No files in this directory
|
|
501
|
+
</td>
|
|
502
|
+
</tr>
|
|
503
|
+
`
|
|
504
|
+
: this._files.map((entry) => html `
|
|
505
|
+
<tr>
|
|
506
|
+
<td>
|
|
507
|
+
<div
|
|
508
|
+
class="file-name ${entry.type === "dir"
|
|
509
|
+
? "clickable"
|
|
510
|
+
: ""}"
|
|
511
|
+
@click=${entry.type === "dir"
|
|
512
|
+
? () => this._navigateTo(entry.path)
|
|
513
|
+
: null}
|
|
514
|
+
>
|
|
515
|
+
<span class="file-icon"
|
|
516
|
+
>${entry.type === "dir" ? "📁" : "📄"}</span
|
|
517
|
+
>
|
|
518
|
+
<span
|
|
519
|
+
>${entry.path.split("/").filter(Boolean).pop() ||
|
|
520
|
+
"/"}</span
|
|
521
|
+
>
|
|
522
|
+
</div>
|
|
523
|
+
</td>
|
|
524
|
+
<td>${entry.type === "dir" ? "Directory" : "File"}</td>
|
|
525
|
+
<td>
|
|
526
|
+
${entry.type === "file"
|
|
527
|
+
? this._formatSize(entry.size)
|
|
528
|
+
: "-"}
|
|
529
|
+
</td>
|
|
530
|
+
<td>
|
|
531
|
+
<div class="file-actions">
|
|
532
|
+
${entry.type === "file"
|
|
533
|
+
? html `
|
|
534
|
+
<ewt-button
|
|
535
|
+
label="Download"
|
|
536
|
+
@click=${() => this._downloadFile(entry.path)}
|
|
537
|
+
?disabled=${this._busy}
|
|
538
|
+
></ewt-button>
|
|
539
|
+
`
|
|
540
|
+
: ""}
|
|
541
|
+
<ewt-button
|
|
542
|
+
class="danger"
|
|
543
|
+
label="Delete"
|
|
544
|
+
@click=${() => this._deleteFile(entry.path, entry.type)}
|
|
545
|
+
?disabled=${this._busy}
|
|
546
|
+
></ewt-button>
|
|
547
|
+
</div>
|
|
548
|
+
</td>
|
|
549
|
+
</tr>
|
|
550
|
+
`)}
|
|
551
|
+
</tbody>
|
|
552
|
+
</table>
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
`;
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
EwtLittleFSManager.styles = css `
|
|
559
|
+
:host {
|
|
560
|
+
display: block;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.littlefs-manager {
|
|
564
|
+
width: 100%;
|
|
565
|
+
max-width: 100%;
|
|
566
|
+
margin: 0 auto;
|
|
567
|
+
padding: 15px;
|
|
568
|
+
border: 2px solid var(--mdc-theme-primary, #03a9f4);
|
|
569
|
+
border-radius: 10px;
|
|
570
|
+
background-color: rgba(3, 169, 244, 0.05);
|
|
571
|
+
box-sizing: border-box;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
h3 {
|
|
575
|
+
margin: 0 0 15px 0;
|
|
576
|
+
color: var(--mdc-theme-primary, #03a9f4);
|
|
577
|
+
font-size: 18px;
|
|
578
|
+
font-weight: 600;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.littlefs-info {
|
|
582
|
+
margin-bottom: 15px;
|
|
583
|
+
padding: 12px;
|
|
584
|
+
background-color: rgba(255, 255, 255, 0.5);
|
|
585
|
+
border-radius: 8px;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.littlefs-partition-info {
|
|
589
|
+
margin-bottom: 10px;
|
|
590
|
+
font-size: 13px;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.littlefs-size {
|
|
594
|
+
color: #666;
|
|
595
|
+
margin-left: 8px;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.littlefs-usage {
|
|
599
|
+
margin-top: 8px;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.usage-bar {
|
|
603
|
+
width: 100%;
|
|
604
|
+
height: 18px;
|
|
605
|
+
background-color: #e0e0e0;
|
|
606
|
+
border-radius: 10px;
|
|
607
|
+
overflow: hidden;
|
|
608
|
+
margin-bottom: 6px;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.usage-fill {
|
|
612
|
+
height: 100%;
|
|
613
|
+
background: linear-gradient(
|
|
614
|
+
90deg,
|
|
615
|
+
var(--mdc-theme-primary, #03a9f4) 0%,
|
|
616
|
+
var(--mdc-theme-primary, #03a9f4) 100%
|
|
617
|
+
);
|
|
618
|
+
transition: width 0.3s ease;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.usage-fill.flashing {
|
|
622
|
+
background: linear-gradient(90deg, #ff9800 0%, #ff5722 100%);
|
|
623
|
+
animation: pulse 1s ease-in-out infinite;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
@keyframes pulse {
|
|
627
|
+
0%,
|
|
628
|
+
100% {
|
|
629
|
+
opacity: 1;
|
|
630
|
+
}
|
|
631
|
+
50% {
|
|
632
|
+
opacity: 0.7;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.flash-status {
|
|
637
|
+
font-weight: 600;
|
|
638
|
+
color: #ff5722;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.usage-text {
|
|
642
|
+
display: flex;
|
|
643
|
+
justify-content: space-between;
|
|
644
|
+
align-items: center;
|
|
645
|
+
font-size: 12px;
|
|
646
|
+
color: #555;
|
|
647
|
+
flex-wrap: wrap;
|
|
648
|
+
gap: 5px;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.disk-version {
|
|
652
|
+
font-size: 11px;
|
|
653
|
+
padding: 2px 6px;
|
|
654
|
+
background-color: var(--mdc-theme-primary, #03a9f4);
|
|
655
|
+
color: white;
|
|
656
|
+
border-radius: 4px;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.littlefs-controls {
|
|
660
|
+
display: flex;
|
|
661
|
+
gap: 8px;
|
|
662
|
+
margin-bottom: 15px;
|
|
663
|
+
flex-wrap: wrap;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.littlefs-breadcrumb {
|
|
667
|
+
display: flex;
|
|
668
|
+
align-items: center;
|
|
669
|
+
gap: 8px;
|
|
670
|
+
margin-bottom: 12px;
|
|
671
|
+
padding: 8px;
|
|
672
|
+
background-color: rgba(255, 255, 255, 0.5);
|
|
673
|
+
border-radius: 8px;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.littlefs-breadcrumb span {
|
|
677
|
+
font-family: monospace;
|
|
678
|
+
font-size: 13px;
|
|
679
|
+
color: #333;
|
|
680
|
+
overflow: hidden;
|
|
681
|
+
text-overflow: ellipsis;
|
|
682
|
+
white-space: nowrap;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.littlefs-file-upload {
|
|
686
|
+
display: flex;
|
|
687
|
+
gap: 8px;
|
|
688
|
+
margin-bottom: 12px;
|
|
689
|
+
align-items: center;
|
|
690
|
+
flex-wrap: wrap;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.littlefs-file-upload input[type="file"] {
|
|
694
|
+
flex: 1;
|
|
695
|
+
min-width: 150px;
|
|
696
|
+
padding: 4px;
|
|
697
|
+
border: 2px solid #ccc;
|
|
698
|
+
border-radius: 8px;
|
|
699
|
+
font-size: 13px;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.littlefs-files {
|
|
703
|
+
max-height: 350px;
|
|
704
|
+
overflow-y: auto;
|
|
705
|
+
overflow-x: auto;
|
|
706
|
+
border: 1px solid #ccc;
|
|
707
|
+
border-radius: 8px;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.file-table {
|
|
711
|
+
width: 100%;
|
|
712
|
+
min-width: 500px;
|
|
713
|
+
border-collapse: collapse;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.file-table thead {
|
|
717
|
+
position: sticky;
|
|
718
|
+
top: 0;
|
|
719
|
+
background-color: #f5f5f5;
|
|
720
|
+
z-index: 10;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.file-table th {
|
|
724
|
+
padding: 8px;
|
|
725
|
+
text-align: left;
|
|
726
|
+
font-weight: 600;
|
|
727
|
+
border-bottom: 2px solid #ccc;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.file-table td {
|
|
731
|
+
padding: 8px 10px;
|
|
732
|
+
border-bottom: 1px solid #e0e0e0;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
.file-table tbody tr:hover {
|
|
736
|
+
background-color: rgba(3, 169, 244, 0.1);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.file-table .empty-state {
|
|
740
|
+
text-align: center;
|
|
741
|
+
color: #999;
|
|
742
|
+
padding: 30px;
|
|
743
|
+
font-style: italic;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.file-name {
|
|
747
|
+
display: flex;
|
|
748
|
+
align-items: center;
|
|
749
|
+
gap: 8px;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.file-name.clickable {
|
|
753
|
+
cursor: pointer;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.file-name.clickable:hover {
|
|
757
|
+
color: var(--mdc-theme-primary, #03a9f4);
|
|
758
|
+
text-decoration: underline;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.file-icon {
|
|
762
|
+
font-size: 16px;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
.file-actions {
|
|
766
|
+
display: flex;
|
|
767
|
+
gap: 5px;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.danger {
|
|
771
|
+
--mdc-theme-primary: var(--improv-danger-color, #db4437);
|
|
772
|
+
}
|
|
773
|
+
`;
|
|
774
|
+
__decorate([
|
|
775
|
+
property({ type: Object })
|
|
776
|
+
], EwtLittleFSManager.prototype, "partition", void 0);
|
|
777
|
+
__decorate([
|
|
778
|
+
property({ type: Object })
|
|
779
|
+
], EwtLittleFSManager.prototype, "espStub", void 0);
|
|
780
|
+
__decorate([
|
|
781
|
+
property({ type: Function })
|
|
782
|
+
], EwtLittleFSManager.prototype, "logger", void 0);
|
|
783
|
+
__decorate([
|
|
784
|
+
property({ type: Function })
|
|
785
|
+
], EwtLittleFSManager.prototype, "onClose", void 0);
|
|
786
|
+
__decorate([
|
|
787
|
+
state()
|
|
788
|
+
], EwtLittleFSManager.prototype, "_currentPath", void 0);
|
|
789
|
+
__decorate([
|
|
790
|
+
state()
|
|
791
|
+
], EwtLittleFSManager.prototype, "_files", void 0);
|
|
792
|
+
__decorate([
|
|
793
|
+
state()
|
|
794
|
+
], EwtLittleFSManager.prototype, "_fs", void 0);
|
|
795
|
+
__decorate([
|
|
796
|
+
state()
|
|
797
|
+
], EwtLittleFSManager.prototype, "_blockSize", void 0);
|
|
798
|
+
__decorate([
|
|
799
|
+
state()
|
|
800
|
+
], EwtLittleFSManager.prototype, "_usage", void 0);
|
|
801
|
+
__decorate([
|
|
802
|
+
state()
|
|
803
|
+
], EwtLittleFSManager.prototype, "_diskVersion", void 0);
|
|
804
|
+
__decorate([
|
|
805
|
+
state()
|
|
806
|
+
], EwtLittleFSManager.prototype, "_busy", void 0);
|
|
807
|
+
__decorate([
|
|
808
|
+
state()
|
|
809
|
+
], EwtLittleFSManager.prototype, "_selectedFile", void 0);
|
|
810
|
+
__decorate([
|
|
811
|
+
state()
|
|
812
|
+
], EwtLittleFSManager.prototype, "_flashProgress", void 0);
|
|
813
|
+
__decorate([
|
|
814
|
+
state()
|
|
815
|
+
], EwtLittleFSManager.prototype, "_isFlashing", void 0);
|
|
816
|
+
EwtLittleFSManager = __decorate([
|
|
817
|
+
customElement("ewt-littlefs-manager")
|
|
818
|
+
], EwtLittleFSManager);
|
|
819
|
+
export { EwtLittleFSManager };
|