1
0
Paulus Schoutsen 4 жил өмнө
parent
commit
357b5f9303
7 өөрчлөгдсөн 220 нэмэгдсэн , 189 устгасан
  1. 25 2
      README.md
  2. 3 3
      example.html
  3. 2 1
      rollup.config.js
  4. 0 43
      src/flash-button.ts
  5. 55 108
      src/flash-log.ts
  6. 52 0
      src/install-button.ts
  7. 83 32
      src/start-flash.ts

+ 25 - 2
README.md

@@ -1,8 +1,14 @@
 # JavaScript SDK for ESPHome
 
-Allow flashing ESPHome or other ESP-based firmwares via the browser.
+Allow flashing ESPHome or other ESP-based firmwares via the browser. Will automatically detect the board type and select a supported firmware.
 
-Defined using a manifest.
+```html
+<esphome-web-install-button
+  manifest="firmware_esphome/manifest.json"
+></esphome-web-install-button>
+```
+
+Manifest definition:
 
 ```json
 {
@@ -17,11 +23,28 @@ Defined using a manifest.
         { "filename": "ota.bin", "offset": 57344 },
         { "filename": "firmware.bin", "offset": 65536 }
       ]
+    },
+    {
+      "chipFamily": "ESP8266",
+      "parts": [
+        { "filename": "esp8266.bin", "offset": 0 },
+      ]
     }
   ]
 }
 ```
 
+Allows for optionally passing an attribute to trigger an erase before installation.
+
+```html
+<esphome-web-install-button
+  manifest="firmware_esphome/manifest.json"
+  erase-first
+></esphome-web-install-button>
+```
+
+All attributes can also be set via properties (`manifest`, `eraseFirst`)
+
 ## Development
 
 Run `script/develop`. This starts a server. Open it on http://localhost:5000.

+ 3 - 3
example.html

@@ -17,13 +17,13 @@
   <body>
     <p>ESPHome Web is a set of tools to allow working with ESP devices in the browser.</p>
     <p>To flash the XX firmware, connect an ESP to your computer and hit the button:</p>
-    <esphome-web-flash-button
+    <esphome-web-install-button
       manifest="firmware_build/manifest.json"
-    ></esphome-web-flash-button>
+    ></esphome-web-install-button>
     <p><i>Note, this only works in desktop Chrome and Edge. Android support has not been implemented yet.</i></div>
     <p>
       This works by combining Web Serial with a <a href="firmware_build/manifest.json">manifest</a> which describes the firmware. It will automatically detect the type of the connected ESP device and find the right firmware files in the manifest.
     </p>
-    <script src="./dist/web/flash-button.js" type="module"></script>
+    <script src="./dist/web/install-button.js" type="module"></script>
   </body>
 </html>

+ 2 - 1
rollup.config.js

@@ -3,11 +3,12 @@ import json from "@rollup/plugin-json";
 import { terser } from "rollup-plugin-terser";
 
 const config = {
-  input: "dist/flash-button.js",
+  input: "dist/install-button.js",
   output: {
     dir: "dist/web",
     format: "module",
   },
+  external: ["https://www.improv-wifi.com/sdk-js/launch-button.js"],
   preserveEntrySignatures: false,
   plugins: [nodeResolve(), json()],
 };

+ 0 - 43
src/flash-button.ts

@@ -1,43 +0,0 @@
-import "./vendor/esptool";
-
-class FlashButton extends HTMLElement {
-  public static isSupported = "serial" in navigator;
-
-  private renderRoot?: ShadowRoot;
-
-  public connectedCallback() {
-    if (this.renderRoot) {
-      return;
-    }
-
-    this.renderRoot = this.attachShadow({ mode: "open" });
-
-    if (FlashButton.isSupported) {
-      this.addEventListener("mouseover", () => {
-        // Preload
-        import("./start-flash");
-      });
-      this.addEventListener("click", async (ev) => {
-        ev.preventDefault();
-        const manifest = this.getAttribute("manifest");
-        if (!manifest) {
-          alert("No manifest defined!");
-          return;
-        }
-
-        const mod = await import("./start-flash");
-
-        const progress = document.createElement("div");
-        document.body.append(progress);
-
-        await mod.startFlash(console, manifest, progress);
-      });
-    }
-
-    this.renderRoot.innerHTML = FlashButton.isSupported
-      ? "<slot name='activate'><button>Flash device</button></slot>"
-      : "<slot name='unsupported'>Your browser does not support flashing ESP devices. Use Google Chrome or Microsoft Edge.</slot>";
-  }
-}
-
-customElements.define("esphome-web-flash-button", FlashButton);

