tasmota-esp-web-tools 8.1.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/.devcontainer/Dockerfile +16 -0
- package/.devcontainer/devcontainer.json +44 -0
- package/.github/dependabot.yml +10 -0
- package/.github/release-drafter.yml +12 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/npmpublish.yml +22 -0
- package/.github/workflows/release-drafter.yml +14 -0
- package/.prettierignore +1 -0
- package/README.md +68 -0
- package/dist/components/ewt-button.d.ts +9 -0
- package/dist/components/ewt-button.js +17 -0
- package/dist/components/ewt-checkbox.d.ts +9 -0
- package/dist/components/ewt-checkbox.js +6 -0
- package/dist/components/ewt-circular-progress.d.ts +9 -0
- package/dist/components/ewt-circular-progress.js +6 -0
- package/dist/components/ewt-console.d.ts +20 -0
- package/dist/components/ewt-console.js +141 -0
- package/dist/components/ewt-dialog.d.ts +9 -0
- package/dist/components/ewt-dialog.js +14 -0
- package/dist/components/ewt-formfield.d.ts +9 -0
- package/dist/components/ewt-formfield.js +6 -0
- package/dist/components/ewt-icon-button.d.ts +9 -0
- package/dist/components/ewt-icon-button.js +6 -0
- package/dist/components/ewt-list-item.d.ts +9 -0
- package/dist/components/ewt-list-item.js +6 -0
- package/dist/components/ewt-select.d.ts +9 -0
- package/dist/components/ewt-select.js +15 -0
- package/dist/components/ewt-textfield.d.ts +9 -0
- package/dist/components/ewt-textfield.js +15 -0
- package/dist/components/svg.d.ts +3 -0
- package/dist/components/svg.js +24 -0
- package/dist/connect.d.ts +3 -0
- package/dist/connect.js +33 -0
- package/dist/const.d.ts +94 -0
- package/dist/const.js +1 -0
- package/dist/flash.d.ts +4 -0
- package/dist/flash.js +191 -0
- package/dist/install-button.d.ts +17 -0
- package/dist/install-button.js +96 -0
- package/dist/install-dialog.d.ts +70 -0
- package/dist/install-dialog.js +899 -0
- package/dist/no-port-picked/index.d.ts +2 -0
- package/dist/no-port-picked/index.js +7 -0
- package/dist/no-port-picked/no-port-picked-dialog.d.ts +15 -0
- package/dist/no-port-picked/no-port-picked-dialog.js +149 -0
- package/dist/pages/ewt-page-message.d.ts +14 -0
- package/dist/pages/ewt-page-message.js +34 -0
- package/dist/pages/ewt-page-progress.d.ts +14 -0
- package/dist/pages/ewt-page-progress.js +39 -0
- package/dist/styles.d.ts +1 -0
- package/dist/styles.js +32 -0
- package/dist/util/chip-family-name.d.ts +3 -0
- package/dist/util/chip-family-name.js +17 -0
- package/dist/util/console-color.d.ts +19 -0
- package/dist/util/console-color.js +265 -0
- package/dist/util/file-download.d.ts +2 -0
- package/dist/util/file-download.js +15 -0
- package/dist/util/fire-event.d.ts +5 -0
- package/dist/util/fire-event.js +12 -0
- package/dist/util/line-break-transformer.d.ts +5 -0
- package/dist/util/line-break-transformer.js +17 -0
- package/dist/util/manifest.d.ts +2 -0
- package/dist/util/manifest.js +12 -0
- package/dist/util/sleep.d.ts +1 -0
- package/dist/util/sleep.js +1 -0
- package/dist/web/connect-3012e6dd.js +886 -0
- package/dist/web/esp32-5f88817f.js +1 -0
- package/dist/web/esp32c3-596796ad.js +1 -0
- package/dist/web/esp32s2-f7a69530.js +1 -0
- package/dist/web/esp32s3-314fbacd.js +1 -0
- package/dist/web/esp8266-c68f89af.js +1 -0
- package/dist/web/index-f110c132.js +126 -0
- package/dist/web/install-button.js +1 -0
- package/package.json +36 -0
- package/rollup.config.js +28 -0
- package/script/build +8 -0
- package/script/develop +17 -0
- package/script/stubgen.py +161 -0
- package/src/components/ewt-button.ts +25 -0
- package/src/components/ewt-checkbox.ts +14 -0
- package/src/components/ewt-circular-progress.ts +14 -0
- package/src/components/ewt-console.ts +163 -0
- package/src/components/ewt-dialog.ts +22 -0
- package/src/components/ewt-formfield.ts +14 -0
- package/src/components/ewt-icon-button.ts +14 -0
- package/src/components/ewt-list-item.ts +14 -0
- package/src/components/ewt-select.ts +23 -0
- package/src/components/ewt-textfield.ts +23 -0
- package/src/components/svg.ts +27 -0
- package/src/connect.ts +42 -0
- package/src/const.ts +101 -0
- package/src/flash.ts +240 -0
- package/src/install-button.ts +128 -0
- package/src/install-dialog.ts +981 -0
- package/src/no-port-picked/index.ts +10 -0
- package/src/no-port-picked/no-port-picked-dialog.ts +158 -0
- package/src/pages/ewt-page-message.ts +39 -0
- package/src/pages/ewt-page-progress.ts +44 -0
- package/src/styles.ts +34 -0
- package/src/util/chip-family-name.ts +28 -0
- package/src/util/console-color.ts +283 -0
- package/src/util/file-download.ts +17 -0
- package/src/util/fire-event.ts +20 -0
- package/src/util/line-break-transformer.ts +20 -0
- package/src/util/manifest.ts +18 -0
- package/src/util/sleep.ts +2 -0
- package/static/logos/canairio.png +0 -0
- package/static/logos/espeasy.png +0 -0
- package/static/logos/esphome.svg +1 -0
- package/static/logos/tasmota.svg +1 -0
- 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 +21 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { ColoredConsole, coloredConsoleStyles } from "../util/console-color";
|
|
2
|
+
import { sleep } from "../util/sleep";
|
|
3
|
+
import { LineBreakTransformer } from "../util/line-break-transformer";
|
|
4
|
+
import { Logger } from "../const";
|
|
5
|
+
|
|
6
|
+
export class EwtConsole extends HTMLElement {
|
|
7
|
+
public port!: SerialPort;
|
|
8
|
+
public logger!: Logger;
|
|
9
|
+
public allowInput = true;
|
|
10
|
+
|
|
11
|
+
private _console?: ColoredConsole;
|
|
12
|
+
private _cancelConnection?: () => Promise<void>;
|
|
13
|
+
|
|
14
|
+
public logs(): string {
|
|
15
|
+
return this._console?.logs() || "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public connectedCallback() {
|
|
19
|
+
if (this._console) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const shadowRoot = this.attachShadow({ mode: "open" });
|
|
23
|
+
|
|
24
|
+
shadowRoot.innerHTML = `
|
|
25
|
+
<style>
|
|
26
|
+
:host, input {
|
|
27
|
+
background-color: #1c1c1c;
|
|
28
|
+
color: #ddd;
|
|
29
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
|
|
30
|
+
monospace;
|
|
31
|
+
line-height: 1.45;
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
}
|
|
35
|
+
form {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
padding: 0 8px 0 16px;
|
|
39
|
+
}
|
|
40
|
+
input {
|
|
41
|
+
flex: 1;
|
|
42
|
+
padding: 4px;
|
|
43
|
+
margin: 0 8px;
|
|
44
|
+
border: 0;
|
|
45
|
+
outline: none;
|
|
46
|
+
}
|
|
47
|
+
${coloredConsoleStyles}
|
|
48
|
+
</style>
|
|
49
|
+
<div class="log"></div>
|
|
50
|
+
${
|
|
51
|
+
this.allowInput
|
|
52
|
+
? `<form>
|
|
53
|
+
>
|
|
54
|
+
<input autofocus>
|
|
55
|
+
</form>
|
|
56
|
+
`
|
|
57
|
+
: ""
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
this._console = new ColoredConsole(this.shadowRoot!.querySelector("div")!);
|
|
62
|
+
|
|
63
|
+
if (this.allowInput) {
|
|
64
|
+
const input = this.shadowRoot!.querySelector("input")!;
|
|
65
|
+
|
|
66
|
+
this.addEventListener("click", () => {
|
|
67
|
+
// Only focus input if user didn't select some text
|
|
68
|
+
if (getSelection()?.toString() === "") {
|
|
69
|
+
input.focus();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
input.addEventListener("keydown", (ev) => {
|
|
74
|
+
if (ev.key === "Enter") {
|
|
75
|
+
ev.preventDefault();
|
|
76
|
+
ev.stopPropagation();
|
|
77
|
+
this._sendCommand();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const abortController = new AbortController();
|
|
83
|
+
const connection = this._connect(abortController.signal);
|
|
84
|
+
this._cancelConnection = () => {
|
|
85
|
+
abortController.abort();
|
|
86
|
+
return connection;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async _connect(abortSignal: AbortSignal) {
|
|
91
|
+
this.logger.debug("Starting console read loop");
|
|
92
|
+
try {
|
|
93
|
+
await this.port
|
|
94
|
+
.readable!.pipeThrough(new TextDecoderStream(), {
|
|
95
|
+
signal: abortSignal,
|
|
96
|
+
})
|
|
97
|
+
.pipeThrough(new TransformStream(new LineBreakTransformer()))
|
|
98
|
+
.pipeTo(
|
|
99
|
+
new WritableStream({
|
|
100
|
+
write: (chunk) => {
|
|
101
|
+
this._console!.addLine(chunk.replace("\r", ""));
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
if (!abortSignal.aborted) {
|
|
106
|
+
this._console!.addLine("");
|
|
107
|
+
this._console!.addLine("");
|
|
108
|
+
this._console!.addLine("Terminal disconnected");
|
|
109
|
+
}
|
|
110
|
+
} catch (e) {
|
|
111
|
+
this._console!.addLine("");
|
|
112
|
+
this._console!.addLine("");
|
|
113
|
+
this._console!.addLine(`Terminal disconnected: ${e}`);
|
|
114
|
+
} finally {
|
|
115
|
+
await sleep(100);
|
|
116
|
+
this.logger.debug("Finished console read loop");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async _sendCommand() {
|
|
121
|
+
const input = this.shadowRoot!.querySelector("input")!;
|
|
122
|
+
const command = input.value;
|
|
123
|
+
const encoder = new TextEncoder();
|
|
124
|
+
const writer = this.port.writable!.getWriter();
|
|
125
|
+
await writer.write(encoder.encode(command + "\r\n"));
|
|
126
|
+
this._console!.addLine(`> ${command}\r\n`);
|
|
127
|
+
input.value = "";
|
|
128
|
+
input.focus();
|
|
129
|
+
try {
|
|
130
|
+
writer.releaseLock();
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error("Ignoring release lock error", err);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public async disconnect() {
|
|
137
|
+
if (this._cancelConnection) {
|
|
138
|
+
await this._cancelConnection();
|
|
139
|
+
this._cancelConnection = undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public async reset() {
|
|
144
|
+
this.logger.debug("Triggering reset.");
|
|
145
|
+
await this.port.setSignals({
|
|
146
|
+
dataTerminalReady: false,
|
|
147
|
+
requestToSend: true,
|
|
148
|
+
});
|
|
149
|
+
await this.port.setSignals({
|
|
150
|
+
dataTerminalReady: false,
|
|
151
|
+
requestToSend: false,
|
|
152
|
+
});
|
|
153
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
customElements.define("ewt-console", EwtConsole);
|
|
158
|
+
|
|
159
|
+
declare global {
|
|
160
|
+
interface HTMLElementTagNameMap {
|
|
161
|
+
"ewt-console": EwtConsole;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DialogBase } from "@material/mwc-dialog/mwc-dialog-base";
|
|
2
|
+
import { styles } from "@material/mwc-dialog/mwc-dialog.css";
|
|
3
|
+
import { css } from "lit";
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface HTMLElementTagNameMap {
|
|
7
|
+
"ewt-dialog": EwtDialog;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class EwtDialog extends DialogBase {
|
|
12
|
+
static override styles = [
|
|
13
|
+
styles,
|
|
14
|
+
css`
|
|
15
|
+
.mdc-dialog__title {
|
|
16
|
+
padding-right: 52px;
|
|
17
|
+
}
|
|
18
|
+
`,
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
customElements.define("ewt-dialog", EwtDialog);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FormfieldBase } from "@material/mwc-formfield/mwc-formfield-base";
|
|
2
|
+
import { styles } from "@material/mwc-formfield/mwc-formfield.css";
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
interface HTMLElementTagNameMap {
|
|
6
|
+
"ewt-formfield": EwtFormfield;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class EwtFormfield extends FormfieldBase {
|
|
11
|
+
static override styles = [styles];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
customElements.define("ewt-formfield", EwtFormfield);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { IconButtonBase } from "@material/mwc-icon-button/mwc-icon-button-base";
|
|
2
|
+
import { styles } from "@material/mwc-icon-button/mwc-icon-button.css";
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
interface HTMLElementTagNameMap {
|
|
6
|
+
"ewt-icon-button": EwtIconButton;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class EwtIconButton extends IconButtonBase {
|
|
11
|
+
static override styles = [styles];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
customElements.define("ewt-icon-button", EwtIconButton);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ListItemBase } from "@material/mwc-list/mwc-list-item-base";
|
|
2
|
+
import { styles } from "@material/mwc-list/mwc-list-item.css";
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
interface HTMLElementTagNameMap {
|
|
6
|
+
"ewt-list-item": EwtListItem;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class EwtListItem extends ListItemBase {
|
|
11
|
+
static override styles = [styles];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
customElements.define("ewt-list-item", EwtListItem);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
|
2
|
+
import { styles } from "@material/mwc-select/mwc-select.css";
|
|
3
|
+
import { css } from "lit";
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface HTMLElementTagNameMap {
|
|
7
|
+
"ewt-select": EwtSelect;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class EwtSelect extends SelectBase {
|
|
12
|
+
static override styles = [
|
|
13
|
+
styles,
|
|
14
|
+
// rem -> em conversion
|
|
15
|
+
css`
|
|
16
|
+
.mdc-floating-label {
|
|
17
|
+
line-height: 1.15em;
|
|
18
|
+
}
|
|
19
|
+
`,
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
customElements.define("ewt-select", EwtSelect);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { TextFieldBase } from "@material/mwc-textfield/mwc-textfield-base";
|
|
2
|
+
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
|
|
3
|
+
import { css } from "lit";
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface HTMLElementTagNameMap {
|
|
7
|
+
"ewt-textfield": EwtTextfield;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class EwtTextfield extends TextFieldBase {
|
|
12
|
+
static override styles = [
|
|
13
|
+
styles,
|
|
14
|
+
// rem -> em conversion
|
|
15
|
+
css`
|
|
16
|
+
.mdc-floating-label {
|
|
17
|
+
line-height: 1.15em;
|
|
18
|
+
}
|
|
19
|
+
`,
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
customElements.define("ewt-textfield", EwtTextfield);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { svg } from "lit";
|
|
2
|
+
|
|
3
|
+
export const closeIcon = svg`
|
|
4
|
+
<svg width="24" height="24" viewBox="0 0 24 24">
|
|
5
|
+
<path
|
|
6
|
+
d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
|
|
7
|
+
/>
|
|
8
|
+
</svg>
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
export const firmwareIcon = svg`
|
|
12
|
+
<svg viewBox="0 0 24 24" title="Software">
|
|
13
|
+
<path
|
|
14
|
+
fill="currentColor"
|
|
15
|
+
d="M9.5,8.5L11,10L8,13L11,16L9.5,17.5L5,13L9.5,8.5M14.5,17.5L13,16L16,13L13,10L14.5,8.5L19,13L14.5,17.5M21,2H3A2,2 0 0,0 1,4V20A2,2 0 0,0 3,22H21A2,2 0 0,0 23,20V4A2,2 0 0,0 21,2M21,20H3V6H21V20Z"
|
|
16
|
+
/>
|
|
17
|
+
</svg>
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
export const chipIcon = svg`
|
|
21
|
+
<svg viewBox="0 0 24 24" title="Chipset">
|
|
22
|
+
<path
|
|
23
|
+
fill="currentColor"
|
|
24
|
+
d="M6,4H18V5H21V7H18V9H21V11H18V13H21V15H18V17H21V19H18V20H6V19H3V17H6V15H3V13H6V11H3V9H6V7H3V5H6V4M11,15V18H12V15H11M13,15V18H14V15H13M15,15V18H16V15H15Z"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
`;
|
package/src/connect.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { InstallButton } from "./install-button.js";
|
|
2
|
+
import "./install-dialog.js";
|
|
3
|
+
|
|
4
|
+
export const connect = async (button: InstallButton) => {
|
|
5
|
+
let port: SerialPort | undefined;
|
|
6
|
+
try {
|
|
7
|
+
port = await navigator.serial.requestPort();
|
|
8
|
+
} catch (err: any) {
|
|
9
|
+
if ((err as DOMException).name === "NotFoundError") {
|
|
10
|
+
import("./no-port-picked/index").then((mod) =>
|
|
11
|
+
mod.openNoPortPickedDialog(() => connect(button))
|
|
12
|
+
);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
alert(`Error: ${err.message}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!port) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await port.open({ baudRate: 115200 });
|
|
25
|
+
} catch (err: any) {
|
|
26
|
+
alert(err.message);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const el = document.createElement("ewt-install-dialog");
|
|
31
|
+
el.port = port;
|
|
32
|
+
el.manifestPath = button.manifest || button.getAttribute("manifest")!;
|
|
33
|
+
el.overrides = button.overrides;
|
|
34
|
+
el.addEventListener(
|
|
35
|
+
"closed",
|
|
36
|
+
() => {
|
|
37
|
+
port!.close();
|
|
38
|
+
},
|
|
39
|
+
{ once: true }
|
|
40
|
+
);
|
|
41
|
+
document.body.appendChild(el);
|
|
42
|
+
};
|
package/src/const.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export interface Logger {
|
|
2
|
+
log(msg: string, ...args: any[]): void;
|
|
3
|
+
error(msg: string, ...args: any[]): void;
|
|
4
|
+
debug(msg: string, ...args: any[]): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface Build {
|
|
8
|
+
chipFamily: "ESP32" | "ESP8266" | "ESP32-S2" | "ESP32-S3" | "ESP32-C3";
|
|
9
|
+
parts: {
|
|
10
|
+
path: string;
|
|
11
|
+
offset: number;
|
|
12
|
+
}[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Manifest {
|
|
16
|
+
name: string;
|
|
17
|
+
version: string;
|
|
18
|
+
home_assistant_domain?: string;
|
|
19
|
+
funding_url?: string;
|
|
20
|
+
/** @deprecated use `new_install_prompt_erase` instead */
|
|
21
|
+
new_install_skip_erase?: boolean;
|
|
22
|
+
new_install_prompt_erase?: boolean;
|
|
23
|
+
/* Time to wait to detect Improv Wi-Fi. Set to 0 to disable. */
|
|
24
|
+
new_install_improv_wait_time?: number;
|
|
25
|
+
builds: Build[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BaseFlashState {
|
|
29
|
+
state: FlashStateType;
|
|
30
|
+
message: string;
|
|
31
|
+
manifest?: Manifest;
|
|
32
|
+
build?: Build;
|
|
33
|
+
chipFamily?: Build["chipFamily"] | "Unknown Chip";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface InitializingState extends BaseFlashState {
|
|
37
|
+
state: FlashStateType.INITIALIZING;
|
|
38
|
+
details: { done: boolean };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ManifestState extends BaseFlashState {
|
|
42
|
+
state: FlashStateType.MANIFEST;
|
|
43
|
+
details: { done: boolean };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PreparingState extends BaseFlashState {
|
|
47
|
+
state: FlashStateType.PREPARING;
|
|
48
|
+
details: { done: boolean };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ErasingState extends BaseFlashState {
|
|
52
|
+
state: FlashStateType.ERASING;
|
|
53
|
+
details: { done: boolean };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface WritingState extends BaseFlashState {
|
|
57
|
+
state: FlashStateType.WRITING;
|
|
58
|
+
details: { bytesTotal: number; bytesWritten: number; percentage: number };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface FinishedState extends BaseFlashState {
|
|
62
|
+
state: FlashStateType.FINISHED;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ErrorState extends BaseFlashState {
|
|
66
|
+
state: FlashStateType.ERROR;
|
|
67
|
+
details: { error: FlashError; details: string | Error };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type FlashState =
|
|
71
|
+
| InitializingState
|
|
72
|
+
| ManifestState
|
|
73
|
+
| PreparingState
|
|
74
|
+
| ErasingState
|
|
75
|
+
| WritingState
|
|
76
|
+
| FinishedState
|
|
77
|
+
| ErrorState;
|
|
78
|
+
|
|
79
|
+
export const enum FlashStateType {
|
|
80
|
+
INITIALIZING = "initializing",
|
|
81
|
+
MANIFEST = "manifest",
|
|
82
|
+
PREPARING = "preparing",
|
|
83
|
+
ERASING = "erasing",
|
|
84
|
+
WRITING = "writing",
|
|
85
|
+
FINISHED = "finished",
|
|
86
|
+
ERROR = "error",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const enum FlashError {
|
|
90
|
+
FAILED_INITIALIZING = "failed_initialize",
|
|
91
|
+
FAILED_MANIFEST_FETCH = "fetch_manifest_failed",
|
|
92
|
+
NOT_SUPPORTED = "not_supported",
|
|
93
|
+
FAILED_FIRMWARE_DOWNLOAD = "failed_firmware_download",
|
|
94
|
+
WRITE_FAILED = "write_failed",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
declare global {
|
|
98
|
+
interface HTMLElementEventMap {
|
|
99
|
+
"state-changed": CustomEvent<FlashState>;
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/flash.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { ESPLoader, Logger } from "tasmota-webserial-esptool";
|
|
2
|
+
import {
|
|
3
|
+
Build,
|
|
4
|
+
FlashError,
|
|
5
|
+
FlashState,
|
|
6
|
+
Manifest,
|
|
7
|
+
FlashStateType,
|
|
8
|
+
} from "./const";
|
|
9
|
+
import { getChipFamilyName } from "./util/chip-family-name";
|
|
10
|
+
import { sleep } from "./util/sleep";
|
|
11
|
+
|
|
12
|
+
export const flash = async (
|
|
13
|
+
onEvent: (state: FlashState) => void,
|
|
14
|
+
port: SerialPort,
|
|
15
|
+
logger: Logger,
|
|
16
|
+
manifestPath: string,
|
|
17
|
+
eraseFirst: boolean
|
|
18
|
+
) => {
|
|
19
|
+
let manifest: Manifest;
|
|
20
|
+
let build: Build | undefined;
|
|
21
|
+
let chipFamily: ReturnType<typeof getChipFamilyName>;
|
|
22
|
+
|
|
23
|
+
const fireStateEvent = (stateUpdate: FlashState) =>
|
|
24
|
+
onEvent({
|
|
25
|
+
...stateUpdate,
|
|
26
|
+
manifest,
|
|
27
|
+
build,
|
|
28
|
+
chipFamily,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const manifestURL = new URL(manifestPath, location.toString()).toString();
|
|
32
|
+
const manifestProm = fetch(manifestURL).then(
|
|
33
|
+
(resp): Promise<Manifest> => resp.json()
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const esploader = new ESPLoader(port, logger);
|
|
37
|
+
|
|
38
|
+
// For debugging
|
|
39
|
+
(window as any).esploader = esploader;
|
|
40
|
+
|
|
41
|
+
fireStateEvent({
|
|
42
|
+
state: FlashStateType.INITIALIZING,
|
|
43
|
+
message: "Initializing...",
|
|
44
|
+
details: { done: false },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await esploader.initialize();
|
|
49
|
+
} catch (err: any) {
|
|
50
|
+
logger.error(err);
|
|
51
|
+
fireStateEvent({
|
|
52
|
+
state: FlashStateType.ERROR,
|
|
53
|
+
message:
|
|
54
|
+
"Failed to initialize. Try resetting your device or holding the BOOT button while clicking INSTALL.",
|
|
55
|
+
details: { error: FlashError.FAILED_INITIALIZING, details: err },
|
|
56
|
+
});
|
|
57
|
+
if (esploader.connected) {
|
|
58
|
+
await esploader.disconnect();
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
chipFamily = getChipFamilyName(esploader);
|
|
64
|
+
|
|
65
|
+
fireStateEvent({
|
|
66
|
+
state: FlashStateType.INITIALIZING,
|
|
67
|
+
message: `Initialized. Found ${chipFamily}`,
|
|
68
|
+
details: { done: true },
|
|
69
|
+
});
|
|
70
|
+
fireStateEvent({
|
|
71
|
+
state: FlashStateType.MANIFEST,
|
|
72
|
+
message: "Fetching manifest...",
|
|
73
|
+
details: { done: false },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
manifest = await manifestProm;
|
|
78
|
+
} catch (err: any) {
|
|
79
|
+
fireStateEvent({
|
|
80
|
+
state: FlashStateType.ERROR,
|
|
81
|
+
message: `Unable to fetch manifest: ${err}`,
|
|
82
|
+
details: { error: FlashError.FAILED_MANIFEST_FETCH, details: err },
|
|
83
|
+
});
|
|
84
|
+
await esploader.disconnect();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
build = manifest.builds.find((b) => b.chipFamily === chipFamily);
|
|
89
|
+
|
|
90
|
+
fireStateEvent({
|
|
91
|
+
state: FlashStateType.MANIFEST,
|
|
92
|
+
message: `Found manifest for ${manifest.name}`,
|
|
93
|
+
details: { done: true },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!build) {
|
|
97
|
+
fireStateEvent({
|
|
98
|
+
state: FlashStateType.ERROR,
|
|
99
|
+
message: `Your ${chipFamily} board is not supported.`,
|
|
100
|
+
details: { error: FlashError.NOT_SUPPORTED, details: chipFamily },
|
|
101
|
+
});
|
|
102
|
+
await esploader.disconnect();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fireStateEvent({
|
|
107
|
+
state: FlashStateType.PREPARING,
|
|
108
|
+
message: "Preparing installation...",
|
|
109
|
+
details: { done: false },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const filePromises = build.parts.map(async (part) => {
|
|
113
|
+
const url = new URL(part.path, manifestURL).toString();
|
|
114
|
+
const resp = await fetch(url);
|
|
115
|
+
if (!resp.ok) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Downlading firmware ${part.path} failed: ${resp.status}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return resp.arrayBuffer();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Run the stub while we wait for files to download
|
|
124
|
+
const espStub = await esploader.runStub();
|
|
125
|
+
|
|
126
|
+
const files: ArrayBuffer[] = [];
|
|
127
|
+
let totalSize = 0;
|
|
128
|
+
|
|
129
|
+
for (const prom of filePromises) {
|
|
130
|
+
try {
|
|
131
|
+
const data = await prom;
|
|
132
|
+
files.push(data);
|
|
133
|
+
totalSize += data.byteLength;
|
|
134
|
+
} catch (err: any) {
|
|
135
|
+
fireStateEvent({
|
|
136
|
+
state: FlashStateType.ERROR,
|
|
137
|
+
message: err.message,
|
|
138
|
+
details: {
|
|
139
|
+
error: FlashError.FAILED_FIRMWARE_DOWNLOAD,
|
|
140
|
+
details: err.message,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
await esploader.disconnect();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fireStateEvent({
|
|
149
|
+
state: FlashStateType.PREPARING,
|
|
150
|
+
message: "Installation prepared",
|
|
151
|
+
details: { done: true },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (eraseFirst) {
|
|
155
|
+
fireStateEvent({
|
|
156
|
+
state: FlashStateType.ERASING,
|
|
157
|
+
message: "Erasing device...",
|
|
158
|
+
details: { done: false },
|
|
159
|
+
});
|
|
160
|
+
await espStub.eraseFlash();
|
|
161
|
+
fireStateEvent({
|
|
162
|
+
state: FlashStateType.ERASING,
|
|
163
|
+
message: "Device erased",
|
|
164
|
+
details: { done: true },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let lastPct = 0;
|
|
169
|
+
|
|
170
|
+
fireStateEvent({
|
|
171
|
+
state: FlashStateType.WRITING,
|
|
172
|
+
message: `Writing progress: ${lastPct}%`,
|
|
173
|
+
details: {
|
|
174
|
+
bytesTotal: totalSize,
|
|
175
|
+
bytesWritten: 0,
|
|
176
|
+
percentage: lastPct,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
let totalWritten = 0;
|
|
181
|
+
|
|
182
|
+
for (const part of build.parts) {
|
|
183
|
+
const file = files.shift()!;
|
|
184
|
+
try {
|
|
185
|
+
await espStub.flashData(
|
|
186
|
+
file,
|
|
187
|
+
(bytesWritten: number) => {
|
|
188
|
+
const newPct = Math.floor(
|
|
189
|
+
((totalWritten + bytesWritten) / totalSize) * 100
|
|
190
|
+
);
|
|
191
|
+
if (newPct === lastPct) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
lastPct = newPct;
|
|
195
|
+
fireStateEvent({
|
|
196
|
+
state: FlashStateType.WRITING,
|
|
197
|
+
message: `Writing progress: ${newPct}%`,
|
|
198
|
+
details: {
|
|
199
|
+
bytesTotal: totalSize,
|
|
200
|
+
bytesWritten: totalWritten + bytesWritten,
|
|
201
|
+
percentage: newPct,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
part.offset,
|
|
206
|
+
true
|
|
207
|
+
);
|
|
208
|
+
} catch (err: any) {
|
|
209
|
+
fireStateEvent({
|
|
210
|
+
state: FlashStateType.ERROR,
|
|
211
|
+
message: err.message,
|
|
212
|
+
details: { error: FlashError.WRITE_FAILED, details: err },
|
|
213
|
+
});
|
|
214
|
+
await esploader.disconnect();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
totalWritten += file.byteLength;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fireStateEvent({
|
|
221
|
+
state: FlashStateType.WRITING,
|
|
222
|
+
message: "Writing complete",
|
|
223
|
+
details: {
|
|
224
|
+
bytesTotal: totalSize,
|
|
225
|
+
bytesWritten: totalWritten,
|
|
226
|
+
percentage: 100,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await sleep(100);
|
|
231
|
+
console.log("DISCONNECT");
|
|
232
|
+
await esploader.disconnect();
|
|
233
|
+
console.log("HARD RESET");
|
|
234
|
+
await esploader.hardReset();
|
|
235
|
+
|
|
236
|
+
fireStateEvent({
|
|
237
|
+
state: FlashStateType.FINISHED,
|
|
238
|
+
message: "All done!",
|
|
239
|
+
});
|
|
240
|
+
};
|