mirror of
https://github.com/esphome/esp-web-tools.git
synced 2025-07-28 14:16:41 +00:00
Fire state changed events and add progess bar (#10)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
4ad010ebd7
commit
4cc28e148b
45
README.md
45
README.md
@ -47,12 +47,57 @@ All attributes can also be set via properties (`manifest`, `eraseFirst`)
|
|||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
|
### Attributes
|
||||||
|
|
||||||
The following attributes are automatically added to `<esp-web-install-button>`:
|
The following attributes are automatically added to `<esp-web-install-button>`:
|
||||||
|
|
||||||
| Attribute | Description |
|
| Attribute | Description |
|
||||||
| -- | -- |
|
| -- | -- |
|
||||||
| `install-supported` | Added if installing firmware is supported
|
| `install-supported` | Added if installing firmware is supported
|
||||||
| `install-unsupported` | Added if installing firmware is not supported
|
| `install-unsupported` | Added if installing firmware is not supported
|
||||||
|
| `active` | Added when flashing is active
|
||||||
|
|
||||||
|
You can add the following attributes or properties to change the UI elements:
|
||||||
|
|
||||||
|
| Attribute | Property | Description |
|
||||||
|
| -- | -- | -- |
|
||||||
|
| `show-log` | `showLog` | Show a log style view of the progress instead of a progress bar
|
||||||
|
| `hide-progress` | `hideProgress` | Hides all progress UI elements
|
||||||
|
|
||||||
|
### CSS custom properties (variables)
|
||||||
|
|
||||||
|
The following variables can be used to change the colors of the default UI elements:
|
||||||
|
|
||||||
|
- `--esp-tools-button-color`
|
||||||
|
- `--esp-tools-button-text-color`
|
||||||
|
- `--esp-tools-success-color`
|
||||||
|
- `--esp-tools-error-color`
|
||||||
|
- `--esp-tools-progress-color`
|
||||||
|
- `--esp-tools-log-background`
|
||||||
|
- `--esp-tools-log-text-color`
|
||||||
|
|
||||||
|
### Slots
|
||||||
|
|
||||||
|
The following slots are available:
|
||||||
|
|
||||||
|
| Slot name | Description |
|
||||||
|
| -- | -- |
|
||||||
|
| `activate` | Button to start the flash progress
|
||||||
|
| `unsupported` | Message to show when the browser is not supported
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
When the state of flashing changes, a `state-changed` event is fired.
|
||||||
|
|
||||||
|
A `state-changed` event contains the following information:
|
||||||
|
|
||||||
|
Field | Description
|
||||||
|
-- | --
|
||||||
|
state | The current [state](https://github.com/esphome/esp-web-tools/blob/main/src/const.ts)
|
||||||
|
message | A description of the current state
|
||||||
|
manifest | The loaded manifest
|
||||||
|
chipFamily | The chip that was detected; "ESP32" \| "ESP8266" \| "ESP32-S2" \| "Unknown Chip"
|
||||||
|
details | An optional extra field that is different [per state](https://github.com/esphome/esp-web-tools/blob/main/src/const.ts)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
146
index.html
146
index.html
@ -27,9 +27,6 @@
|
|||||||
.project .logo img {
|
.project .logo img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
esp-web-flash-log {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
a {
|
a {
|
||||||
color: #03a9f4;
|
color: #03a9f4;
|
||||||
}
|
}
|
||||||
@ -61,6 +58,13 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
table {
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script module>
|
<script module>
|
||||||
import(
|
import(
|
||||||
@ -236,13 +240,58 @@
|
|||||||
|
|
||||||
<h3>Customizing the look and feel</h3>
|
<h3>Customizing the look and feel</h3>
|
||||||
<p>
|
<p>
|
||||||
You can customize both the activation button and the message that is
|
There are multiple options to change the look and feel of the button and
|
||||||
shown when the user uses an unsupported browser. This can be done using
|
other elements.
|
||||||
the <code>activate</code> and <code>unsupported</code> slots:
|
</p>
|
||||||
|
<h4>Change colors</h4>
|
||||||
|
<p>
|
||||||
|
You can change the colors of the default UI elements with CSS custom
|
||||||
|
properties (variables), the following variables are available:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>--esp-tools-button-color</code></li>
|
||||||
|
<li><code>--esp-tools-button-text-color</code></li>
|
||||||
|
|
||||||
|
<li><code>--esp-tools-success-color</code></li>
|
||||||
|
<li><code>--esp-tools-error-color</code></li>
|
||||||
|
|
||||||
|
<li><code>--esp-tools-progress-color</code></li>
|
||||||
|
|
||||||
|
<li><code>--esp-tools-log-background</code></li>
|
||||||
|
<li><code>--esp-tools-log-text-color</code></li>
|
||||||
|
</ul>
|
||||||
|
<p>There are also some attributes that can be used for styling:</p>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><code>install-supported</code></td>
|
||||||
|
<td>Added if installing firmware is supported</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>install-unsupported</code>
|
||||||
|
</td>
|
||||||
|
<td>Added if installing firmware is not supported</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>active</code></td>
|
||||||
|
<td>Added when flashing is active</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p>
|
||||||
|
When you are using a custom button, you should disable it when the
|
||||||
|
<code>active</code> attribute is present.
|
||||||
|
</p>
|
||||||
|
<h4>Replace the button and message with a custom one</h4>
|
||||||
|
<p>
|
||||||
|
You can replace both the activation button and the message that is shown
|
||||||
|
when the user uses an unsupported browser with your own elements. This
|
||||||
|
can be done using the <code>activate</code> and
|
||||||
|
<code>unsupported</code> slots:
|
||||||
</p>
|
</p>
|
||||||
<pre>
|
<pre>
|
||||||
<esp-web-install-button
|
<esp-web-install-button
|
||||||
manifest="static/firmware_build/manifest.json"
|
manifest="static/firmware_build/manifest.json"
|
||||||
|
show-log
|
||||||
erase-first
|
erase-first
|
||||||
>
|
>
|
||||||
<button slot="activate">Custom install button</button>
|
<button slot="activate">Custom install button</button>
|
||||||
@ -250,6 +299,91 @@
|
|||||||
</esp-web-install-button>
|
</esp-web-install-button>
|
||||||
</pre
|
</pre
|
||||||
>
|
>
|
||||||
|
<h4>Show or hide the progress bar and log</h4>
|
||||||
|
<p>
|
||||||
|
By default there is a progress bar showing the state and progress of the
|
||||||
|
flashing progress, you can chnage this progress bar to a log view with
|
||||||
|
the <code>show-log</code> attribute.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can also hide all progress indicators by adding the `hide-progress`
|
||||||
|
attribute. This will hide both the progress bar and the log view. You
|
||||||
|
can then implement your own progress elements using the
|
||||||
|
<a href="#state-events">state events</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 id="state-events">State events</h3>
|
||||||
|
<p>
|
||||||
|
During the flash progress the button will fire
|
||||||
|
<code>state-changed</code> events for every step of the progress and to
|
||||||
|
signal progress in the writing.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
With these events you can create your own progress UI or trigger certain
|
||||||
|
actions. You can also find the current state as the
|
||||||
|
<code>state</code> property of the
|
||||||
|
<code>esp-web-install-button</code> element.
|
||||||
|
</p>
|
||||||
|
<p>Events for the following states are fired:</p>
|
||||||
|
<ul>
|
||||||
|
<li>initializing</li>
|
||||||
|
<li>manifest</li>
|
||||||
|
<li>preparing</li>
|
||||||
|
<li>erasing</li>
|
||||||
|
<li>writing</li>
|
||||||
|
<li>finished</li>
|
||||||
|
<li>error</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
A <code>state-changed</code> event contains the following information:
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><code>state</code></td>
|
||||||
|
<td>The current state; one of the above</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>message</code></td>
|
||||||
|
<td>A description of the current state</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>manifest</code></td>
|
||||||
|
<td>The loaded manifest</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>build</code></td>
|
||||||
|
<td>The manifest's build that was selected</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>chipFamily</code></td>
|
||||||
|
<td>
|
||||||
|
The chip that was detected;
|
||||||
|
<code>"ESP32" | "ESP8266" | "ESP32-S2" | "Unknown Chip"</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>details</code></td>
|
||||||
|
<td>
|
||||||
|
An optional extra field that is different
|
||||||
|
<a
|
||||||
|
href="https://github.com/esphome/esp-web-tools/blob/main/src/const.ts"
|
||||||
|
>per state</a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p>An example that logs all state events:</p>
|
||||||
|
<pre>
|
||||||
|
<esp-web-install-button
|
||||||
|
manifest="static/firmware_build/manifest.json"
|
||||||
|
></esp-web-install-button>
|
||||||
|
<script>
|
||||||
|
const espWebInstallButton = document.querySelector("esp-web-install-button");
|
||||||
|
espWebInstallButton.addEventListener(
|
||||||
|
"state-changed", (ev) => { console.log(ev.detail) }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
</pre>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div>
|
<div>
|
||||||
ESP Web Tools –
|
ESP Web Tools –
|
||||||
|
1301
package-lock.json
generated
1301
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,7 +14,6 @@
|
|||||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
"@rollup/plugin-node-resolve": "^13.0.0",
|
||||||
"@rollup/plugin-typescript": "^8.2.1",
|
"@rollup/plugin-typescript": "^8.2.1",
|
||||||
"@types/w3c-web-serial": "^1.0.1",
|
"@types/w3c-web-serial": "^1.0.1",
|
||||||
"@types/web-bluetooth": "^0.0.9",
|
|
||||||
"prettier": "^2.3.0",
|
"prettier": "^2.3.0",
|
||||||
"rollup": "^2.50.2",
|
"rollup": "^2.50.2",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
@ -22,10 +21,8 @@
|
|||||||
"typescript": "^4.3.2"
|
"typescript": "^4.3.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material/mwc-button": "^0.21.0",
|
"@material/mwc-base": "^0.21.0",
|
||||||
"@material/mwc-circular-progress": "^0.21.0",
|
"@material/mwc-linear-progress": "^0.21.0",
|
||||||
"@material/mwc-dialog": "^0.21.0",
|
|
||||||
"@material/mwc-textfield": "^0.21.0",
|
|
||||||
"esp-web-flasher": "^1.0.4",
|
"esp-web-flasher": "^1.0.4",
|
||||||
"lit": "^2.0.0-rc.2",
|
"lit": "^2.0.0-rc.2",
|
||||||
"tslib": "^2.2.0"
|
"tslib": "^2.2.0"
|
||||||
|
75
src/const.ts
75
src/const.ts
@ -11,3 +11,78 @@ export interface Manifest {
|
|||||||
name: string;
|
name: string;
|
||||||
builds: Build[];
|
builds: Build[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BaseFlashState {
|
||||||
|
state: State;
|
||||||
|
message: string;
|
||||||
|
manifest?: Manifest;
|
||||||
|
build?: Build;
|
||||||
|
chipFamily?: "ESP32" | "ESP8266" | "ESP32-S2" | "Unknown Chip";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitializingState extends BaseFlashState {
|
||||||
|
state: State.INITIALIZING;
|
||||||
|
details: { done: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ManifestState extends BaseFlashState {
|
||||||
|
state: State.MANIFEST;
|
||||||
|
details: { done: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PreparingState extends BaseFlashState {
|
||||||
|
state: State.PREPARING;
|
||||||
|
details: { done: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErasingState extends BaseFlashState {
|
||||||
|
state: State.ERASING;
|
||||||
|
details: { done: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WritingState extends BaseFlashState {
|
||||||
|
state: State.WRITING;
|
||||||
|
details: { bytesTotal: number; bytesWritten: number; percentage: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FinishedState extends BaseFlashState {
|
||||||
|
state: State.FINISHED;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorState extends BaseFlashState {
|
||||||
|
state: State.ERROR;
|
||||||
|
details: { error: FlashError; details: string | Error };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FlashState =
|
||||||
|
| InitializingState
|
||||||
|
| ManifestState
|
||||||
|
| PreparingState
|
||||||
|
| ErasingState
|
||||||
|
| WritingState
|
||||||
|
| FinishedState
|
||||||
|
| ErrorState;
|
||||||
|
|
||||||
|
export const enum State {
|
||||||
|
INITIALIZING = "initializing",
|
||||||
|
MANIFEST = "manifest",
|
||||||
|
PREPARING = "preparing",
|
||||||
|
ERASING = "erasing",
|
||||||
|
WRITING = "writing",
|
||||||
|
FINISHED = "finished",
|
||||||
|
ERROR = "error",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum FlashError {
|
||||||
|
FAILED_INITIALIZING = "failed_initialize",
|
||||||
|
FAILED_MANIFEST_FETCH = "fetch_manifest_failed",
|
||||||
|
NOT_SUPPORTED = "not_supported",
|
||||||
|
FAILED_FIRMWARE_DOWNLOAD = "failed_firmware_download",
|
||||||
|
WRITE_FAILED = "write_failed",
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementEventMap {
|
||||||
|
"state-changed": CustomEvent<FlashState>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import { css, html, HTMLTemplateResult, LitElement } from "lit";
|
import { css, html, HTMLTemplateResult, LitElement } from "lit";
|
||||||
import { customElement, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
import { classMap } from "lit/directives/class-map.js";
|
import { classMap } from "lit/directives/class-map.js";
|
||||||
|
import { FlashState, State } from "./const";
|
||||||
|
|
||||||
interface Row {
|
interface Row {
|
||||||
id?: string;
|
state?: State;
|
||||||
content: HTMLTemplateResult | string;
|
message: HTMLTemplateResult | string;
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
action?: boolean;
|
action?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("esp-web-flash-log")
|
@customElement("esp-web-flash-log")
|
||||||
class FlashLog extends LitElement {
|
export class FlashLog extends LitElement {
|
||||||
@state() _rows: Row[] = [];
|
@state() private _rows: Row[] = [];
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`${this._rows.map(
|
return html`${this._rows.map(
|
||||||
@ -22,20 +23,41 @@ class FlashLog extends LitElement {
|
|||||||
action: row.action === true,
|
action: row.action === true,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
${row.content}
|
${row.message}
|
||||||
</div>`
|
</div>`
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public willUpdate() {
|
||||||
|
this.toggleAttribute("hidden", !this._rows.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear() {
|
||||||
|
this._rows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public processState(state: FlashState) {
|
||||||
|
if (state.state === State.ERROR) {
|
||||||
|
this.addError(state.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.addRow(state);
|
||||||
|
if (state.state === State.FINISHED) {
|
||||||
|
this.addAction(
|
||||||
|
html`<button @click=${this.clear}>Close this log</button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add or replace a row.
|
* Add or replace a row.
|
||||||
*/
|
*/
|
||||||
public addRow(row: Row) {
|
public addRow(row: Row) {
|
||||||
// If last entry has same ID, replace it.
|
// If last entry has same ID, replace it.
|
||||||
if (
|
if (
|
||||||
row.id &&
|
row.state &&
|
||||||
this._rows.length > 0 &&
|
this._rows.length > 0 &&
|
||||||
this._rows[this._rows.length - 1].id === row.id
|
this._rows[this._rows.length - 1].state === row.state
|
||||||
) {
|
) {
|
||||||
const newRows = this._rows.slice(0, -1);
|
const newRows = this._rows.slice(0, -1);
|
||||||
newRows.push(row);
|
newRows.push(row);
|
||||||
@ -48,15 +70,25 @@ class FlashLog extends LitElement {
|
|||||||
/**
|
/**
|
||||||
* Add an error row
|
* Add an error row
|
||||||
*/
|
*/
|
||||||
public addError(content: Row["content"]) {
|
public addError(message: Row["message"]) {
|
||||||
this.addRow({ content, error: true });
|
this.addRow({ message, error: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove last row if ID matches
|
* Add an action row
|
||||||
*/
|
*/
|
||||||
public removeRow(id: string) {
|
public addAction(message: Row["message"]) {
|
||||||
if (this._rows.length > 0 && this._rows[this._rows.length - 1].id === id) {
|
this.addRow({ message, action: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove last row if state matches
|
||||||
|
*/
|
||||||
|
public removeRow(state: string) {
|
||||||
|
if (
|
||||||
|
this._rows.length > 0 &&
|
||||||
|
this._rows[this._rows.length - 1].state === state
|
||||||
|
) {
|
||||||
this._rows = this._rows.slice(0, -1);
|
this._rows = this._rows.slice(0, -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,13 +96,17 @@ class FlashLog extends LitElement {
|
|||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 500px;
|
margin-top: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
background-color: black;
|
background: var(--esp-tools-log-background, black);
|
||||||
color: greenyellow;
|
color: var(--esp-tools-log-text-color, greenyellow);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 19px;
|
line-height: 19px;
|
||||||
padding: 12px 16px;
|
}
|
||||||
|
|
||||||
|
:host([hidden]) {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@ -84,13 +120,13 @@ class FlashLog extends LitElement {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action,
|
|
||||||
.error {
|
.error {
|
||||||
margin-top: 1em;
|
color: var(--esp-tools-error-color, #dc3545);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error,
|
||||||
color: red;
|
.action {
|
||||||
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
85
src/flash-progress.ts
Normal file
85
src/flash-progress.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators.js";
|
||||||
|
import { FlashState, State } from "./const";
|
||||||
|
import "@material/mwc-linear-progress";
|
||||||
|
import { classMap } from "lit/directives/class-map.js";
|
||||||
|
|
||||||
|
@customElement("esp-web-flash-progress")
|
||||||
|
export class FlashProgress extends LitElement {
|
||||||
|
@state() private _state?: FlashState;
|
||||||
|
|
||||||
|
@state() private _indeterminate = true;
|
||||||
|
|
||||||
|
@state() private _progress = 0;
|
||||||
|
|
||||||
|
public processState(state: FlashState) {
|
||||||
|
this._state = state;
|
||||||
|
if (this._state.state === State.WRITING) {
|
||||||
|
this._indeterminate = false;
|
||||||
|
this._progress = this._state.details.percentage / 100;
|
||||||
|
}
|
||||||
|
if (this._state.state === State.ERROR) {
|
||||||
|
this._indeterminate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear() {
|
||||||
|
this._state = undefined;
|
||||||
|
this._progress = 0;
|
||||||
|
this._indeterminate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return html`<h2
|
||||||
|
class=${classMap({
|
||||||
|
error: this._state.state === State.ERROR,
|
||||||
|
done: this._state.state === State.FINISHED,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${this._state.message}
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
${this._state.manifest
|
||||||
|
? html`${this._state.manifest.name}: ${this._state.chipFamily}`
|
||||||
|
: html` `}
|
||||||
|
</p>
|
||||||
|
<mwc-linear-progress
|
||||||
|
class=${classMap({
|
||||||
|
error: this._state.state === State.ERROR,
|
||||||
|
done: this._state.state === State.FINISHED,
|
||||||
|
})}
|
||||||
|
.indeterminate=${this._indeterminate}
|
||||||
|
.progress=${this._progress}
|
||||||
|
></mwc-linear-progress>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
--mdc-theme-primary: var(--esp-tools-progress-color, #03a9f4);
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: var(--esp-tools-error-color, #dc3545);
|
||||||
|
--mdc-theme-primary: var(--esp-tools-error-color, #dc3545);
|
||||||
|
}
|
||||||
|
.done {
|
||||||
|
color: var(--esp-tools-success-color, #28a745);
|
||||||
|
--mdc-theme-primary: var(--esp-tools-success-color, #28a745);
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin: 16px 0 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"esp-web-flash-progress": FlashProgress;
|
||||||
|
}
|
||||||
|
}
|
234
src/flash.ts
Normal file
234
src/flash.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import { connect, ESPLoader, Logger } from "esp-web-flasher";
|
||||||
|
import { Build, FlashError, FlashState, Manifest, State } from "./const";
|
||||||
|
import { fireEvent, getChipFamilyName, sleep } from "./util";
|
||||||
|
|
||||||
|
export const flash = async (
|
||||||
|
eventTarget: EventTarget,
|
||||||
|
logger: Logger,
|
||||||
|
manifestPath: string,
|
||||||
|
eraseFirst: boolean
|
||||||
|
) => {
|
||||||
|
let manifest: Manifest;
|
||||||
|
let build: Build | undefined;
|
||||||
|
let chipFamily: "ESP32" | "ESP8266" | "ESP32-S2" | "Unknown Chip";
|
||||||
|
|
||||||
|
const fireStateEvent = (stateUpdate: FlashState) => {
|
||||||
|
fireEvent(eventTarget, "state-changed", {
|
||||||
|
...stateUpdate,
|
||||||
|
manifest,
|
||||||
|
build,
|
||||||
|
chipFamily,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const manifestURL = new URL(manifestPath, location.toString()).toString();
|
||||||
|
const manifestProm = fetch(manifestURL).then(
|
||||||
|
(resp): Promise<Manifest> => resp.json()
|
||||||
|
);
|
||||||
|
|
||||||
|
let esploader: ESPLoader | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
esploader = await connect(logger);
|
||||||
|
} catch (err) {
|
||||||
|
// User pressed cancel on web serial
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For debugging
|
||||||
|
(window as any).esploader = esploader;
|
||||||
|
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.INITIALIZING,
|
||||||
|
message: "Initializing...",
|
||||||
|
details: { done: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await esploader.initialize();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
if (esploader.connected) {
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.ERROR,
|
||||||
|
message:
|
||||||
|
"Failed to initialize. Try resetting your device or holding the BOOT button before clicking connect.",
|
||||||
|
details: { error: FlashError.FAILED_INITIALIZING, details: err },
|
||||||
|
});
|
||||||
|
await esploader.disconnect();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chipFamily = getChipFamilyName(esploader);
|
||||||
|
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.INITIALIZING,
|
||||||
|
message: `Initialized. Found ${chipFamily}`,
|
||||||
|
details: { done: true },
|
||||||
|
});
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.MANIFEST,
|
||||||
|
message: "Fetching manifest...",
|
||||||
|
details: { done: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
manifest = await manifestProm;
|
||||||
|
} catch (err) {
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.ERROR,
|
||||||
|
message: `Unable to fetch manifest: ${err.message}`,
|
||||||
|
details: { error: FlashError.FAILED_MANIFEST_FETCH, details: err },
|
||||||
|
});
|
||||||
|
await esploader.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
build = manifest.builds.find((b) => b.chipFamily === chipFamily);
|
||||||
|
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.MANIFEST,
|
||||||
|
message: `Found manifest for ${manifest.name}`,
|
||||||
|
details: { done: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!build) {
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.ERROR,
|
||||||
|
message: `Your ${chipFamily} board is not supported.`,
|
||||||
|
details: { error: FlashError.NOT_SUPPORTED, details: chipFamily },
|
||||||
|
});
|
||||||
|
await esploader.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.PREPARING,
|
||||||
|
message: "Preparing installation...",
|
||||||
|
details: { done: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const filePromises = build.parts.map(async (part) => {
|
||||||
|
const url = new URL(part.path, manifestURL).toString();
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Downlading firmware ${part.path} failed: ${resp.status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return resp.arrayBuffer();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run the stub while we wait for files to download
|
||||||
|
const espStub = await esploader.runStub();
|
||||||
|
|
||||||
|
const files: ArrayBuffer[] = [];
|
||||||
|
let totalSize = 0;
|
||||||
|
|
||||||
|
for (const prom of filePromises) {
|
||||||
|
try {
|
||||||
|
const data = await prom;
|
||||||
|
files.push(data);
|
||||||
|
totalSize += data.byteLength;
|
||||||
|
} catch (err) {
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.ERROR,
|
||||||
|
message: err,
|
||||||
|
details: { error: FlashError.FAILED_FIRMWARE_DOWNLOAD, details: err },
|
||||||
|
});
|
||||||
|
await esploader.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.PREPARING,
|
||||||
|
message: "Installation prepared",
|
||||||
|
details: { done: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eraseFirst) {
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.ERASING,
|
||||||
|
message: "Erasing device...",
|
||||||
|
details: { done: false },
|
||||||
|
});
|
||||||
|
await espStub.eraseFlash();
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.ERASING,
|
||||||
|
message: "Device erased",
|
||||||
|
details: { done: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastPct = 0;
|
||||||
|
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.WRITING,
|
||||||
|
message: `Writing progress: ${lastPct}%`,
|
||||||
|
details: {
|
||||||
|
bytesTotal: totalSize,
|
||||||
|
bytesWritten: 0,
|
||||||
|
percentage: lastPct,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let totalWritten = 0;
|
||||||
|
|
||||||
|
for (const part of build.parts) {
|
||||||
|
const file = files.shift()!;
|
||||||
|
try {
|
||||||
|
await espStub.flashData(
|
||||||
|
file,
|
||||||
|
(bytesWritten) => {
|
||||||
|
const newPct = Math.floor(
|
||||||
|
((totalWritten + bytesWritten) / totalSize) * 100
|
||||||
|
);
|
||||||
|
if (newPct === lastPct) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastPct = newPct;
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.WRITING,
|
||||||
|
message: `Writing progress: ${newPct}%`,
|
||||||
|
details: {
|
||||||
|
bytesTotal: totalSize,
|
||||||
|
bytesWritten: totalWritten + bytesWritten,
|
||||||
|
percentage: newPct,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
part.offset
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.ERROR,
|
||||||
|
message: err,
|
||||||
|
details: { error: FlashError.WRITE_FAILED, details: err },
|
||||||
|
});
|
||||||
|
await esploader.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
totalWritten += file.byteLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.WRITING,
|
||||||
|
message: "Writing complete",
|
||||||
|
details: {
|
||||||
|
bytesTotal: totalSize,
|
||||||
|
bytesWritten: totalWritten,
|
||||||
|
percentage: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(100);
|
||||||
|
await esploader.softReset();
|
||||||
|
await esploader.disconnect();
|
||||||
|
|
||||||
|
fireStateEvent({
|
||||||
|
state: State.FINISHED,
|
||||||
|
message: "All done!",
|
||||||
|
});
|
||||||
|
};
|
@ -1,11 +1,73 @@
|
|||||||
class InstallButton extends HTMLElement {
|
import { FlashState } from "./const";
|
||||||
|
|
||||||
|
export class InstallButton extends HTMLElement {
|
||||||
public static isSupported = "serial" in navigator;
|
public static isSupported = "serial" in navigator;
|
||||||
|
|
||||||
|
private static style = `
|
||||||
|
button {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 28px;
|
||||||
|
color: var(--esp-tools-button-text-color, #fff);
|
||||||
|
background-color: var(--esp-tools-button-color, #03a9f4);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.12), 0 1px 5px 0 rgba(0,0,0,.2);
|
||||||
|
}
|
||||||
|
button::before {
|
||||||
|
content: " ";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
opacity: 0.2;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
box-shadow: 0 4px 8px 0 rgba(0,0,0,.14), 0 1px 7px 0 rgba(0,0,0,.12), 0 3px 1px -1px rgba(0,0,0,.2);
|
||||||
|
}
|
||||||
|
button:hover::before {
|
||||||
|
background-color: rgba(255,255,255,.8);
|
||||||
|
}
|
||||||
|
button:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
button:focus::before {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
button:active::before {
|
||||||
|
background-color: grey;
|
||||||
|
}
|
||||||
|
:host([active]) button {
|
||||||
|
color: rgba(0, 0, 0, 0.38);
|
||||||
|
background-color: rgba(0, 0, 0, 0.12);
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: unset;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
improv-wifi-launch-button {
|
||||||
|
display: block;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}`;
|
||||||
|
|
||||||
public manifest?: string;
|
public manifest?: string;
|
||||||
|
|
||||||
public eraseFirst?: boolean;
|
public eraseFirst?: boolean;
|
||||||
|
|
||||||
private renderRoot?: ShadowRoot;
|
public hideProgress?: boolean;
|
||||||
|
|
||||||
|
public showLog?: boolean;
|
||||||
|
|
||||||
|
public logConsole?: boolean;
|
||||||
|
|
||||||
|
public state?: FlashState;
|
||||||
|
|
||||||
|
public renderRoot?: ShadowRoot;
|
||||||
|
|
||||||
public static preload() {
|
public static preload() {
|
||||||
import("./start-flash");
|
import("./start-flash");
|
||||||
@ -19,34 +81,46 @@ class InstallButton extends HTMLElement {
|
|||||||
this.renderRoot = this.attachShadow({ mode: "open" });
|
this.renderRoot = this.attachShadow({ mode: "open" });
|
||||||
|
|
||||||
if (!InstallButton.isSupported) {
|
if (!InstallButton.isSupported) {
|
||||||
this.setAttribute("install-unsupported", "");
|
this.toggleAttribute("install-unsupported", true);
|
||||||
this.renderRoot.innerHTML =
|
const slot = document.createElement("slot");
|
||||||
"<slot name='unsupported'>Your browser does not support installing things on ESP devices. Use Google Chrome or Microsoft Edge.</slot>";
|
slot.name = "unsupported";
|
||||||
|
slot.innerText =
|
||||||
|
"Your browser does not support installing things on ESP devices. Use Google Chrome or Microsoft Edge.";
|
||||||
|
this.renderRoot.append(slot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setAttribute("install-supported", "");
|
this.toggleAttribute("install-supported", true);
|
||||||
|
|
||||||
this.addEventListener("mouseover", InstallButton.preload);
|
this.addEventListener("mouseover", InstallButton.preload);
|
||||||
this.addEventListener("click", async (ev) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
const manifest = this.manifest || this.getAttribute("manifest");
|
|
||||||
if (!manifest) {
|
|
||||||
alert("No manifest defined!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const slot = document.createElement("slot");
|
||||||
|
|
||||||
|
slot.addEventListener("click", async (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
const mod = await import("./start-flash");
|
const mod = await import("./start-flash");
|
||||||
await mod.startFlash(
|
mod.startFlash(this);
|
||||||
console,
|
|
||||||
manifest,
|
|
||||||
(logEl) => this.parentElement!.insertBefore(logEl, this.nextSibling),
|
|
||||||
this.eraseFirst !== undefined
|
|
||||||
? this.eraseFirst
|
|
||||||
: this.hasAttribute("erase-first")
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.renderRoot.innerHTML = `<slot name='activate'><button>Install</button></slot>`;
|
slot.name = "activate";
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.innerText = "INSTALL";
|
||||||
|
slot.append(button);
|
||||||
|
if (
|
||||||
|
"adoptedStyleSheets" in Document.prototype &&
|
||||||
|
"replaceSync" in CSSStyleSheet.prototype
|
||||||
|
) {
|
||||||
|
const sheet = new CSSStyleSheet();
|
||||||
|
// @ts-expect-error
|
||||||
|
sheet.replaceSync(InstallButton.style);
|
||||||
|
// @ts-expect-error
|
||||||
|
this.renderRoot.adoptedStyleSheets = [sheet];
|
||||||
|
} else {
|
||||||
|
const styleSheet = document.createElement("style");
|
||||||
|
styleSheet.innerText = InstallButton.style;
|
||||||
|
this.renderRoot.append(styleSheet);
|
||||||
|
}
|
||||||
|
this.renderRoot.append(slot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,213 +1,126 @@
|
|||||||
import { html } from "lit";
|
import { flash } from "./flash";
|
||||||
import { connect, ESPLoader, Logger } from "esp-web-flasher";
|
|
||||||
import { Build, Manifest } from "./const";
|
|
||||||
import "./flash-log";
|
import "./flash-log";
|
||||||
import { getChipFamilyName, sleep } from "./util";
|
import "./flash-progress";
|
||||||
|
import type { FlashLog } from "./flash-log";
|
||||||
|
import type { FlashProgress } from "./flash-progress";
|
||||||
|
import type { InstallButton } from "./install-button";
|
||||||
|
import { State } from "./const";
|
||||||
|
|
||||||
export const startFlash = async (
|
let stateListenerAdded = false;
|
||||||
logger: Logger,
|
|
||||||
manifestPath: string,
|
|
||||||
addLogElement: (el: HTMLElement) => void,
|
|
||||||
eraseFirst: boolean
|
|
||||||
) => {
|
|
||||||
const manifestURL = new URL(manifestPath, location.toString()).toString();
|
|
||||||
const manifestProm = fetch(manifestURL).then(
|
|
||||||
(resp): Promise<Manifest> => resp.json()
|
|
||||||
);
|
|
||||||
|
|
||||||
let esploader: ESPLoader | undefined;
|
let logEl: FlashLog | undefined;
|
||||||
|
|
||||||
try {
|
let progressEl: FlashProgress | undefined;
|
||||||
esploader = await connect(logger);
|
|
||||||
} catch (err) {
|
|
||||||
// User pressed cancel on web serial
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For debugging
|
let improvEl: HTMLElement | undefined;
|
||||||
(window as any).esploader = esploader;
|
|
||||||
|
|
||||||
const logEl = document.createElement("esp-web-flash-log");
|
const addElement = <T extends HTMLElement>(
|
||||||
// logEl.esploader = esploader;
|
button: InstallButton,
|
||||||
logEl.addRow({ id: "initializing", content: "Initializing..." });
|
element: T
|
||||||
addLogElement(logEl);
|
): T => {
|
||||||
|
button.renderRoot!.append(element);
|
||||||
try {
|
return element;
|
||||||
await esploader.initialize();
|
};
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
export const startFlash = async (button: InstallButton) => {
|
||||||
if (esploader.connected) {
|
if (button.hasAttribute("active")) {
|
||||||
logEl.addError(
|
return;
|
||||||
"Failed to initialize. Try resetting your device or holding the BOOT button before clicking connect."
|
}
|
||||||
);
|
|
||||||
await esploader.disconnect();
|
const manifest = button.manifest || button.getAttribute("manifest");
|
||||||
}
|
if (!manifest) {
|
||||||
return;
|
alert("No manifest defined!");
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
const chipFamily = getChipFamilyName(esploader);
|
|
||||||
|
let hasImprov = false;
|
||||||
logEl.addRow({
|
|
||||||
id: "initializing",
|
if (!stateListenerAdded) {
|
||||||
content: html`Initialized. Found ${chipFamily}`,
|
stateListenerAdded = true;
|
||||||
});
|
button.addEventListener("state-changed", (ev) => {
|
||||||
logEl.addRow({ id: "manifest", content: "Fetching manifest..." });
|
const state = (button.state = ev.detail);
|
||||||
|
if (state.state === State.INITIALIZING) {
|
||||||
let manifest: Manifest | undefined;
|
button.toggleAttribute("active", true);
|
||||||
try {
|
} else if (state.state === State.MANIFEST && state.build?.improv) {
|
||||||
manifest = await manifestProm;
|
hasImprov = true;
|
||||||
} catch (err) {
|
// @ts-ignore
|
||||||
logEl.addError(`Unable to fetch manifest: ${err}`);
|
// preload improv button
|
||||||
await esploader.disconnect();
|
import("https://www.improv-wifi.com/sdk-js/launch-button.js");
|
||||||
return;
|
} else if (state.state === State.FINISHED) {
|
||||||
}
|
button.toggleAttribute("active", false);
|
||||||
|
if (hasImprov) {
|
||||||
logEl.addRow({
|
startImprov(button);
|
||||||
id: "manifest",
|
}
|
||||||
content: html`Found manifest for ${manifest.name}`,
|
} else if (state.state === State.ERROR) {
|
||||||
});
|
button.toggleAttribute("active", false);
|
||||||
|
}
|
||||||
let build: Build | undefined;
|
progressEl?.processState(ev.detail);
|
||||||
for (const b of manifest.builds) {
|
logEl?.processState(ev.detail);
|
||||||
if (b.chipFamily === chipFamily) {
|
});
|
||||||
build = b;
|
}
|
||||||
break;
|
|
||||||
}
|
const logConsole = button.logConsole || button.hasAttribute("log-console");
|
||||||
}
|
const showLog = button.showLog || button.hasAttribute("show-log");
|
||||||
|
const showProgress =
|
||||||
if (!build) {
|
!showLog &&
|
||||||
logEl.addError(`Your ${chipFamily} board is not supported.`);
|
button.hideProgress !== true &&
|
||||||
await esploader.disconnect();
|
!button.hasAttribute("hide-progress");
|
||||||
return;
|
|
||||||
}
|
if (showLog && !logEl) {
|
||||||
|
logEl = addElement<FlashLog>(
|
||||||
logEl.addRow({
|
button,
|
||||||
id: "preparing",
|
document.createElement("esp-web-flash-log")
|
||||||
content: "Preparing installation...",
|
);
|
||||||
});
|
} else if (!showLog && logEl) {
|
||||||
|
logEl.remove();
|
||||||
const filePromises = build.parts.map(async (part) => {
|
logEl = undefined;
|
||||||
const url = new URL(part.path, manifestURL).toString();
|
}
|
||||||
const resp = await fetch(url);
|
|
||||||
if (!resp.ok) {
|
if (showProgress && !progressEl) {
|
||||||
throw new Error(
|
progressEl = addElement<FlashProgress>(
|
||||||
`Downlading firmware ${part.path} failed: ${resp.status}`
|
button,
|
||||||
);
|
document.createElement("esp-web-flash-progress")
|
||||||
}
|
);
|
||||||
return resp.arrayBuffer();
|
} else if (!showProgress && progressEl) {
|
||||||
});
|
progressEl.remove();
|
||||||
|
progressEl = undefined;
|
||||||
// Run the stub while we wait for files to download
|
}
|
||||||
const espStub = await esploader.runStub();
|
|
||||||
|
logEl?.clear();
|
||||||
const files: ArrayBuffer[] = [];
|
progressEl?.clear();
|
||||||
let totalSize = 0;
|
improvEl?.classList.toggle("hidden", true);
|
||||||
|
|
||||||
for (const prom of filePromises) {
|
flash(
|
||||||
try {
|
button,
|
||||||
const data = await prom;
|
logConsole
|
||||||
files.push(data);
|
? console
|
||||||
totalSize += data.byteLength;
|
: {
|
||||||
} catch (err) {
|
log: () => {},
|
||||||
logEl.addError(err.message);
|
error: () => {},
|
||||||
await esploader.disconnect();
|
debug: () => {},
|
||||||
return;
|
},
|
||||||
}
|
manifest,
|
||||||
}
|
button.eraseFirst !== undefined
|
||||||
|
? button.eraseFirst
|
||||||
logEl.addRow({
|
: button.hasAttribute("erase-first")
|
||||||
id: "preparing",
|
);
|
||||||
content: `Installation prepared`,
|
};
|
||||||
});
|
|
||||||
|
const startImprov = async (button: InstallButton) => {
|
||||||
// Pre-load improv for later
|
// @ts-ignore
|
||||||
if (build.improv) {
|
await import("https://www.improv-wifi.com/sdk-js/launch-button.js");
|
||||||
// @ts-ignore
|
|
||||||
import("https://www.improv-wifi.com/sdk-js/launch-button.js");
|
if (!customElements.get("improv-wifi-launch-button").isSupported) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
if (eraseFirst) {
|
|
||||||
logEl.addRow({
|
if (!improvEl) {
|
||||||
id: "erase",
|
improvEl = document.createElement("improv-wifi-launch-button");
|
||||||
content: html`Erasing device...`,
|
const improvButton = document.createElement("button");
|
||||||
});
|
improvButton.slot = "activate";
|
||||||
await espStub.eraseFlash();
|
improvButton.textContent = "CLICK HERE TO FINISH SETTING UP YOUR DEVICE";
|
||||||
logEl.addRow({
|
improvEl.appendChild(improvButton);
|
||||||
id: "erase",
|
addElement(button, improvEl);
|
||||||
content: html`Device erased`,
|
}
|
||||||
});
|
improvEl.classList.toggle("hidden", false);
|
||||||
}
|
|
||||||
|
|
||||||
let lastPct = 0;
|
|
||||||
|
|
||||||
logEl.addRow({
|
|
||||||
id: "write",
|
|
||||||
content: html`Writing progress: ${lastPct}%`,
|
|
||||||
});
|
|
||||||
|
|
||||||
let totalWritten = 0;
|
|
||||||
|
|
||||||
for (const part of build.parts) {
|
|
||||||
const file = files.shift()!;
|
|
||||||
await espStub.flashData(
|
|
||||||
file,
|
|
||||||
(bytesWritten) => {
|
|
||||||
const newPct = Math.floor(
|
|
||||||
((totalWritten + bytesWritten) / totalSize) * 100
|
|
||||||
);
|
|
||||||
if (newPct === lastPct) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastPct = newPct;
|
|
||||||
logEl.addRow({
|
|
||||||
id: "write",
|
|
||||||
content: html`Writing progress: ${newPct}%`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
part.offset
|
|
||||||
);
|
|
||||||
totalWritten += file.byteLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
logEl.addRow({
|
|
||||||
id: "write",
|
|
||||||
content: html`Writing progress: 100%`,
|
|
||||||
});
|
|
||||||
|
|
||||||
await sleep(100);
|
|
||||||
await esploader.softReset();
|
|
||||||
await esploader.disconnect();
|
|
||||||
|
|
||||||
const doImprov =
|
|
||||||
build.improv &&
|
|
||||||
customElements.get("improv-wifi-launch-button")?.isSupported;
|
|
||||||
|
|
||||||
logEl.addRow({
|
|
||||||
id: "write",
|
|
||||||
content: html`Writing
|
|
||||||
complete${doImprov
|
|
||||||
? ""
|
|
||||||
: html`, all done!<br /><br /><button
|
|
||||||
@click=${() => logEl.parentElement?.removeChild(logEl)}
|
|
||||||
>
|
|
||||||
Close this dialog
|
|
||||||
</button>`}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!doImprov) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Todo: listen for improv events to know when to close dialog
|
|
||||||
logEl.addRow({
|
|
||||||
id: "improv",
|
|
||||||
action: true,
|
|
||||||
content: html`
|
|
||||||
<improv-wifi-launch-button
|
|
||||||
><button slot="activate">
|
|
||||||
Click here to finish setting up your device.
|
|
||||||
</button></improv-wifi-launch-button
|
|
||||||
>
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
21
src/util.ts
21
src/util.ts
@ -20,3 +20,24 @@ export const getChipFamilyName = (esploader: ESPLoader) => {
|
|||||||
|
|
||||||
export const sleep = (time: number) =>
|
export const sleep = (time: number) =>
|
||||||
new Promise((resolve) => setTimeout(resolve, time));
|
new Promise((resolve) => setTimeout(resolve, time));
|
||||||
|
|
||||||
|
export const fireEvent = <Event extends keyof HTMLElementEventMap>(
|
||||||
|
eventTarget: EventTarget,
|
||||||
|
type: Event,
|
||||||
|
// @ts-ignore
|
||||||
|
detail?: HTMLElementEventMap[Event]["detail"],
|
||||||
|
options?: {
|
||||||
|
bubbles?: boolean;
|
||||||
|
cancelable?: boolean;
|
||||||
|
composed?: boolean;
|
||||||
|
}
|
||||||
|
): void => {
|
||||||
|
options = options || {};
|
||||||
|
const event = new CustomEvent(type, {
|
||||||
|
bubbles: options.bubbles === undefined ? true : options.bubbles,
|
||||||
|
cancelable: Boolean(options.cancelable),
|
||||||
|
composed: options.composed === undefined ? true : options.composed,
|
||||||
|
detail,
|
||||||
|
});
|
||||||
|
eventTarget.dispatchEvent(event);
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user