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
package/dist/flash.js
CHANGED
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
import { ESPLoader } from "tasmota-webserial-esptool";
|
|
2
2
|
import { getChipFamilyName } from "./util/chip-family-name";
|
|
3
3
|
import { sleep } from "./util/sleep";
|
|
4
|
-
|
|
5
|
-
* Check if a serial port is an ESP32-S2 Native USB device in TinyUSB CDC mode
|
|
6
|
-
* VID 0x303a = Espressif
|
|
7
|
-
* PID 0x0002 = ESP32-S2 TinyUSB CDC
|
|
8
|
-
*/
|
|
9
|
-
const isESP32S2NativeUSB = (port) => {
|
|
10
|
-
const info = port.getInfo();
|
|
11
|
-
return info.usbVendorId === 0x303a && info.usbProductId === 0x0002;
|
|
12
|
-
};
|
|
4
|
+
import { isESP32S2NativeUSB, isESP32S2ReconnectError, closeAndForgetPort, } from "./util/esp32s2-reconnect";
|
|
13
5
|
export const flash = async (onEvent, port, logger, manifestPath, eraseFirst, firmwareBuffer, baudRate) => {
|
|
14
6
|
let manifest;
|
|
15
7
|
let build;
|
|
16
8
|
let chipFamily;
|
|
17
9
|
let chipVariant = null;
|
|
18
|
-
let esp32s2ReconnectRequired = false;
|
|
19
10
|
const isS2NativeUSB = isESP32S2NativeUSB(port);
|
|
20
11
|
const fireStateEvent = (stateUpdate) => onEvent({
|
|
21
12
|
...stateUpdate,
|
|
@@ -38,7 +29,6 @@ export const flash = async (onEvent, port, logger, manifestPath, eraseFirst, fir
|
|
|
38
29
|
window.esploader = esploader;
|
|
39
30
|
// ESP32-S2 Native USB event handler - listen on ESPLoader instance
|
|
40
31
|
const handleESP32S2Reconnect = () => {
|
|
41
|
-
esp32s2ReconnectRequired = true;
|
|
42
32
|
logger.log("ESP32-S2 Native USB disconnect detected - reconnection required");
|
|
43
33
|
};
|
|
44
34
|
// Register event listener for ESP32-S2 Native USB reconnect on ESPLoader
|
|
@@ -63,26 +53,10 @@ export const flash = async (onEvent, port, logger, manifestPath, eraseFirst, fir
|
|
|
63
53
|
catch (err) {
|
|
64
54
|
logger.error(err);
|
|
65
55
|
// Check if this is an ESP32-S2 Native USB reconnect situation
|
|
66
|
-
|
|
67
|
-
// 1. ESP32-S2 in TinyUSB CDC mode was reset to bootloader
|
|
68
|
-
// 2. ESP32-S2 disconnected during initialization
|
|
69
|
-
if (isS2NativeUSB &&
|
|
70
|
-
(esp32s2ReconnectRequired || String(err).includes("reconnect required"))) {
|
|
56
|
+
if (isS2NativeUSB && isESP32S2ReconnectError(err)) {
|
|
71
57
|
cleanup();
|
|
72
|
-
// Close the old port
|
|
73
|
-
|
|
74
|
-
await port.close();
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
// Port may already be closed
|
|
78
|
-
}
|
|
79
|
-
// Forget the old port to allow reselection
|
|
80
|
-
try {
|
|
81
|
-
await port.forget();
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
// Forget may not be supported or port already released
|
|
85
|
-
}
|
|
58
|
+
// Close and forget the old port
|
|
59
|
+
await closeAndForgetPort(port);
|
|
86
60
|
// Fire reconnect event to trigger port reselection dialog
|
|
87
61
|
fireStateEvent({
|
|
88
62
|
state: "esp32_s2_usb_reconnect" /* FlashStateType.ESP32_S2_USB_RECONNECT */,
|
package/dist/install-dialog.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import "./components/ewt-icon-button";
|
|
|
8
8
|
import "./components/ewt-textfield";
|
|
9
9
|
import "./components/ewt-select";
|
|
10
10
|
import "./components/ewt-list-item";
|
|
11
|
+
import "./components/ewt-littlefs-manager";
|
|
11
12
|
import "./pages/ewt-page-progress";
|
|
12
13
|
import "./pages/ewt-page-message";
|
|
13
14
|
import { Logger, Manifest } from "./const.js";
|
|
@@ -35,6 +36,9 @@ export declare class EwtInstallDialog extends LitElement {
|
|
|
35
36
|
private _esp32s2ReconnectInProgress;
|
|
36
37
|
private _ssids?;
|
|
37
38
|
private _selectedSsid;
|
|
39
|
+
private _partitions?;
|
|
40
|
+
private _selectedPartition?;
|
|
41
|
+
private _espStub?;
|
|
38
42
|
protected render(): TemplateResult<1>;
|
|
39
43
|
_renderProgress(label: string | TemplateResult, progress?: number): TemplateResult<1>;
|
|
40
44
|
_renderError(label: string): [string, TemplateResult, boolean];
|
|
@@ -44,6 +48,13 @@ export declare class EwtInstallDialog extends LitElement {
|
|
|
44
48
|
_renderAskErase(): [string | undefined, TemplateResult];
|
|
45
49
|
_renderInstall(): [string | undefined, TemplateResult, boolean, boolean];
|
|
46
50
|
_renderLogs(): [string | undefined, TemplateResult, boolean];
|
|
51
|
+
_renderPartitions(): [string | undefined, TemplateResult, boolean];
|
|
52
|
+
_renderLittleFS(): [string | undefined, TemplateResult, boolean, boolean];
|
|
53
|
+
_renderESP32S2Reconnect(): [string | undefined, TemplateResult, boolean];
|
|
54
|
+
private _handleESP32S2ReconnectClick;
|
|
55
|
+
private _readPartitionTable;
|
|
56
|
+
private _openFilesystem;
|
|
57
|
+
private _formatSize;
|
|
47
58
|
willUpdate(changedProps: PropertyValues): void;
|
|
48
59
|
protected firstUpdated(changedProps: PropertyValues): void;
|
|
49
60
|
protected updated(changedProps: PropertyValues): void;
|
package/dist/install-dialog.js
CHANGED
|
@@ -10,6 +10,7 @@ import "./components/ewt-icon-button";
|
|
|
10
10
|
import "./components/ewt-textfield";
|
|
11
11
|
import "./components/ewt-select";
|
|
12
12
|
import "./components/ewt-list-item";
|
|
13
|
+
import "./components/ewt-littlefs-manager";
|
|
13
14
|
import "./pages/ewt-page-progress";
|
|
14
15
|
import "./pages/ewt-page-message";
|
|
15
16
|
import { chipIcon, closeIcon, firmwareIcon } from "./components/svg";
|
|
@@ -21,6 +22,9 @@ import { fireEvent } from "./util/fire-event";
|
|
|
21
22
|
import { sleep } from "./util/sleep";
|
|
22
23
|
import { downloadManifest } from "./util/manifest";
|
|
23
24
|
import { dialogStyles } from "./styles";
|
|
25
|
+
import { parsePartitionTable } from "./partition.js";
|
|
26
|
+
import { detectFilesystemType } from "./util/partition.js";
|
|
27
|
+
import { isESP32S2NativeUSB, isESP32S2ReconnectError, closeAndForgetPort, } from "./util/esp32s2-reconnect";
|
|
24
28
|
const ERROR_ICON = "⚠️";
|
|
25
29
|
const OK_ICON = "🎉";
|
|
26
30
|
export class EwtInstallDialog extends LitElement {
|
|
@@ -82,6 +86,15 @@ export class EwtInstallDialog extends LitElement {
|
|
|
82
86
|
else if (this._state === "LOGS") {
|
|
83
87
|
[heading, content, hideActions] = this._renderLogs();
|
|
84
88
|
}
|
|
89
|
+
else if (this._state === "PARTITIONS") {
|
|
90
|
+
[heading, content, hideActions] = this._renderPartitions();
|
|
91
|
+
}
|
|
92
|
+
else if (this._state === "LITTLEFS") {
|
|
93
|
+
[heading, content, hideActions, allowClosing] = this._renderLittleFS();
|
|
94
|
+
}
|
|
95
|
+
else if (this._state === "ESP32S2_RECONNECT") {
|
|
96
|
+
[heading, content, hideActions] = this._renderESP32S2Reconnect();
|
|
97
|
+
}
|
|
85
98
|
return html `
|
|
86
99
|
<ewt-dialog
|
|
87
100
|
open
|
|
@@ -215,6 +228,15 @@ export class EwtInstallDialog extends LitElement {
|
|
|
215
228
|
}}
|
|
216
229
|
></ewt-button>
|
|
217
230
|
</div>
|
|
231
|
+
<div>
|
|
232
|
+
<ewt-button
|
|
233
|
+
label="Manage Filesystem"
|
|
234
|
+
@click=${() => {
|
|
235
|
+
this._state = "PARTITIONS";
|
|
236
|
+
this._readPartitionTable();
|
|
237
|
+
}}
|
|
238
|
+
></ewt-button>
|
|
239
|
+
</div>
|
|
218
240
|
${this._isSameFirmware && this._manifest.funding_url
|
|
219
241
|
? html `
|
|
220
242
|
<div>
|
|
@@ -276,6 +298,16 @@ export class EwtInstallDialog extends LitElement {
|
|
|
276
298
|
}}
|
|
277
299
|
></ewt-button>
|
|
278
300
|
</div>
|
|
301
|
+
|
|
302
|
+
<div>
|
|
303
|
+
<ewt-button
|
|
304
|
+
label="Manage Filesystem"
|
|
305
|
+
@click=${() => {
|
|
306
|
+
this._state = "PARTITIONS";
|
|
307
|
+
this._readPartitionTable();
|
|
308
|
+
}}
|
|
309
|
+
></ewt-button>
|
|
310
|
+
</div>
|
|
279
311
|
</div>
|
|
280
312
|
`;
|
|
281
313
|
return [heading, content, hideActions, allowClosing];
|
|
@@ -651,6 +683,263 @@ export class EwtInstallDialog extends LitElement {
|
|
|
651
683
|
`;
|
|
652
684
|
return [heading, content, hideActions];
|
|
653
685
|
}
|
|
686
|
+
_renderPartitions() {
|
|
687
|
+
const heading = "Partition Table";
|
|
688
|
+
let content;
|
|
689
|
+
const hideActions = false;
|
|
690
|
+
if (this._busy) {
|
|
691
|
+
content = this._renderProgress("Reading partition table...");
|
|
692
|
+
}
|
|
693
|
+
else if (!this._partitions || this._partitions.length === 0) {
|
|
694
|
+
content = html `
|
|
695
|
+
<ewt-page-message
|
|
696
|
+
.icon=${ERROR_ICON}
|
|
697
|
+
label="No partitions found"
|
|
698
|
+
></ewt-page-message>
|
|
699
|
+
<ewt-button
|
|
700
|
+
slot="primaryAction"
|
|
701
|
+
label="Back"
|
|
702
|
+
@click=${() => {
|
|
703
|
+
this._state = "DASHBOARD";
|
|
704
|
+
}}
|
|
705
|
+
></ewt-button>
|
|
706
|
+
`;
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
content = html `
|
|
710
|
+
<div class="partition-list">
|
|
711
|
+
<table class="partition-table">
|
|
712
|
+
<thead>
|
|
713
|
+
<tr>
|
|
714
|
+
<th>Name</th>
|
|
715
|
+
<th>Type</th>
|
|
716
|
+
<th>SubType</th>
|
|
717
|
+
<th>Offset</th>
|
|
718
|
+
<th>Size</th>
|
|
719
|
+
<th>Action</th>
|
|
720
|
+
</tr>
|
|
721
|
+
</thead>
|
|
722
|
+
<tbody>
|
|
723
|
+
${this._partitions.map((partition) => html `
|
|
724
|
+
<tr>
|
|
725
|
+
<td>${partition.name}</td>
|
|
726
|
+
<td>${partition.typeName}</td>
|
|
727
|
+
<td>${partition.subtypeName}</td>
|
|
728
|
+
<td>0x${partition.offset.toString(16)}</td>
|
|
729
|
+
<td>${this._formatSize(partition.size)}</td>
|
|
730
|
+
<td>
|
|
731
|
+
${partition.type === 0x01 && partition.subtype === 0x82
|
|
732
|
+
? html `
|
|
733
|
+
<ewt-button
|
|
734
|
+
label="Open FS"
|
|
735
|
+
@click=${() => this._openFilesystem(partition)}
|
|
736
|
+
></ewt-button>
|
|
737
|
+
`
|
|
738
|
+
: ""}
|
|
739
|
+
</td>
|
|
740
|
+
</tr>
|
|
741
|
+
`)}
|
|
742
|
+
</tbody>
|
|
743
|
+
</table>
|
|
744
|
+
</div>
|
|
745
|
+
<ewt-button
|
|
746
|
+
slot="primaryAction"
|
|
747
|
+
label="Back"
|
|
748
|
+
@click=${() => {
|
|
749
|
+
this._state = "DASHBOARD";
|
|
750
|
+
}}
|
|
751
|
+
></ewt-button>
|
|
752
|
+
`;
|
|
753
|
+
}
|
|
754
|
+
return [heading, content, hideActions];
|
|
755
|
+
}
|
|
756
|
+
_renderLittleFS() {
|
|
757
|
+
const heading = undefined;
|
|
758
|
+
const hideActions = true;
|
|
759
|
+
const allowClosing = true;
|
|
760
|
+
const content = html `
|
|
761
|
+
<ewt-littlefs-manager
|
|
762
|
+
.partition=${this._selectedPartition}
|
|
763
|
+
.espStub=${this._espStub}
|
|
764
|
+
.logger=${this.logger}
|
|
765
|
+
.onClose=${() => {
|
|
766
|
+
this._state = "PARTITIONS";
|
|
767
|
+
}}
|
|
768
|
+
></ewt-littlefs-manager>
|
|
769
|
+
`;
|
|
770
|
+
return [heading, content, hideActions, allowClosing];
|
|
771
|
+
}
|
|
772
|
+
_renderESP32S2Reconnect() {
|
|
773
|
+
const heading = "ESP32-S2 USB Port Changed";
|
|
774
|
+
const content = html `
|
|
775
|
+
<ewt-page-message
|
|
776
|
+
.icon=${"⚠️"}
|
|
777
|
+
.label=${"The ESP32-S2 has switched to USB CDC mode. Please select the new USB port to continue."}
|
|
778
|
+
></ewt-page-message>
|
|
779
|
+
<ewt-button
|
|
780
|
+
slot="primaryAction"
|
|
781
|
+
label="Select New Port"
|
|
782
|
+
@click=${this._handleESP32S2ReconnectClick}
|
|
783
|
+
></ewt-button>
|
|
784
|
+
<ewt-button
|
|
785
|
+
slot="secondaryAction"
|
|
786
|
+
label="Cancel"
|
|
787
|
+
@click=${() => {
|
|
788
|
+
this._state = "DASHBOARD";
|
|
789
|
+
}}
|
|
790
|
+
></ewt-button>
|
|
791
|
+
`;
|
|
792
|
+
const hideActions = false;
|
|
793
|
+
return [heading, content, hideActions];
|
|
794
|
+
}
|
|
795
|
+
async _handleESP32S2ReconnectClick() {
|
|
796
|
+
try {
|
|
797
|
+
this._busy = true;
|
|
798
|
+
// Request new port (this is triggered by user click, so it works)
|
|
799
|
+
this.logger.log("Requesting new port selection...");
|
|
800
|
+
const newPort = await navigator.serial.requestPort();
|
|
801
|
+
await newPort.open({ baudRate: 115200 });
|
|
802
|
+
this.logger.log("New port selected, updating...");
|
|
803
|
+
this.port = newPort;
|
|
804
|
+
// Restart partition table reading with new port
|
|
805
|
+
this._state = "PARTITIONS";
|
|
806
|
+
await this._readPartitionTable();
|
|
807
|
+
}
|
|
808
|
+
catch (err) {
|
|
809
|
+
if (err.name === "NotFoundError") {
|
|
810
|
+
this.logger.log("Port selection cancelled");
|
|
811
|
+
this._state = "DASHBOARD";
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
this._error = `Failed to reconnect: ${err.message}`;
|
|
815
|
+
this._state = "ERROR";
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
finally {
|
|
819
|
+
this._busy = false;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
async _readPartitionTable() {
|
|
823
|
+
const PARTITION_TABLE_OFFSET = 0x8000;
|
|
824
|
+
const PARTITION_TABLE_SIZE = 0x1000;
|
|
825
|
+
this._busy = true;
|
|
826
|
+
this._partitions = undefined;
|
|
827
|
+
try {
|
|
828
|
+
this.logger.log("Reading partition table from 0x8000...");
|
|
829
|
+
// Import ESPLoader from the package
|
|
830
|
+
const { ESPLoader } = await import("tasmota-webserial-esptool");
|
|
831
|
+
let currentPort = this.port;
|
|
832
|
+
let esploader = new ESPLoader(currentPort, {
|
|
833
|
+
log: (msg, ...args) => this.logger.log(msg, ...args),
|
|
834
|
+
debug: (msg, ...args) => { var _a, _b; return (_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, msg, ...args); },
|
|
835
|
+
error: (msg, ...args) => this.logger.error(msg, ...args),
|
|
836
|
+
});
|
|
837
|
+
// Set up ESP32-S2 reconnect handler BEFORE initialize
|
|
838
|
+
const reconnectHandler = async (event) => {
|
|
839
|
+
this.logger.log("ESP32-S2 USB reconnect event:", event.detail.message);
|
|
840
|
+
await closeAndForgetPort(currentPort);
|
|
841
|
+
this.logger.log("Please select the new ESP32-S2 USB CDC port");
|
|
842
|
+
};
|
|
843
|
+
esploader.addEventListener("esp32s2-usb-reconnect", reconnectHandler, {
|
|
844
|
+
once: true,
|
|
845
|
+
});
|
|
846
|
+
// Initialize ESP loader
|
|
847
|
+
this.logger.log("Initializing ESP loader...");
|
|
848
|
+
try {
|
|
849
|
+
await esploader.initialize();
|
|
850
|
+
this.logger.log("ESP loader initialized successfully");
|
|
851
|
+
}
|
|
852
|
+
catch (err) {
|
|
853
|
+
// Check if this is an ESP32-S2 reconnect scenario
|
|
854
|
+
if (isESP32S2NativeUSB(currentPort) && isESP32S2ReconnectError(err)) {
|
|
855
|
+
this.logger.log("ESP32-S2 USB port changed - user needs to select new port");
|
|
856
|
+
// Close and forget old port
|
|
857
|
+
await closeAndForgetPort(currentPort);
|
|
858
|
+
// Show UI with button for user to click
|
|
859
|
+
this._busy = false;
|
|
860
|
+
this._state = "ESP32S2_RECONNECT";
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
throw err;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
this.logger.log("Running stub...");
|
|
868
|
+
const espStub = await esploader.runStub();
|
|
869
|
+
this._espStub = espStub;
|
|
870
|
+
// Add a small delay after stub is running
|
|
871
|
+
await sleep(500);
|
|
872
|
+
this.logger.log("Reading flash data...");
|
|
873
|
+
const data = await espStub.readFlash(PARTITION_TABLE_OFFSET, PARTITION_TABLE_SIZE);
|
|
874
|
+
const partitions = parsePartitionTable(data);
|
|
875
|
+
if (partitions.length === 0) {
|
|
876
|
+
this.logger.error("No valid partition table found");
|
|
877
|
+
this._partitions = [];
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
this.logger.log(`Found ${partitions.length} partition(s)`);
|
|
881
|
+
this._partitions = partitions;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
catch (e) {
|
|
885
|
+
this.logger.error(`Failed to read partition table: ${e.message || e}`);
|
|
886
|
+
if (e.message === "Port selection cancelled") {
|
|
887
|
+
this._error = "Port selection cancelled";
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
this._error = `Failed to read partition table: ${e.message || e}. Try resetting your device.`;
|
|
891
|
+
}
|
|
892
|
+
this._state = "ERROR";
|
|
893
|
+
}
|
|
894
|
+
finally {
|
|
895
|
+
this._busy = false;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async _openFilesystem(partition) {
|
|
899
|
+
try {
|
|
900
|
+
this._busy = true;
|
|
901
|
+
this.logger.log(`Detecting filesystem type for partition "${partition.name}"...`);
|
|
902
|
+
// Check if ESP stub is still available
|
|
903
|
+
if (!this._espStub) {
|
|
904
|
+
throw new Error("ESP stub not available. Please reconnect.");
|
|
905
|
+
}
|
|
906
|
+
const fsType = await detectFilesystemType(this._espStub, partition.offset, partition.size, this.logger);
|
|
907
|
+
this.logger.log(`Detected filesystem: ${fsType}`);
|
|
908
|
+
if (fsType === "littlefs") {
|
|
909
|
+
this._selectedPartition = partition;
|
|
910
|
+
this._state = "LITTLEFS";
|
|
911
|
+
}
|
|
912
|
+
else if (fsType === "spiffs") {
|
|
913
|
+
this.logger.error("SPIFFS support not yet implemented. Use LittleFS partitions.");
|
|
914
|
+
this._error = "SPIFFS support not yet implemented";
|
|
915
|
+
this._state = "ERROR";
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
this.logger.error("Unknown filesystem type. Cannot open partition.");
|
|
919
|
+
this._error = "Unknown filesystem type";
|
|
920
|
+
this._state = "ERROR";
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
catch (e) {
|
|
924
|
+
this.logger.error(`Failed to open filesystem: ${e.message || e}`);
|
|
925
|
+
this._error = `Failed to open filesystem: ${e.message || e}`;
|
|
926
|
+
this._state = "ERROR";
|
|
927
|
+
}
|
|
928
|
+
finally {
|
|
929
|
+
this._busy = false;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
_formatSize(bytes) {
|
|
933
|
+
if (bytes < 1024) {
|
|
934
|
+
return `${bytes} B`;
|
|
935
|
+
}
|
|
936
|
+
else if (bytes < 1024 * 1024) {
|
|
937
|
+
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
654
943
|
willUpdate(changedProps) {
|
|
655
944
|
if (!changedProps.has("_state")) {
|
|
656
945
|
return;
|
|
@@ -972,6 +1261,44 @@ EwtInstallDialog.styles = [
|
|
|
972
1261
|
width: calc(80vw - 48px);
|
|
973
1262
|
height: 80vh;
|
|
974
1263
|
}
|
|
1264
|
+
:host([state="PARTITIONS"]) ewt-dialog {
|
|
1265
|
+
--mdc-dialog-max-width: 800px;
|
|
1266
|
+
}
|
|
1267
|
+
:host([state="LITTLEFS"]) ewt-dialog {
|
|
1268
|
+
--mdc-dialog-max-width: 95vw;
|
|
1269
|
+
--mdc-dialog-max-height: 90vh;
|
|
1270
|
+
}
|
|
1271
|
+
:host([state="LITTLEFS"]) .mdc-dialog__content {
|
|
1272
|
+
padding: 10px 20px;
|
|
1273
|
+
}
|
|
1274
|
+
:host([state="LITTLEFS"]) ewt-littlefs-manager {
|
|
1275
|
+
display: block;
|
|
1276
|
+
max-width: 100%;
|
|
1277
|
+
}
|
|
1278
|
+
.partition-list {
|
|
1279
|
+
max-height: 60vh;
|
|
1280
|
+
overflow-y: auto;
|
|
1281
|
+
}
|
|
1282
|
+
.partition-table {
|
|
1283
|
+
width: 100%;
|
|
1284
|
+
border-collapse: collapse;
|
|
1285
|
+
margin: 16px 0;
|
|
1286
|
+
}
|
|
1287
|
+
.partition-table th,
|
|
1288
|
+
.partition-table td {
|
|
1289
|
+
padding: 8px 12px;
|
|
1290
|
+
text-align: left;
|
|
1291
|
+
border: 1px solid #ccc;
|
|
1292
|
+
}
|
|
1293
|
+
.partition-table th {
|
|
1294
|
+
font-weight: 600;
|
|
1295
|
+
background-color: #f0f0f0;
|
|
1296
|
+
position: sticky;
|
|
1297
|
+
top: 0;
|
|
1298
|
+
}
|
|
1299
|
+
.partition-table tbody tr:hover {
|
|
1300
|
+
background-color: rgba(3, 169, 244, 0.1);
|
|
1301
|
+
}
|
|
975
1302
|
`,
|
|
976
1303
|
];
|
|
977
1304
|
__decorate([
|
|
@@ -1004,4 +1331,13 @@ __decorate([
|
|
|
1004
1331
|
__decorate([
|
|
1005
1332
|
state()
|
|
1006
1333
|
], EwtInstallDialog.prototype, "_selectedSsid", void 0);
|
|
1334
|
+
__decorate([
|
|
1335
|
+
state()
|
|
1336
|
+
], EwtInstallDialog.prototype, "_partitions", void 0);
|
|
1337
|
+
__decorate([
|
|
1338
|
+
state()
|
|
1339
|
+
], EwtInstallDialog.prototype, "_selectedPartition", void 0);
|
|
1340
|
+
__decorate([
|
|
1341
|
+
state()
|
|
1342
|
+
], EwtInstallDialog.prototype, "_espStub", void 0);
|
|
1007
1343
|
customElements.define("ewt-install-dialog", EwtInstallDialog);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESP32 Partition Table Parser
|
|
3
|
+
* Based on ESP-IDF partition table format
|
|
4
|
+
*/
|
|
5
|
+
export interface Partition {
|
|
6
|
+
name: string;
|
|
7
|
+
type: number;
|
|
8
|
+
subtype: number;
|
|
9
|
+
offset: number;
|
|
10
|
+
size: number;
|
|
11
|
+
flags: number;
|
|
12
|
+
typeName: string;
|
|
13
|
+
subtypeName: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parse the entire partition table
|
|
17
|
+
*/
|
|
18
|
+
export declare function parsePartitionTable(data: Uint8Array): Partition[];
|
|
19
|
+
/**
|
|
20
|
+
* Get the default partition table offset
|
|
21
|
+
*/
|
|
22
|
+
export declare function getPartitionTableOffset(): number;
|
|
23
|
+
/**
|
|
24
|
+
* Format size in human-readable format
|
|
25
|
+
*/
|
|
26
|
+
export declare function formatSize(bytes: number): string;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESP32 Partition Table Parser
|
|
3
|
+
* Based on ESP-IDF partition table format
|
|
4
|
+
*/
|
|
5
|
+
// Partition types
|
|
6
|
+
const PARTITION_TYPES = {
|
|
7
|
+
0x00: "app",
|
|
8
|
+
0x01: "data",
|
|
9
|
+
};
|
|
10
|
+
// App subtypes
|
|
11
|
+
const APP_SUBTYPES = {
|
|
12
|
+
0x00: "factory",
|
|
13
|
+
0x10: "ota_0",
|
|
14
|
+
0x11: "ota_1",
|
|
15
|
+
0x12: "ota_2",
|
|
16
|
+
0x13: "ota_3",
|
|
17
|
+
0x14: "ota_4",
|
|
18
|
+
0x15: "ota_5",
|
|
19
|
+
0x16: "ota_6",
|
|
20
|
+
0x17: "ota_7",
|
|
21
|
+
0x18: "ota_8",
|
|
22
|
+
0x19: "ota_9",
|
|
23
|
+
0x1a: "ota_10",
|
|
24
|
+
0x1b: "ota_11",
|
|
25
|
+
0x1c: "ota_12",
|
|
26
|
+
0x1d: "ota_13",
|
|
27
|
+
0x1e: "ota_14",
|
|
28
|
+
0x1f: "ota_15",
|
|
29
|
+
0x20: "test",
|
|
30
|
+
};
|
|
31
|
+
// Data subtypes
|
|
32
|
+
const DATA_SUBTYPES = {
|
|
33
|
+
0x00: "ota",
|
|
34
|
+
0x01: "phy",
|
|
35
|
+
0x02: "nvs",
|
|
36
|
+
0x03: "coredump",
|
|
37
|
+
0x04: "nvs_keys",
|
|
38
|
+
0x05: "efuse",
|
|
39
|
+
0x80: "esphttpd",
|
|
40
|
+
0x81: "fat",
|
|
41
|
+
0x82: "spiffs",
|
|
42
|
+
};
|
|
43
|
+
const PARTITION_TABLE_OFFSET = 0x8000; // Default partition table offset
|
|
44
|
+
const PARTITION_ENTRY_SIZE = 32;
|
|
45
|
+
const PARTITION_MAGIC = 0x50aa;
|
|
46
|
+
/**
|
|
47
|
+
* Parse a single partition entry from binary data
|
|
48
|
+
*/
|
|
49
|
+
function parsePartitionEntry(data) {
|
|
50
|
+
if (data.length < PARTITION_ENTRY_SIZE) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// Check magic bytes
|
|
54
|
+
const magic = (data[0] | (data[1] << 8)) & 0xffff;
|
|
55
|
+
if (magic !== PARTITION_MAGIC) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const type = data[2];
|
|
59
|
+
const subtype = data[3];
|
|
60
|
+
const offset = data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24);
|
|
61
|
+
const size = data[8] | (data[9] << 8) | (data[10] << 16) | (data[11] << 24);
|
|
62
|
+
// Name is at offset 12, max 16 bytes, null-terminated
|
|
63
|
+
let name = "";
|
|
64
|
+
for (let i = 12; i < 28; i++) {
|
|
65
|
+
if (data[i] === 0)
|
|
66
|
+
break;
|
|
67
|
+
name += String.fromCharCode(data[i]);
|
|
68
|
+
}
|
|
69
|
+
const flags = data[28] | (data[29] << 8) | (data[30] << 16) | (data[31] << 24);
|
|
70
|
+
// Get type and subtype names
|
|
71
|
+
const typeName = PARTITION_TYPES[type] || `unknown(0x${type.toString(16)})`;
|
|
72
|
+
let subtypeName = "";
|
|
73
|
+
if (type === 0x00) {
|
|
74
|
+
subtypeName = APP_SUBTYPES[subtype] || `unknown(0x${subtype.toString(16)})`;
|
|
75
|
+
}
|
|
76
|
+
else if (type === 0x01) {
|
|
77
|
+
subtypeName =
|
|
78
|
+
DATA_SUBTYPES[subtype] || `unknown(0x${subtype.toString(16)})`;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
subtypeName = `0x${subtype.toString(16)}`;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
name,
|
|
85
|
+
type,
|
|
86
|
+
subtype,
|
|
87
|
+
offset,
|
|
88
|
+
size,
|
|
89
|
+
flags,
|
|
90
|
+
typeName,
|
|
91
|
+
subtypeName,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Parse the entire partition table
|
|
96
|
+
*/
|
|
97
|
+
export function parsePartitionTable(data) {
|
|
98
|
+
const partitions = [];
|
|
99
|
+
for (let i = 0; i < data.length; i += PARTITION_ENTRY_SIZE) {
|
|
100
|
+
const entryData = data.slice(i, i + PARTITION_ENTRY_SIZE);
|
|
101
|
+
const partition = parsePartitionEntry(entryData);
|
|
102
|
+
if (partition === null) {
|
|
103
|
+
// End of partition table or invalid entry
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
partitions.push(partition);
|
|
107
|
+
}
|
|
108
|
+
return partitions;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get the default partition table offset
|
|
112
|
+
*/
|
|
113
|
+
export function getPartitionTableOffset() {
|
|
114
|
+
return PARTITION_TABLE_OFFSET;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Format size in human-readable format
|
|
118
|
+
*/
|
|
119
|
+
export function formatSize(bytes) {
|
|
120
|
+
if (bytes < 1024) {
|
|
121
|
+
return `${bytes} B`;
|
|
122
|
+
}
|
|
123
|
+
else if (bytes < 1024 * 1024) {
|
|
124
|
+
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESP32-S2 Native USB Reconnect Utilities
|
|
3
|
+
*
|
|
4
|
+
* The ESP32-S2 can switch between ROM bootloader mode and USB CDC mode,
|
|
5
|
+
* causing the serial port to change. These utilities help detect and handle
|
|
6
|
+
* this scenario consistently across the application.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Check if a serial port is an ESP32-S2 Native USB device
|
|
10
|
+
* VID 0x303a = Espressif
|
|
11
|
+
* PID 0x0002 = ESP32-S2 Native USB (ROM bootloader)
|
|
12
|
+
*/
|
|
13
|
+
export declare const isESP32S2NativeUSB: (port: SerialPort) => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Check if an error is likely caused by ESP32-S2 port switching
|
|
16
|
+
*/
|
|
17
|
+
export declare const isESP32S2ReconnectError: (error: any) => boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Close and forget a serial port
|
|
20
|
+
* This releases the port so a new one can be selected
|
|
21
|
+
*/
|
|
22
|
+
export declare const closeAndForgetPort: (port: SerialPort) => Promise<void>;
|