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.
Files changed (101) hide show
  1. package/.vscode/settings.json +2 -0
  2. package/README.md +4 -3
  3. package/READ_FLASH_FEATURE.md +130 -0
  4. package/css/light.css +11 -0
  5. package/css/style.css +213 -45
  6. package/dist/const.d.ts +42 -3
  7. package/dist/const.js +102 -4
  8. package/dist/esp_loader.d.ts +27 -3
  9. package/dist/esp_loader.js +376 -17
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.js +1 -2
  12. package/dist/partition.d.ts +26 -0
  13. package/dist/partition.js +129 -0
  14. package/dist/stubs/esp32.json +4 -4
  15. package/dist/stubs/esp32c2.json +4 -4
  16. package/dist/stubs/esp32c3.json +4 -4
  17. package/dist/stubs/esp32c5.json +4 -4
  18. package/dist/stubs/esp32c6.json +4 -4
  19. package/dist/stubs/esp32c61.json +4 -4
  20. package/dist/stubs/esp32h2.json +4 -4
  21. package/dist/stubs/esp32p4.json +4 -4
  22. package/dist/stubs/esp32p4r3.json +4 -4
  23. package/dist/stubs/esp32s2.json +4 -4
  24. package/dist/stubs/esp32s3.json +4 -4
  25. package/dist/stubs/esp8266.json +3 -3
  26. package/dist/stubs/index.d.ts +1 -1
  27. package/dist/stubs/index.js +7 -1
  28. package/dist/web/esp32-CijhsJH1.js +1 -0
  29. package/dist/web/esp32c2-C17SM4gO.js +1 -0
  30. package/dist/web/esp32c3-DxRGijbg.js +1 -0
  31. package/dist/web/esp32c5-3mDOIGa4.js +1 -0
  32. package/dist/web/esp32c6-h6U0SQTm.js +1 -0
  33. package/dist/web/esp32c61-BKtexhPZ.js +1 -0
  34. package/dist/web/esp32h2-RtuWSEmP.js +1 -0
  35. package/dist/web/esp32p4-5nkIjxqJ.js +1 -0
  36. package/dist/web/esp32p4r3-CpHBYEwI.js +1 -0
  37. package/dist/web/esp32s2-IiDBtXxo.js +1 -0
  38. package/dist/web/esp32s3-6yv5yxum.js +1 -0
  39. package/dist/web/esp8266-CUwxJpGa.js +1 -0
  40. package/dist/web/index.js +1 -1
  41. package/index.html +158 -34
  42. package/js/modules/esp32-CijhsJH1.js +1 -0
  43. package/js/modules/esp32c2-C17SM4gO.js +1 -0
  44. package/js/modules/esp32c3-DxRGijbg.js +1 -0
  45. package/js/modules/esp32c5-3mDOIGa4.js +1 -0
  46. package/js/modules/esp32c6-h6U0SQTm.js +1 -0
  47. package/js/modules/esp32c61-BKtexhPZ.js +1 -0
  48. package/js/modules/esp32h2-RtuWSEmP.js +1 -0
  49. package/js/modules/esp32p4-5nkIjxqJ.js +1 -0
  50. package/js/modules/esp32p4r3-CpHBYEwI.js +1 -0
  51. package/js/modules/esp32s2-IiDBtXxo.js +1 -0
  52. package/js/modules/esp32s3-6yv5yxum.js +1 -0
  53. package/js/modules/esp8266-CUwxJpGa.js +1 -0
  54. package/js/modules/esptool.js +1 -1
  55. package/js/script.js +456 -11
  56. package/package.json +6 -6
  57. package/src/const.ts +109 -5
  58. package/src/esp_loader.ts +491 -18
  59. package/src/index.ts +3 -1
  60. package/src/partition.ts +155 -0
  61. package/src/stubs/README.md +1 -1
  62. package/src/stubs/esp32.json +4 -4
  63. package/src/stubs/esp32c2.json +4 -4
  64. package/src/stubs/esp32c3.json +4 -4
  65. package/src/stubs/esp32c5.json +4 -4
  66. package/src/stubs/esp32c6.json +4 -4
  67. package/src/stubs/esp32c61.json +4 -4
  68. package/src/stubs/esp32h2.json +4 -4
  69. package/src/stubs/esp32p4.json +4 -4
  70. package/src/stubs/esp32p4r3.json +4 -4
  71. package/src/stubs/esp32s2.json +4 -4
  72. package/src/stubs/esp32s3.json +4 -4
  73. package/src/stubs/esp8266.json +3 -3
  74. package/src/stubs/index.ts +14 -2
  75. package/BUGFIX_GET_SECURITY_INFO.md +0 -126
  76. package/IMPLEMENTATION_SUMMARY.md +0 -232
  77. package/SECURITY_INFO_EXPLANATION.md +0 -145
  78. package/dist/web/esp32-BNIFdu1P.js +0 -1
  79. package/dist/web/esp32c2-BqxquOKw.js +0 -1
  80. package/dist/web/esp32c3-BOOqe8me.js +0 -1
  81. package/dist/web/esp32c5-mcj52-K1.js +0 -1
  82. package/dist/web/esp32c6-Cg5qYgg7.js +0 -1
  83. package/dist/web/esp32c61-CzCdsydk.js +0 -1
  84. package/dist/web/esp32h2-DZa_lpff.js +0 -1
  85. package/dist/web/esp32p4-DyGqUAeZ.js +0 -1
  86. package/dist/web/esp32p4r3-Cle9QJmZ.js +0 -1
  87. package/dist/web/esp32s2-Bk4mqADi.js +0 -1
  88. package/dist/web/esp32s3-Df3OUCOC.js +0 -1
  89. package/dist/web/esp8266-CQFcqJ_a.js +0 -1
  90. package/js/modules/esp32-BNIFdu1P.js +0 -1
  91. package/js/modules/esp32c2-BqxquOKw.js +0 -1
  92. package/js/modules/esp32c3-BOOqe8me.js +0 -1
  93. package/js/modules/esp32c5-mcj52-K1.js +0 -1
  94. package/js/modules/esp32c6-Cg5qYgg7.js +0 -1
  95. package/js/modules/esp32c61-CzCdsydk.js +0 -1
  96. package/js/modules/esp32h2-DZa_lpff.js +0 -1
  97. package/js/modules/esp32p4-DyGqUAeZ.js +0 -1
  98. package/js/modules/esp32p4r3-Cle9QJmZ.js +0 -1
  99. package/js/modules/esp32s2-Bk4mqADi.js +0 -1
  100. package/js/modules/esp32s3-Df3OUCOC.js +0 -1
  101. 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("ESP Web Flasher loaded.");
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 < 4; 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 < 4; 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 < 4; 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
- if (filename.length > 17) {
385
- label.innerHTML = filename.substring(0, 14) + "&hellip;";
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&hellip;";
@@ -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 < 4; 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": "6.5.3",
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.1.4",
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.6.2",
27
- "rollup": "^4.46.3",
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.3.0",
32
+ "@types/node": "^24.10.1",
33
33
  "pako": "^2.1.0",
34
34
  "tslib": "^2.8.1"
35
35
  }