+ 55 - 108
src/flash-log.ts

@@ -1,121 +1,64 @@
-import { css, html, HTMLTemplateResult, LitElement, PropertyValues } from "lit";
-import { customElement, property } from "lit/decorators.js";
-import { Manifest } from "./const";
-import { getChipFamilyName } from "./util";
-import { ESPLoader } from "./vendor/esptool/esp_loader";
+import { css, html, HTMLTemplateResult, LitElement } from "lit";
+import { customElement, state } from "lit/decorators.js";
+import { classMap } from "lit/directives/class-map.js";
+
+interface Row {
+  id?: string;
+  content: HTMLTemplateResult | string;
+  error?: boolean;
+  action?: boolean;
+}
 
 @customElement("esphome-web-flash-log")
 class FlashLog extends LitElement {
-  @property() public offerImprov = false;
-
-  @property() public esploader?: ESPLoader;
-
-  @property() public manifest?: Manifest;
-
-  @property() public totalBytes?: number;
-
-  @property() public bytesWritten?: number;
-
-  @property() public extraMsg: string = "";
-
-  @property() public errorMsg: string = "";
-
-  @property() public allowClose = false;
-
-  render() {
-    if (!this.esploader) {
-      return this._renderBody(["Establishing connection..."]);
-    }
-
-    const lines: Array<HTMLTemplateResult | string> = [
-      html`Connection established<br />`,
-    ];
-
-    if (!this.esploader.chipFamily) {
-      lines.push("Initializing...");
-      return this._renderBody(lines);
-    }
-
-    lines.push(
-      html`Initialized. Found ${getChipFamilyName(this.esploader)}<br />`
-    );
-
-    if (this.manifest === undefined) {
-      lines.push(html`Fetching manifest...<br />`);
-      return this._renderBody(lines);
-    }
-
-    lines.push(html`Found manifest for ${this.manifest.name}<br />`);
-
-    if (!this.totalBytes) {
-      return this._renderBody(lines);
-    }
-
-    lines.push(html`Bytes to be written: ${this.totalBytes}<br />`);
-
-    if (!this.bytesWritten) {
-      return this._renderBody(lines);
-    }
-
-    if (this.bytesWritten !== this.totalBytes) {
-      lines.push(
-        html`Writing progress:
-          ${Math.floor((this.bytesWritten / this.totalBytes) * 100)}%<br />`
-      );
-      return this._renderBody(lines);
-    }
-
-    const doImprov =
-      this.offerImprov &&
-      customElements.get("improv-wifi-launch-button")?.isSupported;
-
-    lines.push(html`Writing complete${doImprov ? "" : ", all done!"}<br />`);
-
-    if (doImprov) {
-      lines.push(html`
-        <br />
-        <improv-wifi-launch-button
-          ><button slot="activate">
-            Click here to finish setting up your device.
-          </button></improv-wifi-launch-button
+  @state() _rows: Row[] = [];
+
+  protected render() {
+    return html`${this._rows.map(
+      (row) =>
+        html`<div
+          class=${classMap({
+            error: row.error === true,
+            action: row.action === true,
+          })}
         >
-      `);
-    }
-
-    return this._renderBody(lines, !doImprov);
+          ${row.content}
+        </div>`
+    )}`;
   }
 
-  private _renderBody(
-    lines: Array<HTMLTemplateResult | string>,
-    allowClose = false
-  ) {
-    // allow closing if esploader not connected
-    // or we are at the end.
-    // TODO force allow close if not connected
-    return html`
-      ${lines} ${this.extraMsg}
-      ${allowClose
-        ? html` <br /><button @click=${this._close}>Close this dialog</button> `
-        : ""}
-      ${this.errorMsg
-        ? html`<div class="error">Error: ${this.errorMsg}</div>`
-        : ""}
-      ${this.esploader && !this.esploader.connected
-        ? html`<div class="error">Connection lost</div>`
-        : ""}
-    `;
+  /**
+   * Add or replace a row.
+   */
+  public addRow(row: Row) {
+    // If last entry has same ID, replace it.
+    if (
+      row.id &&
+      this._rows.length > 0 &&
+      this._rows[this._rows.length - 1].id === row.id
+    ) {
+      const newRows = this._rows.slice(0, -1);
+      newRows.push(row);
+      this._rows = newRows;
+    } else {
+      this._rows = [...this._rows, row];
+    }
   }
 
-  protected updated(props: PropertyValues) {
-    super.updated(props);
-
-    if (props.has("esploader") && this.esploader) {
-      this.esploader.addEventListener("disconnect", () => this.requestUpdate());
-    }
+  /**
+   * Add an error row
+   */
+  public addError(content: Row["content"]) {
+    this.addRow({ content, error: true });
   }
 
-  private _close() {
-    this.parentElement?.removeChild(this);
+  /**
+   * Remove last row if ID matches
+   */
+  public removeRow(id: string) {
+    if (this._rows.length > 0 && this._rows[this._rows.length - 1].id === id) {
+      this._rows = this._rows.slice(0, -1);
+    }
   }
 
   static styles = css`
@@ -141,8 +84,12 @@ class FlashLog extends LitElement {
       cursor: pointer;
     }
 
+    .action,
     .error {
       margin-top: 1em;
+    }
+
+    .error {
       color: red;
     }
   `;

+ 52 - 0
src/install-button.ts

@@ -0,0 +1,52 @@
+class InstallButton extends HTMLElement {
+  public static isSupported = "serial" in navigator;
+
+  public eraseFirst?: boolean;
+
+  private renderRoot?: ShadowRoot;
+
+  public connectedCallback() {
+    if (this.renderRoot) {
+      return;
+    }
+
+    this.renderRoot = this.attachShadow({ mode: "open" });
+
+    if (!InstallButton.isSupported) {
+      this.renderRoot.innerHTML =
+        "<slot name='unsupported'>Your browser does not support installing things on ESP devices. Use Google Chrome or Microsoft Edge.</slot>";
+      return;
+    }
+
+    this.addEventListener("mouseover", () => {
+      // Preload
+      import("./start-flash");
+    });
+    this.addEventListener("click", async (ev) => {
+      ev.preventDefault();
+      const manifest = this.getAttribute("manifest");
+      if (!manifest) {
+        alert("No manifest defined!");
+        return;
+      }
+
+      const mod = await import("./start-flash");
+
+      const progress = document.createElement("div");
+      document.body.append(progress);
+
+      await mod.startFlash(
+        console,
+        manifest,
+        progress,
+        this.eraseFirst !== undefined
+          ? this.eraseFirst
+          : this.hasAttribute("erase-first")
+      );
+    });
+
+    this.renderRoot.innerHTML = `<slot name='activate'><button>Install</button></slot>`;
+  }
+}
+
+customElements.define("esphome-web-install-button", InstallButton);

+ 83 - 32
src/start-flash.ts

@@ -1,25 +1,23 @@
-import { Build, Manifest } from "./const";
+import { html } from "lit";
 import { connect } from "./vendor/esptool";
-import { Logger } from "./vendor/esptool/const";
-import { ESPLoader } from "./vendor/esptool/esp_loader";
+import type { Logger } from "./vendor/esptool/const";
+import type { ESPLoader } from "./vendor/esptool/esp_loader";
+import { Build, Manifest } from "./const";
 import "./flash-log";
 import { getChipFamilyName } from "./util";
 
 export const startFlash = async (
   logger: Logger,
   manifestPath: string,
-  logParent: HTMLElement
+  logParent: HTMLElement,
+  eraseFirst: boolean
 ) => {
   const manifestURL = new URL(manifestPath, location.toString()).toString();
   const manifestProm = fetch(manifestURL).then(
     (resp): Promise<Manifest> => resp.json()
   );
 
-  let bytesWritten = 0;
-  let totalSize = 0;
-
   let esploader: ESPLoader | undefined;
-  let manifest: Manifest | undefined;
 
   try {
     esploader = await connect(logger);
@@ -28,8 +26,12 @@ export const startFlash = async (
     return;
   }
 
+  // For debugging
+  (window as any).esploader = esploader;
+
   const logEl = document.createElement("esphome-web-flash-log");
-  logEl.esploader = esploader;
+  // logEl.esploader = esploader;
+  logEl.addRow({ id: "initializing", content: "Initializing..." });
   logParent.append(logEl);
 
   try {
@@ -37,25 +39,33 @@ export const startFlash = async (
   } catch (err) {
     console.error(err);
     if (esploader.connected) {
-      logEl.errorMsg =
-        "Failed to initialize. Try resetting your device or holding the BOOT button before clicking connect.";
+      logEl.addError(
+        "Failed to initialize. Try resetting your device or holding the BOOT button before clicking connect."
+      );
       await esploader.disconnect();
     }
     return;
   }
 
-  // To reflect initialized status
-  logEl.requestUpdate();
+  logEl.addRow({
+    id: "initializing",
+    content: html`Initialized. Found ${getChipFamilyName(esploader)}`,
+  });
+  logEl.addRow({ id: "manifest", content: "Fetching manifest..." });
 
+  let manifest: Manifest | undefined;
   try {
     manifest = await manifestProm;
   } catch (err) {
-    logEl.errorMsg = `Unable to fetch manifest: ${err}`;
+    logEl.addError(`Unable to fetch manifest: ${err}`);
     await esploader.disconnect();
     return;
   }
 
-  logEl.manifest = manifest;
+  logEl.addRow({
+    id: "manifest",
+    content: html`Found manifest for ${manifest.name}`,
+  });
 
   const chipFamily = getChipFamilyName(esploader);
 
@@ -68,21 +78,15 @@ export const startFlash = async (
   }
 
   if (!build) {
-    logEl.errorMsg = `Your ${chipFamily} board is not supported.`;
+    logEl.addError(`Your ${chipFamily} board is not supported.`);
     await esploader.disconnect();
     return;
   }
 
-  logEl.offerImprov = build.improv;
-  logEl.extraMsg = "Preparing installation...";
-
-  // Pre-load improv for later
-  if (build.improv) {
-    // @ts-ignore
-    import("https://www.improv-wifi.com/sdk-js/launch-button.js");
-  }
-
-  (window as any).esploader = esploader;
+  logEl.addRow({
+    id: "preparing",
+    content: "Preparing installation...",
+  });
 
   const filePromises = build.parts.map(async (part) => {
     const url = new URL(part.filename, manifestURL).toString();
@@ -95,10 +99,17 @@ export const startFlash = async (
     return resp.arrayBuffer();
   });
 
+  // Pre-load improv for later
+  if (build.improv) {
+    // @ts-ignore
+    import("https://www.improv-wifi.com/sdk-js/launch-button.js");
+  }
+
   // 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 {
@@ -106,14 +117,21 @@ export const startFlash = async (
       files.push(data);
       totalSize += data.byteLength;
     } catch (err) {
-      logEl.errorMsg = err.message;
+      logEl.addError(err.message);
       await esploader.disconnect();
       return;
     }
   }
 
-  logEl.totalBytes = totalSize;
-  logEl.extraMsg = "";
+  logEl.removeRow("preparing");
+
+  if (eraseFirst) {
+    logEl.addRow({
+      id: "erase",
+      content: html`Erasing device`,
+    });
+  }
+
   let lastPct = -1;
 
   for (const part of build.parts) {
@@ -125,8 +143,10 @@ export const startFlash = async (
           return;
         }
         lastPct = newPct;
-        bytesWritten = newBytesWritten;
-        logEl.bytesWritten = bytesWritten;
+        logEl.addRow({
+          id: "write",
+          content: html`Writing progress: ${newPct}%`,
+        });
       },
       part.offset
     );
@@ -134,7 +154,38 @@ export const startFlash = async (
 
   await esploader.softReset();
 
-  logEl.bytesWritten = totalSize;
+  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=${() => logParent.removeChild(logEl)}
+          >
+            Close this dialog
+          </button>`}`,
+  });
 
   await esploader.disconnect();
+
+  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
+      >
+    `,
+  });
 };