Browse Source

Rewrite log element

Paulus Schoutsen 4 years ago
parent
commit
357b5f9303
7 changed files with 220 additions and 189 deletions
  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
 # 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
 ```json
 {
 {
@@ -17,11 +23,28 @@ Defined using a manifest.
         { "filename": "ota.bin", "offset": 57344 },
         { "filename": "ota.bin", "offset": 57344 },
         { "filename": "firmware.bin", "offset": 65536 }
         { "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
 ## Development
 
 
 Run `script/develop`. This starts a server. Open it on http://localhost:5000.
 Run `script/develop`. This starts a server. Open it on http://localhost:5000.

+ 3 - 3
example.html

@@ -17,13 +17,13 @@
   <body>
   <body>
     <p>ESPHome Web is a set of tools to allow working with ESP devices in the browser.</p>
     <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>
     <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"
       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><i>Note, this only works in desktop Chrome and Edge. Android support has not been implemented yet.</i></div>
     <p>
     <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.
       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>
     </p>
-    <script src="./dist/web/flash-button.js" type="module"></script>
+    <script src="./dist/web/install-button.js" type="module"></script>
   </body>
   </body>
 </html>
 </html>

+ 2 - 1
rollup.config.js

@@ -3,11 +3,12 @@ import json from "@rollup/plugin-json";
 import { terser } from "rollup-plugin-terser";
 import { terser } from "rollup-plugin-terser";
 
 
 const config = {
 const config = {
-  input: "dist/flash-button.js",
+  input: "dist/install-button.js",
   output: {
   output: {
     dir: "dist/web",
     dir: "dist/web",
     format: "module",
     format: "module",
   },
   },
+  external: ["https://www.improv-wifi.com/sdk-js/launch-button.js"],
   preserveEntrySignatures: false,
   preserveEntrySignatures: false,
   plugins: [nodeResolve(), json()],
   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")
 @customElement("esphome-web-flash-log")
 class FlashLog extends LitElement {
 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`
   static styles = css`
@@ -141,8 +84,12 @@ class FlashLog extends LitElement {
       cursor: pointer;
       cursor: pointer;
     }
     }
 
 
+    .action,
     .error {
     .error {
       margin-top: 1em;
       margin-top: 1em;
+    }
+
+    .error {
       color: red;
       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 { 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 "./flash-log";
 import { getChipFamilyName } from "./util";
 import { getChipFamilyName } from "./util";
 
 
 export const startFlash = async (
 export const startFlash = async (
   logger: Logger,
   logger: Logger,
   manifestPath: string,
   manifestPath: string,
-  logParent: HTMLElement
+  logParent: HTMLElement,
+  eraseFirst: boolean
 ) => {
 ) => {
   const manifestURL = new URL(manifestPath, location.toString()).toString();
   const manifestURL = new URL(manifestPath, location.toString()).toString();
   const manifestProm = fetch(manifestURL).then(
   const manifestProm = fetch(manifestURL).then(
     (resp): Promise<Manifest> => resp.json()
     (resp): Promise<Manifest> => resp.json()
   );
   );
 
 
-  let bytesWritten = 0;
-  let totalSize = 0;
-
   let esploader: ESPLoader | undefined;
   let esploader: ESPLoader | undefined;
-  let manifest: Manifest | undefined;
 
 
   try {
   try {
     esploader = await connect(logger);
     esploader = await connect(logger);
@@ -28,8 +26,12 @@ export const startFlash = async (
     return;
     return;
   }
   }
 
 
+  // For debugging
+  (window as any).esploader = esploader;
+
   const logEl = document.createElement("esphome-web-flash-log");
   const logEl = document.createElement("esphome-web-flash-log");
-  logEl.esploader = esploader;
+  // logEl.esploader = esploader;
+  logEl.addRow({ id: "initializing", content: "Initializing..." });
   logParent.append(logEl);
   logParent.append(logEl);
 
 
   try {
   try {
@@ -37,25 +39,33 @@ export const startFlash = async (
   } catch (err) {
   } catch (err) {
     console.error(err);
     console.error(err);
     if (esploader.connected) {
     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();
       await esploader.disconnect();
     }
     }
     return;
     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 {
   try {
     manifest = await manifestProm;
     manifest = await manifestProm;
   } catch (err) {
   } catch (err) {
-    logEl.errorMsg = `Unable to fetch manifest: ${err}`;
+    logEl.addError(`Unable to fetch manifest: ${err}`);
     await esploader.disconnect();
     await esploader.disconnect();
     return;
     return;
   }
   }
 
 
-  logEl.manifest = manifest;
+  logEl.addRow({
+    id: "manifest",
+    content: html`Found manifest for ${manifest.name}`,
+  });
 
 
   const chipFamily = getChipFamilyName(esploader);
   const chipFamily = getChipFamilyName(esploader);
 
 
@@ -68,21 +78,15 @@ export const startFlash = async (
   }
   }
 
 
   if (!build) {
   if (!build) {
-    logEl.errorMsg = `Your ${chipFamily} board is not supported.`;
+    logEl.addError(`Your ${chipFamily} board is not supported.`);
     await esploader.disconnect();
     await esploader.disconnect();
     return;
     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 filePromises = build.parts.map(async (part) => {
     const url = new URL(part.filename, manifestURL).toString();
     const url = new URL(part.filename, manifestURL).toString();
@@ -95,10 +99,17 @@ export const startFlash = async (
     return resp.arrayBuffer();
     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
   // Run the stub while we wait for files to download
   const espStub = await esploader.runStub();
   const espStub = await esploader.runStub();
 
 
   const files: ArrayBuffer[] = [];
   const files: ArrayBuffer[] = [];
+  let totalSize = 0;
 
 
   for (const prom of filePromises) {
   for (const prom of filePromises) {
     try {
     try {
@@ -106,14 +117,21 @@ export const startFlash = async (
       files.push(data);
       files.push(data);
       totalSize += data.byteLength;
       totalSize += data.byteLength;
     } catch (err) {
     } catch (err) {
-      logEl.errorMsg = err.message;
+      logEl.addError(err.message);
       await esploader.disconnect();
       await esploader.disconnect();
       return;
       return;
     }
     }
   }
   }
 
 
-  logEl.totalBytes = totalSize;
-  logEl.extraMsg = "";
+  logEl.removeRow("preparing");
+
+  if (eraseFirst) {
+    logEl.addRow({
+      id: "erase",
+      content: html`Erasing device`,
+    });
+  }
+
   let lastPct = -1;
   let lastPct = -1;
 
 
   for (const part of build.parts) {
   for (const part of build.parts) {
@@ -125,8 +143,10 @@ export const startFlash = async (
           return;
           return;
         }
         }
         lastPct = newPct;
         lastPct = newPct;
-        bytesWritten = newBytesWritten;
-        logEl.bytesWritten = bytesWritten;
+        logEl.addRow({
+          id: "write",
+          content: html`Writing progress: ${newPct}%`,
+        });
       },
       },
       part.offset
       part.offset
     );
     );
@@ -134,7 +154,38 @@ export const startFlash = async (
 
 
   await esploader.softReset();
   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();
   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
+      >
+    `,
+  });
 };
 };