tasmota-webserial-esptool 6.5.3 → 7.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/.vscode/settings.json +2 -0
- package/README.md +4 -3
- package/READ_FLASH_FEATURE.md +130 -0
- package/css/light.css +11 -0
- package/css/style.css +213 -45
- package/dist/const.d.ts +42 -3
- package/dist/const.js +102 -4
- package/dist/esp_loader.d.ts +27 -3
- package/dist/esp_loader.js +376 -17
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -2
- package/dist/partition.d.ts +26 -0
- package/dist/partition.js +129 -0
- package/dist/stubs/esp32.json +4 -4
- package/dist/stubs/esp32c2.json +4 -4
- package/dist/stubs/esp32c3.json +4 -4
- package/dist/stubs/esp32c5.json +4 -4
- package/dist/stubs/esp32c6.json +4 -4
- package/dist/stubs/esp32c61.json +4 -4
- package/dist/stubs/esp32h2.json +4 -4
- package/dist/stubs/esp32p4.json +4 -4
- package/dist/stubs/esp32p4r3.json +4 -4
- package/dist/stubs/esp32s2.json +4 -4
- package/dist/stubs/esp32s3.json +4 -4
- package/dist/stubs/esp8266.json +3 -3
- package/dist/stubs/index.d.ts +1 -1
- package/dist/stubs/index.js +7 -1
- package/dist/web/esp32-CijhsJH1.js +1 -0
- package/dist/web/esp32c2-C17SM4gO.js +1 -0
- package/dist/web/esp32c3-DxRGijbg.js +1 -0
- package/dist/web/esp32c5-3mDOIGa4.js +1 -0
- package/dist/web/esp32c6-h6U0SQTm.js +1 -0
- package/dist/web/esp32c61-BKtexhPZ.js +1 -0
- package/dist/web/esp32h2-RtuWSEmP.js +1 -0
- package/dist/web/esp32p4-5nkIjxqJ.js +1 -0
- package/dist/web/esp32p4r3-CpHBYEwI.js +1 -0
- package/dist/web/esp32s2-IiDBtXxo.js +1 -0
- package/dist/web/esp32s3-6yv5yxum.js +1 -0
- package/dist/web/esp8266-CUwxJpGa.js +1 -0
- package/dist/web/index.js +1 -1
- package/index.html +158 -34
- package/js/modules/esp32-CijhsJH1.js +1 -0
- package/js/modules/esp32c2-C17SM4gO.js +1 -0
- package/js/modules/esp32c3-DxRGijbg.js +1 -0
- package/js/modules/esp32c5-3mDOIGa4.js +1 -0
- package/js/modules/esp32c6-h6U0SQTm.js +1 -0
- package/js/modules/esp32c61-BKtexhPZ.js +1 -0
- package/js/modules/esp32h2-RtuWSEmP.js +1 -0
- package/js/modules/esp32p4-5nkIjxqJ.js +1 -0
- package/js/modules/esp32p4r3-CpHBYEwI.js +1 -0
- package/js/modules/esp32s2-IiDBtXxo.js +1 -0
- package/js/modules/esp32s3-6yv5yxum.js +1 -0
- package/js/modules/esp8266-CUwxJpGa.js +1 -0
- package/js/modules/esptool.js +1 -1
- package/js/script.js +456 -11
- package/package.json +6 -6
- package/src/const.ts +109 -5
- package/src/esp_loader.ts +491 -18
- package/src/index.ts +3 -1
- package/src/partition.ts +155 -0
- package/src/stubs/README.md +1 -1
- package/src/stubs/esp32.json +4 -4
- package/src/stubs/esp32c2.json +4 -4
- package/src/stubs/esp32c3.json +4 -4
- package/src/stubs/esp32c5.json +4 -4
- package/src/stubs/esp32c6.json +4 -4
- package/src/stubs/esp32c61.json +4 -4
- package/src/stubs/esp32h2.json +4 -4
- package/src/stubs/esp32p4.json +4 -4
- package/src/stubs/esp32p4r3.json +4 -4
- package/src/stubs/esp32s2.json +4 -4
- package/src/stubs/esp32s3.json +4 -4
- package/src/stubs/esp8266.json +3 -3
- package/src/stubs/index.ts +14 -2
- package/BUGFIX_GET_SECURITY_INFO.md +0 -126
- package/IMPLEMENTATION_SUMMARY.md +0 -232
- package/SECURITY_INFO_EXPLANATION.md +0 -145
- package/dist/web/esp32-BNIFdu1P.js +0 -1
- package/dist/web/esp32c2-BqxquOKw.js +0 -1
- package/dist/web/esp32c3-BOOqe8me.js +0 -1
- package/dist/web/esp32c5-mcj52-K1.js +0 -1
- package/dist/web/esp32c6-Cg5qYgg7.js +0 -1
- package/dist/web/esp32c61-CzCdsydk.js +0 -1
- package/dist/web/esp32h2-DZa_lpff.js +0 -1
- package/dist/web/esp32p4-DyGqUAeZ.js +0 -1
- package/dist/web/esp32p4r3-Cle9QJmZ.js +0 -1
- package/dist/web/esp32s2-Bk4mqADi.js +0 -1
- package/dist/web/esp32s3-Df3OUCOC.js +0 -1
- package/dist/web/esp8266-CQFcqJ_a.js +0 -1
- package/js/modules/esp32-BNIFdu1P.js +0 -1
- package/js/modules/esp32c2-BqxquOKw.js +0 -1
- package/js/modules/esp32c3-BOOqe8me.js +0 -1
- package/js/modules/esp32c5-mcj52-K1.js +0 -1
- package/js/modules/esp32c6-Cg5qYgg7.js +0 -1
- package/js/modules/esp32c61-CzCdsydk.js +0 -1
- package/js/modules/esp32h2-DZa_lpff.js +0 -1
- package/js/modules/esp32p4-DyGqUAeZ.js +0 -1
- package/js/modules/esp32p4r3-Cle9QJmZ.js +0 -1
- package/js/modules/esp32s2-Bk4mqADi.js +0 -1
- package/js/modules/esp32s3-Df3OUCOC.js +0 -1
- package/js/modules/esp8266-CQFcqJ_a.js +0 -1
package/js/script.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
let espStub;
|
|
2
2
|
|
|
3
|
-
const baudRates = [2000000, 1500000, 921600, 460800, 230400, 153600, 128000, 115200];
|
|
3
|
+
const baudRates = [2000000, 1500000, 921600, 500000, 460800, 230400, 153600, 128000, 115200];
|
|
4
4
|
const bufferSize = 512;
|
|
5
5
|
const colors = ["#00a7e9", "#f89521", "#be1e2d"];
|
|
6
6
|
const measurementPeriodId = "0001";
|
|
@@ -12,11 +12,18 @@ const baudRate = document.getElementById("baudRate");
|
|
|
12
12
|
const butClear = document.getElementById("butClear");
|
|
13
13
|
const butErase = document.getElementById("butErase");
|
|
14
14
|
const butProgram = document.getElementById("butProgram");
|
|
15
|
+
const butReadFlash = document.getElementById("butReadFlash");
|
|
16
|
+
const readOffset = document.getElementById("readOffset");
|
|
17
|
+
const readSize = document.getElementById("readSize");
|
|
18
|
+
const readProgress = document.getElementById("readProgress");
|
|
19
|
+
const butReadPartitions = document.getElementById("butReadPartitions");
|
|
20
|
+
const partitionList = document.getElementById("partitionList");
|
|
15
21
|
const autoscroll = document.getElementById("autoscroll");
|
|
16
22
|
const lightSS = document.getElementById("light");
|
|
17
23
|
const darkSS = document.getElementById("dark");
|
|
18
24
|
const darkMode = document.getElementById("darkmode");
|
|
19
25
|
const debugMode = document.getElementById("debugmode");
|
|
26
|
+
const showLog = document.getElementById("showlog");
|
|
20
27
|
const firmware = document.querySelectorAll(".upload .firmware input");
|
|
21
28
|
const progress = document.querySelectorAll(".upload .progress-bar");
|
|
22
29
|
const offsets = document.querySelectorAll(".upload .offset");
|
|
@@ -36,19 +43,57 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
36
43
|
butClear.addEventListener("click", clickClear);
|
|
37
44
|
butErase.addEventListener("click", clickErase);
|
|
38
45
|
butProgram.addEventListener("click", clickProgram);
|
|
46
|
+
butReadFlash.addEventListener("click", clickReadFlash);
|
|
47
|
+
butReadPartitions.addEventListener("click", clickReadPartitions);
|
|
39
48
|
for (let i = 0; i < firmware.length; i++) {
|
|
40
49
|
firmware[i].addEventListener("change", checkFirmware);
|
|
41
50
|
}
|
|
42
51
|
for (let i = 0; i < offsets.length; i++) {
|
|
43
52
|
offsets[i].addEventListener("change", checkProgrammable);
|
|
44
53
|
}
|
|
54
|
+
|
|
55
|
+
// Initialize upload rows visibility - only show first row
|
|
56
|
+
updateUploadRowsVisibility();
|
|
57
|
+
|
|
45
58
|
autoscroll.addEventListener("click", clickAutoscroll);
|
|
46
59
|
baudRate.addEventListener("change", changeBaudRate);
|
|
47
60
|
darkMode.addEventListener("click", clickDarkMode);
|
|
48
61
|
debugMode.addEventListener("click", clickDebugMode);
|
|
62
|
+
showLog.addEventListener("click", clickShowLog);
|
|
49
63
|
window.addEventListener("error", function (event) {
|
|
50
64
|
console.log("Got an uncaught error: ", event.error);
|
|
51
65
|
});
|
|
66
|
+
|
|
67
|
+
// Header auto-hide functionality
|
|
68
|
+
const header = document.querySelector(".header");
|
|
69
|
+
const main = document.querySelector(".main");
|
|
70
|
+
|
|
71
|
+
// Show header on mouse enter at top of page
|
|
72
|
+
main.addEventListener("mousemove", (e) => {
|
|
73
|
+
if (e.clientY < 5 && header.classList.contains("header-hidden")) {
|
|
74
|
+
header.classList.remove("header-hidden");
|
|
75
|
+
main.classList.remove("no-header-padding");
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Keep header visible when mouse is over it
|
|
80
|
+
header.addEventListener("mouseenter", () => {
|
|
81
|
+
header.classList.remove("header-hidden");
|
|
82
|
+
main.classList.remove("no-header-padding");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Hide header when mouse leaves (only if connected)
|
|
86
|
+
header.addEventListener("mouseleave", () => {
|
|
87
|
+
if (espStub && header.classList.contains("header-hidden") === false) {
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
if (!header.matches(":hover")) {
|
|
90
|
+
header.classList.add("header-hidden");
|
|
91
|
+
main.classList.add("no-header-padding");
|
|
92
|
+
}
|
|
93
|
+
}, 1000);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
52
97
|
if ("serial" in navigator) {
|
|
53
98
|
const notSupported = document.getElementById("notSupported");
|
|
54
99
|
notSupported.classList.add("hidden");
|
|
@@ -57,7 +102,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
57
102
|
initBaudRate();
|
|
58
103
|
loadAllSettings();
|
|
59
104
|
updateTheme();
|
|
60
|
-
logMsg("
|
|
105
|
+
logMsg("WebSerial ESPTool loaded.");
|
|
61
106
|
});
|
|
62
107
|
|
|
63
108
|
function initBaudRate() {
|
|
@@ -206,6 +251,12 @@ async function clickConnect() {
|
|
|
206
251
|
toggleUIConnected(true);
|
|
207
252
|
toggleUIToolbar(true);
|
|
208
253
|
|
|
254
|
+
// Set detected flash size in the read size field
|
|
255
|
+
if (espStub.flashSize) {
|
|
256
|
+
const flashSizeBytes = parseInt(espStub.flashSize) * 1024 * 1024; // Convert MB to bytes
|
|
257
|
+
readSize.value = "0x" + flashSizeBytes.toString(16);
|
|
258
|
+
}
|
|
259
|
+
|
|
209
260
|
// Set the selected baud rate
|
|
210
261
|
let baud = parseInt(baudRate.value);
|
|
211
262
|
if (baudRates.includes(baud)) {
|
|
@@ -262,6 +313,35 @@ async function clickDebugMode() {
|
|
|
262
313
|
logMsg("Debug mode " + (debugMode.checked ? "enabled" : "disabled"));
|
|
263
314
|
}
|
|
264
315
|
|
|
316
|
+
/**
|
|
317
|
+
* @name clickShowLog
|
|
318
|
+
* Change handler for the Show Log checkbox.
|
|
319
|
+
*/
|
|
320
|
+
async function clickShowLog() {
|
|
321
|
+
saveSetting("showlog", showLog.checked);
|
|
322
|
+
updateLogVisibility();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @name updateLogVisibility
|
|
327
|
+
* Update log and log controls visibility
|
|
328
|
+
*/
|
|
329
|
+
function updateLogVisibility() {
|
|
330
|
+
const logControls = document.querySelector(".log-controls");
|
|
331
|
+
|
|
332
|
+
if (showLog.checked) {
|
|
333
|
+
log.classList.remove("hidden");
|
|
334
|
+
if (logControls) {
|
|
335
|
+
logControls.classList.remove("hidden");
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
log.classList.add("hidden");
|
|
339
|
+
if (logControls) {
|
|
340
|
+
logControls.classList.add("hidden");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
265
345
|
/**
|
|
266
346
|
* @name clickErase
|
|
267
347
|
* Click handler for the erase button.
|
|
@@ -312,7 +392,7 @@ async function clickProgram() {
|
|
|
312
392
|
baudRate.disabled = true;
|
|
313
393
|
butErase.disabled = true;
|
|
314
394
|
butProgram.disabled = true;
|
|
315
|
-
for (let i = 0; i <
|
|
395
|
+
for (let i = 0; i < firmware.length; i++) {
|
|
316
396
|
firmware[i].disabled = true;
|
|
317
397
|
offsets[i].disabled = true;
|
|
318
398
|
}
|
|
@@ -336,7 +416,7 @@ async function clickProgram() {
|
|
|
336
416
|
errorMsg(e);
|
|
337
417
|
}
|
|
338
418
|
}
|
|
339
|
-
for (let i = 0; i <
|
|
419
|
+
for (let i = 0; i < firmware.length; i++) {
|
|
340
420
|
firmware[i].disabled = false;
|
|
341
421
|
offsets[i].disabled = false;
|
|
342
422
|
progress[i].classList.add("hidden");
|
|
@@ -354,7 +434,7 @@ function getValidFiles() {
|
|
|
354
434
|
// and will also return a list of files to program
|
|
355
435
|
let validFiles = [];
|
|
356
436
|
let offsetVals = [];
|
|
357
|
-
for (let i = 0; i <
|
|
437
|
+
for (let i = 0; i < firmware.length; i++) {
|
|
358
438
|
let offs = parseInt(offsets[i].value, 16);
|
|
359
439
|
if (firmware[i].files.length > 0 && !offsetVals.includes(offs)) {
|
|
360
440
|
validFiles.push(i);
|
|
@@ -381,11 +461,7 @@ async function checkFirmware(event) {
|
|
|
381
461
|
let label = event.target.parentNode.querySelector("span");
|
|
382
462
|
let icon = event.target.parentNode.querySelector("svg");
|
|
383
463
|
if (filename != "") {
|
|
384
|
-
|
|
385
|
-
label.innerHTML = filename.substring(0, 14) + "…";
|
|
386
|
-
} else {
|
|
387
|
-
label.innerHTML = filename;
|
|
388
|
-
}
|
|
464
|
+
label.innerHTML = filename;
|
|
389
465
|
icon.classList.add("hidden");
|
|
390
466
|
} else {
|
|
391
467
|
label.innerHTML = "Choose a file…";
|
|
@@ -393,6 +469,358 @@ async function checkFirmware(event) {
|
|
|
393
469
|
}
|
|
394
470
|
|
|
395
471
|
await checkProgrammable();
|
|
472
|
+
updateUploadRowsVisibility();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* @name updateUploadRowsVisibility
|
|
477
|
+
* Show/hide upload rows dynamically - only for flash write section
|
|
478
|
+
*/
|
|
479
|
+
function updateUploadRowsVisibility() {
|
|
480
|
+
const uploadRows = document.querySelectorAll(".upload");
|
|
481
|
+
let lastFilledIndex = -1;
|
|
482
|
+
|
|
483
|
+
// Find the last filled row
|
|
484
|
+
for (let i = 0; i < firmware.length; i++) {
|
|
485
|
+
if (firmware[i].files.length > 0) {
|
|
486
|
+
lastFilledIndex = i;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Show rows up to lastFilledIndex + 1 (next empty row), minimum 1 row
|
|
491
|
+
for (let i = 0; i < uploadRows.length; i++) {
|
|
492
|
+
if (i <= lastFilledIndex + 1) {
|
|
493
|
+
uploadRows[i].style.display = "flex";
|
|
494
|
+
} else {
|
|
495
|
+
uploadRows[i].style.display = "none";
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* @name clickReadFlash
|
|
502
|
+
* Click handler for the read flash button.
|
|
503
|
+
*/
|
|
504
|
+
async function clickReadFlash() {
|
|
505
|
+
const offset = parseInt(readOffset.value, 16);
|
|
506
|
+
const size = parseInt(readSize.value, 16);
|
|
507
|
+
|
|
508
|
+
if (isNaN(offset) || isNaN(size) || size <= 0) {
|
|
509
|
+
errorMsg("Invalid offset or size value");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Prompt user for filename
|
|
514
|
+
const defaultFilename = `flash_0x${offset.toString(16)}_0x${size.toString(16)}.bin`;
|
|
515
|
+
const filename = prompt(`Enter filename for flash data:`, defaultFilename);
|
|
516
|
+
|
|
517
|
+
// User cancelled
|
|
518
|
+
if (filename === null) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// User entered empty string
|
|
523
|
+
if (filename.trim() === "") {
|
|
524
|
+
errorMsg("Filename cannot be empty");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
baudRate.disabled = true;
|
|
529
|
+
butErase.disabled = true;
|
|
530
|
+
butProgram.disabled = true;
|
|
531
|
+
butReadFlash.disabled = true;
|
|
532
|
+
readOffset.disabled = true;
|
|
533
|
+
readSize.disabled = true;
|
|
534
|
+
readProgress.classList.remove("hidden");
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const progressBar = readProgress.querySelector("div");
|
|
538
|
+
|
|
539
|
+
const data = await espStub.readFlash(
|
|
540
|
+
offset,
|
|
541
|
+
size,
|
|
542
|
+
(packet, progress, totalSize) => {
|
|
543
|
+
progressBar.style.width =
|
|
544
|
+
Math.floor((progress / totalSize) * 100) + "%";
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
logMsg(`Successfully read ${data.length} bytes from flash`);
|
|
549
|
+
|
|
550
|
+
// Create a download link with user-specified filename
|
|
551
|
+
const blob = new Blob([data], { type: "application/octet-stream" });
|
|
552
|
+
const url = URL.createObjectURL(blob);
|
|
553
|
+
const a = document.createElement("a");
|
|
554
|
+
a.href = url;
|
|
555
|
+
a.download = filename;
|
|
556
|
+
document.body.appendChild(a);
|
|
557
|
+
a.click();
|
|
558
|
+
document.body.removeChild(a);
|
|
559
|
+
URL.revokeObjectURL(url);
|
|
560
|
+
|
|
561
|
+
logMsg(`Flash data downloaded as "${filename}"`);
|
|
562
|
+
} catch (e) {
|
|
563
|
+
errorMsg("Failed to read flash: " + e);
|
|
564
|
+
} finally {
|
|
565
|
+
readProgress.classList.add("hidden");
|
|
566
|
+
readProgress.querySelector("div").style.width = "0";
|
|
567
|
+
butErase.disabled = false;
|
|
568
|
+
baudRate.disabled = false;
|
|
569
|
+
butProgram.disabled = getValidFiles().length == 0;
|
|
570
|
+
butReadFlash.disabled = false;
|
|
571
|
+
readOffset.disabled = false;
|
|
572
|
+
readSize.disabled = false;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* @name clickReadPartitions
|
|
578
|
+
* Click handler for the read partitions button.
|
|
579
|
+
*/
|
|
580
|
+
async function clickReadPartitions() {
|
|
581
|
+
const PARTITION_TABLE_OFFSET = 0x8000;
|
|
582
|
+
const PARTITION_TABLE_SIZE = 0x1000; // Read 4KB to get all partitions
|
|
583
|
+
|
|
584
|
+
butReadPartitions.disabled = true;
|
|
585
|
+
butErase.disabled = true;
|
|
586
|
+
butProgram.disabled = true;
|
|
587
|
+
butReadFlash.disabled = true;
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
logMsg("Reading partition table from 0x8000...");
|
|
591
|
+
|
|
592
|
+
const data = await espStub.readFlash(PARTITION_TABLE_OFFSET, PARTITION_TABLE_SIZE);
|
|
593
|
+
|
|
594
|
+
const partitions = parsePartitionTable(data);
|
|
595
|
+
|
|
596
|
+
if (partitions.length === 0) {
|
|
597
|
+
errorMsg("No valid partition table found");
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
logMsg(`Found ${partitions.length} partition(s)`);
|
|
602
|
+
|
|
603
|
+
// Display partitions
|
|
604
|
+
displayPartitions(partitions);
|
|
605
|
+
|
|
606
|
+
} catch (e) {
|
|
607
|
+
errorMsg("Failed to read partition table: " + e);
|
|
608
|
+
} finally {
|
|
609
|
+
butReadPartitions.disabled = false;
|
|
610
|
+
butErase.disabled = false;
|
|
611
|
+
butProgram.disabled = getValidFiles().length == 0;
|
|
612
|
+
butReadFlash.disabled = false;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Parse partition table from binary data
|
|
618
|
+
*/
|
|
619
|
+
function parsePartitionTable(data) {
|
|
620
|
+
const PARTITION_MAGIC = 0x50aa;
|
|
621
|
+
const PARTITION_ENTRY_SIZE = 32;
|
|
622
|
+
const partitions = [];
|
|
623
|
+
|
|
624
|
+
for (let i = 0; i < data.length; i += PARTITION_ENTRY_SIZE) {
|
|
625
|
+
const magic = data[i] | (data[i + 1] << 8);
|
|
626
|
+
|
|
627
|
+
if (magic !== PARTITION_MAGIC) {
|
|
628
|
+
break; // End of partition table
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const type = data[i + 2];
|
|
632
|
+
const subtype = data[i + 3];
|
|
633
|
+
const offset = data[i + 4] | (data[i + 5] << 8) | (data[i + 6] << 16) | (data[i + 7] << 24);
|
|
634
|
+
const size = data[i + 8] | (data[i + 9] << 8) | (data[i + 10] << 16) | (data[i + 11] << 24);
|
|
635
|
+
|
|
636
|
+
// Read name (16 bytes, null-terminated)
|
|
637
|
+
let name = "";
|
|
638
|
+
for (let j = 12; j < 28; j++) {
|
|
639
|
+
if (data[i + j] === 0) break;
|
|
640
|
+
name += String.fromCharCode(data[i + j]);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const flags = data[i + 28] | (data[i + 29] << 8) | (data[i + 30] << 16) | (data[i + 31] << 24);
|
|
644
|
+
|
|
645
|
+
// Get type names
|
|
646
|
+
const typeNames = { 0x00: "app", 0x01: "data" };
|
|
647
|
+
const appSubtypes = {
|
|
648
|
+
0x00: "factory", 0x10: "ota_0", 0x11: "ota_1", 0x12: "ota_2",
|
|
649
|
+
0x13: "ota_3", 0x14: "ota_4", 0x15: "ota_5", 0x20: "test"
|
|
650
|
+
};
|
|
651
|
+
const dataSubtypes = {
|
|
652
|
+
0x00: "ota", 0x01: "phy", 0x02: "nvs", 0x03: "coredump",
|
|
653
|
+
0x04: "nvs_keys", 0x05: "efuse", 0x81: "fat", 0x82: "spiffs"
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const typeName = typeNames[type] || `0x${type.toString(16)}`;
|
|
657
|
+
let subtypeName = "";
|
|
658
|
+
if (type === 0x00) {
|
|
659
|
+
subtypeName = appSubtypes[subtype] || `0x${subtype.toString(16)}`;
|
|
660
|
+
} else if (type === 0x01) {
|
|
661
|
+
subtypeName = dataSubtypes[subtype] || `0x${subtype.toString(16)}`;
|
|
662
|
+
} else {
|
|
663
|
+
subtypeName = `0x${subtype.toString(16)}`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
partitions.push({
|
|
667
|
+
name,
|
|
668
|
+
type,
|
|
669
|
+
subtype,
|
|
670
|
+
offset,
|
|
671
|
+
size,
|
|
672
|
+
flags,
|
|
673
|
+
typeName,
|
|
674
|
+
subtypeName
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return partitions;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Display partitions in the UI
|
|
683
|
+
*/
|
|
684
|
+
function displayPartitions(partitions) {
|
|
685
|
+
partitionList.innerHTML = "";
|
|
686
|
+
partitionList.classList.remove("hidden");
|
|
687
|
+
|
|
688
|
+
// Hide the Read Partition Table button after successful read
|
|
689
|
+
butReadPartitions.classList.add("hidden");
|
|
690
|
+
|
|
691
|
+
const table = document.createElement("table");
|
|
692
|
+
table.className = "partition-table-display";
|
|
693
|
+
|
|
694
|
+
// Header
|
|
695
|
+
const thead = document.createElement("thead");
|
|
696
|
+
const headerRow = document.createElement("tr");
|
|
697
|
+
["Name", "Type", "SubType", "Offset", "Size", "Action"].forEach(text => {
|
|
698
|
+
const th = document.createElement("th");
|
|
699
|
+
th.textContent = text;
|
|
700
|
+
headerRow.appendChild(th);
|
|
701
|
+
});
|
|
702
|
+
thead.appendChild(headerRow);
|
|
703
|
+
table.appendChild(thead);
|
|
704
|
+
|
|
705
|
+
// Body
|
|
706
|
+
const tbody = document.createElement("tbody");
|
|
707
|
+
partitions.forEach(partition => {
|
|
708
|
+
const row = document.createElement("tr");
|
|
709
|
+
|
|
710
|
+
// Name
|
|
711
|
+
const nameCell = document.createElement("td");
|
|
712
|
+
nameCell.textContent = partition.name;
|
|
713
|
+
row.appendChild(nameCell);
|
|
714
|
+
|
|
715
|
+
// Type
|
|
716
|
+
const typeCell = document.createElement("td");
|
|
717
|
+
typeCell.textContent = partition.typeName;
|
|
718
|
+
row.appendChild(typeCell);
|
|
719
|
+
|
|
720
|
+
// SubType
|
|
721
|
+
const subtypeCell = document.createElement("td");
|
|
722
|
+
subtypeCell.textContent = partition.subtypeName;
|
|
723
|
+
row.appendChild(subtypeCell);
|
|
724
|
+
|
|
725
|
+
// Offset
|
|
726
|
+
const offsetCell = document.createElement("td");
|
|
727
|
+
offsetCell.textContent = `0x${partition.offset.toString(16)}`;
|
|
728
|
+
row.appendChild(offsetCell);
|
|
729
|
+
|
|
730
|
+
// Size
|
|
731
|
+
const sizeCell = document.createElement("td");
|
|
732
|
+
sizeCell.textContent = formatSize(partition.size);
|
|
733
|
+
row.appendChild(sizeCell);
|
|
734
|
+
|
|
735
|
+
// Action
|
|
736
|
+
const actionCell = document.createElement("td");
|
|
737
|
+
const downloadBtn = document.createElement("button");
|
|
738
|
+
downloadBtn.textContent = "Download";
|
|
739
|
+
downloadBtn.className = "partition-download-btn";
|
|
740
|
+
downloadBtn.onclick = () => downloadPartition(partition);
|
|
741
|
+
actionCell.appendChild(downloadBtn);
|
|
742
|
+
row.appendChild(actionCell);
|
|
743
|
+
|
|
744
|
+
tbody.appendChild(row);
|
|
745
|
+
});
|
|
746
|
+
table.appendChild(tbody);
|
|
747
|
+
|
|
748
|
+
partitionList.appendChild(table);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Download a partition
|
|
753
|
+
*/
|
|
754
|
+
async function downloadPartition(partition) {
|
|
755
|
+
// Prompt user for filename
|
|
756
|
+
const defaultFilename = `${partition.name}_0x${partition.offset.toString(16)}.bin`;
|
|
757
|
+
const filename = prompt(
|
|
758
|
+
`Enter filename for partition "${partition.name}":`,
|
|
759
|
+
defaultFilename
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
// User cancelled
|
|
763
|
+
if (filename === null) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// User entered empty string
|
|
768
|
+
if (filename.trim() === "") {
|
|
769
|
+
errorMsg("Filename cannot be empty");
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const partitionProgress = document.getElementById("partitionProgress");
|
|
774
|
+
const progressBar = partitionProgress.querySelector("div");
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
partitionProgress.classList.remove("hidden");
|
|
778
|
+
progressBar.style.width = "0%";
|
|
779
|
+
|
|
780
|
+
logMsg(
|
|
781
|
+
`Downloading partition "${partition.name}" (${formatSize(partition.size)})...`
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
const data = await espStub.readFlash(
|
|
785
|
+
partition.offset,
|
|
786
|
+
partition.size,
|
|
787
|
+
(packet, progress, totalSize) => {
|
|
788
|
+
const percent = Math.floor((progress / totalSize) * 100);
|
|
789
|
+
progressBar.style.width = percent + "%";
|
|
790
|
+
}
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
// Create download with user-specified filename
|
|
794
|
+
const blob = new Blob([data], { type: "application/octet-stream" });
|
|
795
|
+
const url = URL.createObjectURL(blob);
|
|
796
|
+
const a = document.createElement("a");
|
|
797
|
+
a.href = url;
|
|
798
|
+
a.download = filename;
|
|
799
|
+
document.body.appendChild(a);
|
|
800
|
+
a.click();
|
|
801
|
+
document.body.removeChild(a);
|
|
802
|
+
URL.revokeObjectURL(url);
|
|
803
|
+
|
|
804
|
+
logMsg(`Partition "${partition.name}" downloaded as "${filename}"`);
|
|
805
|
+
} catch (e) {
|
|
806
|
+
errorMsg(`Failed to download partition: ${e}`);
|
|
807
|
+
} finally {
|
|
808
|
+
partitionProgress.classList.add("hidden");
|
|
809
|
+
progressBar.style.width = "0%";
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Format size in human-readable format
|
|
815
|
+
*/
|
|
816
|
+
function formatSize(bytes) {
|
|
817
|
+
if (bytes < 1024) {
|
|
818
|
+
return `${bytes} B`;
|
|
819
|
+
} else if (bytes < 1024 * 1024) {
|
|
820
|
+
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
821
|
+
} else {
|
|
822
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
823
|
+
}
|
|
396
824
|
}
|
|
397
825
|
|
|
398
826
|
/**
|
|
@@ -415,7 +843,7 @@ function convertJSON(chunk) {
|
|
|
415
843
|
|
|
416
844
|
function toggleUIToolbar(show) {
|
|
417
845
|
isConnected = show;
|
|
418
|
-
for (let i = 0; i <
|
|
846
|
+
for (let i = 0; i < progress.length; i++) {
|
|
419
847
|
progress[i].classList.add("hidden");
|
|
420
848
|
progress[i].querySelector("div").style.width = "0";
|
|
421
849
|
}
|
|
@@ -425,14 +853,27 @@ function toggleUIToolbar(show) {
|
|
|
425
853
|
appDiv.classList.remove("connected");
|
|
426
854
|
}
|
|
427
855
|
butErase.disabled = !show;
|
|
856
|
+
butReadFlash.disabled = !show;
|
|
857
|
+
butReadPartitions.disabled = !show;
|
|
428
858
|
}
|
|
429
859
|
|
|
430
860
|
function toggleUIConnected(connected) {
|
|
431
861
|
let lbl = "Connect";
|
|
862
|
+
const header = document.querySelector(".header");
|
|
863
|
+
const main = document.querySelector(".main");
|
|
864
|
+
|
|
432
865
|
if (connected) {
|
|
433
866
|
lbl = "Disconnect";
|
|
867
|
+
// Auto-hide header after connection
|
|
868
|
+
setTimeout(() => {
|
|
869
|
+
header.classList.add("header-hidden");
|
|
870
|
+
main.classList.add("no-header-padding");
|
|
871
|
+
}, 2000); // Hide after 2 seconds
|
|
434
872
|
} else {
|
|
435
873
|
toggleUIToolbar(false);
|
|
874
|
+
// Show header when disconnected
|
|
875
|
+
header.classList.remove("header-hidden");
|
|
876
|
+
main.classList.remove("no-header-padding");
|
|
436
877
|
}
|
|
437
878
|
butConnect.textContent = lbl;
|
|
438
879
|
}
|
|
@@ -443,6 +884,10 @@ function loadAllSettings() {
|
|
|
443
884
|
baudRate.value = loadSetting("baudrate", 1500000);
|
|
444
885
|
darkMode.checked = loadSetting("darkmode", false);
|
|
445
886
|
debugMode.checked = loadSetting("debugmode", true);
|
|
887
|
+
showLog.checked = loadSetting("showlog", false);
|
|
888
|
+
|
|
889
|
+
// Apply show log setting
|
|
890
|
+
updateLogVisibility();
|
|
446
891
|
}
|
|
447
892
|
|
|
448
893
|
function loadSetting(setting, defaultValue) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tasmota-webserial-esptool",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Flash ESP devices using WebSerial",
|
|
3
|
+
"version": "7.0.0",
|
|
4
|
+
"description": "Flash & Read ESP devices using WebSerial",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -20,16 +20,16 @@
|
|
|
20
20
|
"@rollup/plugin-json": "^6.1.0",
|
|
21
21
|
"@rollup/plugin-node-resolve": "^16.0.0",
|
|
22
22
|
"@rollup/plugin-terser": "^0.4.4",
|
|
23
|
-
"@rollup/plugin-typescript": "^12.
|
|
23
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
24
24
|
"@types/pako": "^2.0.4",
|
|
25
25
|
"@types/w3c-web-serial": "^1.0.7",
|
|
26
|
-
"prettier": "^3.
|
|
27
|
-
"rollup": "^4.
|
|
26
|
+
"prettier": "^3.7.3",
|
|
27
|
+
"rollup": "^4.53.3",
|
|
28
28
|
"serve": "^14.2.4",
|
|
29
29
|
"typescript": "^5.7.3"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@types/node": "^24.
|
|
32
|
+
"@types/node": "^24.10.1",
|
|
33
33
|
"pako": "^2.1.0",
|
|
34
34
|
"tslib": "^2.8.1"
|
|
35
35
|
}
|