Jelajahi Sumber

v4.0.0 - Next Gen (#87)

Paulus Schoutsen 3 tahun lalu
induk
melakukan
74187d9f44

+ 2 - 21
README.md

@@ -13,10 +13,10 @@ Manifest definition:
 ```json
 ```json
 {
 {
   "name": "ESPHome",
   "name": "ESPHome",
+  "version": "2021.10.3",
   "builds": [
   "builds": [
     {
     {
       "chipFamily": "ESP32",
       "chipFamily": "ESP32",
-      "improv": true,
       "parts": [
       "parts": [
         { "path": "bootloader.bin", "offset": 4096 },
         { "path": "bootloader.bin", "offset": 4096 },
         { "path": "partitions.bin", "offset": 32768 },
         { "path": "partitions.bin", "offset": 32768 },
@@ -46,17 +46,6 @@ Manifest definition:
 }
 }
 ```
 ```
 
 
-Allows for optionally passing an attribute to trigger an erase before installation.
-
-```html
-<esp-web-install-button
-  manifest="firmware_esphome/manifest.json"
-  erase-first
-></esp-web-install-button>
-```
-
-All attributes can also be set via properties (`manifest`, `eraseFirst`)
-
 ## Styling
 ## Styling
 
 
 ### Attributes
 ### Attributes
@@ -67,14 +56,6 @@ The following attributes are automatically added to `<esp-web-install-button>` a
 | -- | -- |
 | -- | -- |
 | `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)
 ### CSS custom properties (variables)
 
 
@@ -115,4 +96,4 @@ details | An optional extra field that is different [per state](https://github.c
 
 
 ## 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:5001.

+ 31 - 7
index.html

@@ -90,13 +90,22 @@
         padding: 8px;
         padding: 8px;
         border-bottom: 1px solid #ccc;
         border-bottom: 1px solid #ccc;
       }
       }
+      @media (prefers-color-scheme: dark) {
+        body {
+          background-color: #333;
+          color: #fff;
+        }
+        a {
+          color: #58a6ff;
+        }
+      }
     </style>
     </style>
     <script module>
     <script module>
       import(
       import(
         // In development we import locally.
         // In development we import locally.
         window.location.hostname === "localhost"
         window.location.hostname === "localhost"
           ? "/dist/web/install-button.js"
           ? "/dist/web/install-button.js"
-          : "https://unpkg.com/esp-web-tools@3.6.0/dist/web/install-button.js?module"
+          : "https://unpkg.com/esp-web-tools@4.0.0/dist/web/install-button.js?module"
       );
       );
     </script>
     </script>
   </head>
   </head>
@@ -112,8 +121,8 @@
       </p>
       </p>
       <p>
       <p>
         To try it out and install
         To try it out and install
-        <a href="https://esphome.io">the ESPHome firmware</a>, connect an ESP to
+        <a href="https://esphome.io">ESPHome</a> on an ESP, connect it to your
-        your computer and hit the button:
+        computer and hit the button:
       </p>
       </p>
       <esp-web-install-button
       <esp-web-install-button
         log-console
         log-console
@@ -209,7 +218,7 @@
       <pre>
       <pre>
 &lt;script
 &lt;script
   type="module"
   type="module"
-  src="https://unpkg.com/esp-web-tools@3.6.0/dist/web/install-button.js?module"
+  src="https://unpkg.com/esp-web-tools@4.0.0/dist/web/install-button.js?module"
 >&lt;/script>
 >&lt;/script>
 
 
 &lt;esp-web-install-button
 &lt;esp-web-install-button
@@ -243,17 +252,17 @@
         ESP Web Tools manifest describe the firmware that you want to install.
         ESP Web Tools manifest describe the firmware that you want to install.
         It allows specifying different builds for the different types of ESP
         It allows specifying different builds for the different types of ESP
         devices. Current supported chip families are <code>ESP8266</code>,
         devices. Current supported chip families are <code>ESP8266</code>,
-        <code>ESP32</code>, <code>ESP32-C3</code> and <code>ESP32-S2</code>. The
+        <code>ESP32</code>, <code>ESP32C3</code> and <code>ESP32S2</code>. The
         correct build will be automatically selected based on the type of the
         correct build will be automatically selected based on the type of the
         ESP device we detect via the serial port.
         ESP device we detect via the serial port.
       </p>
       </p>
       <pre>
       <pre>
 {
 {
   "name": "ESPHome",
   "name": "ESPHome",
+  "version": "2021.11.0",
   "builds": [
   "builds": [
     {
     {
       "chipFamily": "ESP32",
       "chipFamily": "ESP32",
-      "improv": true,
       "parts": [
       "parts": [
         { "path": "bootloader.bin", "offset": 4096 },
         { "path": "bootloader.bin", "offset": 4096 },
         { "path": "partitions.bin", "offset": 32768 },
         { "path": "partitions.bin", "offset": 32768 },
@@ -276,7 +285,22 @@
         where it should be installed. Part paths are resolved relative to the
         where it should be installed. Part paths are resolved relative to the
         path of the manifest, but can also be URLs to other hosts.
         path of the manifest, but can also be URLs to other hosts.
       </p>
       </p>
+      <h3 id="improv">Wi-Fi provisioning</h3>
+      <p>
+        ESP Web Tools has support for the
+        <a href="https://www.improv-wifi.com/serial"
+          >Improv Wi-Fi serial standard</a
+        >. This is an open standard to allow configuring Wi-Fi via the serial
+        port.
+      </p>
       <p>
       <p>
+        If Improv is supported, a user will be guided to connect the device to
+        the network after installation. It also allows the user to connect
+        already installed devices and re-configure the wireless network
+        settings.
+      </p>
+      <p>TODO EXAMPLE VIDEO</p>
+      <!-- <p>
         Each build also allows you to specify if it supports
         Each build also allows you to specify if it supports
         <a href="https://www.improv-wifi.com">the Improv Wi-Fi standard</a>. If
         <a href="https://www.improv-wifi.com">the Improv Wi-Fi standard</a>. If
         it does, the user will be offered to configure the Wi-Fi after
         it does, the user will be offered to configure the Wi-Fi after
@@ -292,7 +316,7 @@
           allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
           allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
           allowfullscreen
           allowfullscreen
         ></iframe>
         ></iframe>
-      </div>
+      </div> -->
 
 
       <h3 id="customize">Customizing the look and feel</h3>
       <h3 id="customize">Customizing the look and feel</h3>
       <p>
       <p>

+ 733 - 9
package-lock.json

@@ -1,15 +1,23 @@
 {
 {
   "name": "esp-web-tools",
   "name": "esp-web-tools",
-  "version": "3.6.0",
+  "version": "4.0.0",
   "lockfileVersion": 2,
   "lockfileVersion": 2,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
-      "version": "3.6.0",
+      "name": "esp-web-tools",
+      "version": "4.0.0",
       "license": "Apache-2.0",
       "license": "Apache-2.0",
       "dependencies": {
       "dependencies": {
+        "@material/mwc-button": "^0.25.3",
+        "@material/mwc-checkbox": "^0.25.3",
+        "@material/mwc-circular-progress": "^0.25.3",
+        "@material/mwc-dialog": "^0.25.3",
+        "@material/mwc-icon-button": "^0.25.3",
         "@material/mwc-linear-progress": "^0.25.1",
         "@material/mwc-linear-progress": "^0.25.1",
-        "esp-web-flasher": "^3.2.0",
+        "@material/mwc-textfield": "^0.25.3",
+        "esp-web-flasher": "^4.0.0",
+        "improv-wifi-serial-sdk": "^1.0.0",
         "lit": "^2.0.0",
         "lit": "^2.0.0",
         "tslib": "^2.3.1"
         "tslib": "^2.3.1"
       },
       },
@@ -72,6 +80,69 @@
         "tslib": "^2.1.0"
         "tslib": "^2.1.0"
       }
       }
     },
     },
+    "node_modules/@material/button": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/button/-/button-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-DB0MAvdIGWKuFwlQ57hjv7ZuHIioT2mnG7RWtL7ZoCWoY45nCrsbJirmX5zZFipm9gIOJ3YnIkIrUyMVSrDX+g==",
+      "dependencies": {
+        "@material/density": "14.0.0-canary.261f2db59.0",
+        "@material/dom": "14.0.0-canary.261f2db59.0",
+        "@material/elevation": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/ripple": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/shape": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "@material/tokens": "14.0.0-canary.261f2db59.0",
+        "@material/touch-target": "14.0.0-canary.261f2db59.0",
+        "@material/typography": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
+    "node_modules/@material/circular-progress": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-Gi6Ika8MEZQOT3Qei2NfTj+sRWxCDFjchPM7szNjIKgL2DyH03bHmodQFVcyBFiPWEcWMc/mqVYgGf/XJXs85w==",
+      "dependencies": {
+        "@material/animation": "14.0.0-canary.261f2db59.0",
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/progress-indicator": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
+    "node_modules/@material/density": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/density/-/density-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-zOR5wISqPVr8KS/ERNC1jdRV9O832lzclyS9Ea20rDrWfuOiYsQ9bbIk12xWlxpgsn7r9fxQJyd1O2SURoHdRA==",
+      "dependencies": {
+        "tslib": "^2.1.0"
+      }
+    },
+    "node_modules/@material/dialog": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-NfQR0fmNS/y2iRAx5YeODLLywBAnSyZI/CL9GUq4NiNj+FeSxe+5bhG1p9NxHeGMjEVrl6fG5L9ql7lqtfQaYQ==",
+      "dependencies": {
+        "@material/animation": "14.0.0-canary.261f2db59.0",
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/button": "14.0.0-canary.261f2db59.0",
+        "@material/dom": "14.0.0-canary.261f2db59.0",
+        "@material/elevation": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/icon-button": "14.0.0-canary.261f2db59.0",
+        "@material/ripple": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/shape": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "@material/tokens": "14.0.0-canary.261f2db59.0",
+        "@material/touch-target": "14.0.0-canary.261f2db59.0",
+        "@material/typography": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
     "node_modules/@material/dom": {
     "node_modules/@material/dom": {
       "version": "14.0.0-canary.261f2db59.0",
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/dom/-/dom-14.0.0-canary.261f2db59.0.tgz",
       "resolved": "https://registry.npmjs.org/@material/dom/-/dom-14.0.0-canary.261f2db59.0.tgz",
@@ -81,6 +152,19 @@
         "tslib": "^2.1.0"
         "tslib": "^2.1.0"
       }
       }
     },
     },
+    "node_modules/@material/elevation": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-AqN/tsTGGyBzZ7CtoSMBY9bDYvCuUt98EUfiGjZGXcf4HgoHV3Cn/JSLrhru5Cq8Nx6HF6AmHh3dQCfNCQduew==",
+      "dependencies": {
+        "@material/animation": "14.0.0-canary.261f2db59.0",
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
     "node_modules/@material/feature-targeting": {
     "node_modules/@material/feature-targeting": {
       "version": "14.0.0-canary.261f2db59.0",
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-14.0.0-canary.261f2db59.0.tgz",
       "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-14.0.0-canary.261f2db59.0.tgz",
@@ -89,6 +173,49 @@
         "tslib": "^2.1.0"
         "tslib": "^2.1.0"
       }
       }
     },
     },
+    "node_modules/@material/floating-label": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-Cp0/LngkW6/uZWbEDTe3Ox143V4kYtxl9twiM3XLKd6a67JHCzneQWFzC0qSg90b3r5O+1zOkT3ZMF2Pbu2Vwg==",
+      "dependencies": {
+        "@material/animation": "14.0.0-canary.261f2db59.0",
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/dom": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "@material/typography": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
+    "node_modules/@material/icon-button": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-9P6cjRqKtjE6ML+r5yz0ExU/f2KLdNabHQxmO6RpKd/FnjTyP1NcWqqj8dsvo/DZ7mOtT1MIThgkQDdiMqcYLg==",
+      "dependencies": {
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/density": "14.0.0-canary.261f2db59.0",
+        "@material/elevation": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/ripple": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "@material/touch-target": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
+    "node_modules/@material/line-ripple": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-LlyiyxpHNlFt0PZ8Q2tvOPbjNcgm3L7tUebXsM7iGyoKXfj0HwyDI31S0KgtU3Vs5DIK4U4mnRWtoAxtBW6Jfg==",
+      "dependencies": {
+        "@material/animation": "14.0.0-canary.261f2db59.0",
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
     "node_modules/@material/linear-progress": {
     "node_modules/@material/linear-progress": {
       "version": "14.0.0-canary.261f2db59.0",
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-14.0.0-canary.261f2db59.0.tgz",
       "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-14.0.0-canary.261f2db59.0.tgz",
@@ -115,6 +242,94 @@
         "tslib": "^2.0.1"
         "tslib": "^2.0.1"
       }
       }
     },
     },
+    "node_modules/@material/mwc-button": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-button/-/mwc-button-0.25.3.tgz",
+      "integrity": "sha512-usHEKchj9hqetY7n0yebTz1Pk9Z+9W/sNZheFoSaiWQCv9XhtCdKkHH0MXTv8SpwxWuEKUf/XjtyvikGIcIn7w==",
+      "dependencies": {
+        "@material/mwc-icon": "^0.25.3",
+        "@material/mwc-ripple": "^0.25.3",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "node_modules/@material/mwc-checkbox": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-checkbox/-/mwc-checkbox-0.25.3.tgz",
+      "integrity": "sha512-PSh9IAgQK4XiDzBwgclheejkA4cbZ3K9V1JTTl/YVRDD/OLLM+Bh8tbnAg/1kGVlPWOUfDrYCcZ0gg472ca7gw==",
+      "dependencies": {
+        "@material/mwc-base": "^0.25.3",
+        "@material/mwc-ripple": "^0.25.3",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "node_modules/@material/mwc-circular-progress": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-circular-progress/-/mwc-circular-progress-0.25.3.tgz",
+      "integrity": "sha512-ajgSzfdRfq0/sZg0Z5W/ZpgZwD8Ioj59m5ScCPXXdkRoVHf7+8lsD/2Fh4095GfoYE4PWSkXYVlWsQCx+aJbcA==",
+      "dependencies": {
+        "@material/circular-progress": "=14.0.0-canary.261f2db59.0",
+        "@material/mwc-base": "^0.25.3",
+        "@material/theme": "=14.0.0-canary.261f2db59.0",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "node_modules/@material/mwc-dialog": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-dialog/-/mwc-dialog-0.25.3.tgz",
+      "integrity": "sha512-UpxAYAzKXO1MW4ezpiYfEQgov08p0J8KDVKqKrMwg7lsZRkAtUMk4YJkM6qmWGqGPqd/cN++42PMPHAISJH3yA==",
+      "dependencies": {
+        "@material/dialog": "=14.0.0-canary.261f2db59.0",
+        "@material/dom": "=14.0.0-canary.261f2db59.0",
+        "@material/mwc-base": "^0.25.3",
+        "@material/mwc-button": "^0.25.3",
+        "blocking-elements": "^0.1.0",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1",
+        "wicg-inert": "^3.0.0"
+      }
+    },
+    "node_modules/@material/mwc-floating-label": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-floating-label/-/mwc-floating-label-0.25.3.tgz",
+      "integrity": "sha512-3uFMi8Y680P0nzP5zih4YuOZJLl/C6Ux9G810Unwo44zblG/ckgJlFiM+T+oR+OH5KM8LbfNlV0ypo7FT5zYJA==",
+      "dependencies": {
+        "@material/floating-label": "=14.0.0-canary.261f2db59.0",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "node_modules/@material/mwc-icon": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-icon/-/mwc-icon-0.25.3.tgz",
+      "integrity": "sha512-36076AWZIRSr8qYOLjuDDkxej/HA0XAosrj7TS1ZeLlUBnLUtbDtvc1S7KSa0hqez7ouzOqGaWK24yoNnTa2OA==",
+      "dependencies": {
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "node_modules/@material/mwc-icon-button": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-icon-button/-/mwc-icon-button-0.25.3.tgz",
+      "integrity": "sha512-FexkMpK3ZSHh7NF+PIqvVhvAbBOgFDYPck/lqnxIDC3VGJ0rjD/1MqevDy2fY6IcHGlc8Ai7VuYbdQ6Cvw8WcQ==",
+      "dependencies": {
+        "@material/mwc-ripple": "^0.25.3",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "node_modules/@material/mwc-line-ripple": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-line-ripple/-/mwc-line-ripple-0.25.3.tgz",
+      "integrity": "sha512-ANJzSyumb+shBVTIhqF1+YByPU/EpFXxI9CS26qThFqlUDpYXg5xcoZpkMSmZv3Wv/loF1rs2mJfFWOcC6nFnw==",
+      "dependencies": {
+        "@material/line-ripple": "=14.0.0-canary.261f2db59.0",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
     "node_modules/@material/mwc-linear-progress": {
     "node_modules/@material/mwc-linear-progress": {
       "version": "0.25.3",
       "version": "0.25.3",
       "resolved": "https://registry.npmjs.org/@material/mwc-linear-progress/-/mwc-linear-progress-0.25.3.tgz",
       "resolved": "https://registry.npmjs.org/@material/mwc-linear-progress/-/mwc-linear-progress-0.25.3.tgz",
@@ -127,6 +342,59 @@
         "tslib": "^2.0.1"
         "tslib": "^2.0.1"
       }
       }
     },
     },
+    "node_modules/@material/mwc-notched-outline": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-notched-outline/-/mwc-notched-outline-0.25.3.tgz",
+      "integrity": "sha512-8jvU8GD0Pke+pfTQ0PdXpZmkU3XIHhMVY6AHM/2IQrXHkVZmAm9kbwL7ne3Ao+6f5n+DeXDGd+SG9U6ZZjD7gw==",
+      "dependencies": {
+        "@material/mwc-base": "^0.25.3",
+        "@material/notched-outline": "=14.0.0-canary.261f2db59.0",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "node_modules/@material/mwc-ripple": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-ripple/-/mwc-ripple-0.25.3.tgz",
+      "integrity": "sha512-G/gt/csxgME6/sAku3GiuB0O2LLvoPWsRTLq/9iABpaGLJjqaKHvNg/IVzNDdF3YZT7EORgR9cBWWl7umA4i4Q==",
+      "dependencies": {
+        "@material/dom": "=14.0.0-canary.261f2db59.0",
+        "@material/mwc-base": "^0.25.3",
+        "@material/ripple": "=14.0.0-canary.261f2db59.0",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "node_modules/@material/mwc-textfield": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-textfield/-/mwc-textfield-0.25.3.tgz",
+      "integrity": "sha512-stpZ8sEyo2Mb9fG2XCoTc1Kom8oRXZiVI5rU88GtfcBU7nH0em8S4grq9X1mVfUG6Cfi1G/T+avCSIhzbYtr0w==",
+      "dependencies": {
+        "@material/floating-label": "=14.0.0-canary.261f2db59.0",
+        "@material/line-ripple": "=14.0.0-canary.261f2db59.0",
+        "@material/mwc-base": "^0.25.3",
+        "@material/mwc-floating-label": "^0.25.3",
+        "@material/mwc-line-ripple": "^0.25.3",
+        "@material/mwc-notched-outline": "^0.25.3",
+        "@material/textfield": "=14.0.0-canary.261f2db59.0",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "node_modules/@material/notched-outline": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-gtn+IKAiX2rbfbX3a9aDlfUoKCEYrlAPOZifKXUaZ4UJYMNLzZuAqy7l5Ds30emtqUE22mySTEWqhzK6dePKsA==",
+      "dependencies": {
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/floating-label": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/shape": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
     "node_modules/@material/progress-indicator": {
     "node_modules/@material/progress-indicator": {
       "version": "14.0.0-canary.261f2db59.0",
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-14.0.0-canary.261f2db59.0.tgz",
       "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-14.0.0-canary.261f2db59.0.tgz",
@@ -135,6 +403,20 @@
         "tslib": "^2.1.0"
         "tslib": "^2.1.0"
       }
       }
     },
     },
+    "node_modules/@material/ripple": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-3FLCLj8X7KrFfuYBHJg1b7Odb3V/AW7fxk3m1i1zhDnygKmlQ/abVucH1s2qbX3Y+JIiq+5/C5407h9BFtOf+A==",
+      "dependencies": {
+        "@material/animation": "14.0.0-canary.261f2db59.0",
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/dom": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
     "node_modules/@material/rtl": {
     "node_modules/@material/rtl": {
       "version": "14.0.0-canary.261f2db59.0",
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-14.0.0-canary.261f2db59.0.tgz",
       "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-14.0.0-canary.261f2db59.0.tgz",
@@ -144,6 +426,38 @@
         "tslib": "^2.1.0"
         "tslib": "^2.1.0"
       }
       }
     },
     },
+    "node_modules/@material/shape": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/shape/-/shape-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-VjcQltd1uF9ugvLExMy00SMISjy/370o8lsZlb1T+xHyhXHL3UxeuWYLW5Amq6mbx65+c9Df9WmlXXOdebpEkw==",
+      "dependencies": {
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
+    "node_modules/@material/textfield": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-KBPgpvvVFBfLx9nc6+wWOS2hJ40JVwh5KBjMoYbiOEFLf0O7SgCAVREHaFAXrPsC8AeTyUipx6TReONIGfMCPQ==",
+      "dependencies": {
+        "@material/animation": "14.0.0-canary.261f2db59.0",
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/density": "14.0.0-canary.261f2db59.0",
+        "@material/dom": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/floating-label": "14.0.0-canary.261f2db59.0",
+        "@material/line-ripple": "14.0.0-canary.261f2db59.0",
+        "@material/notched-outline": "14.0.0-canary.261f2db59.0",
+        "@material/ripple": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/shape": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "@material/typography": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
     "node_modules/@material/theme": {
     "node_modules/@material/theme": {
       "version": "14.0.0-canary.261f2db59.0",
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/theme/-/theme-14.0.0-canary.261f2db59.0.tgz",
       "resolved": "https://registry.npmjs.org/@material/theme/-/theme-14.0.0-canary.261f2db59.0.tgz",
@@ -153,6 +467,35 @@
         "tslib": "^2.1.0"
         "tslib": "^2.1.0"
       }
       }
     },
     },
+    "node_modules/@material/tokens": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-mgar9gsLv00HTvXIDvNR1vEEXpfKgeWhVTO8a7aWofSNyENNOVc5ImJwBgCAMb5SgLHBi6w8/c1tPzjOewBfCA==",
+      "dependencies": {
+        "@material/elevation": "14.0.0-canary.261f2db59.0"
+      }
+    },
+    "node_modules/@material/touch-target": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-xA6TTHN7aOTXg/+c6mQJlogzTD+Sp8WPC5TK8RBXbQxEykGXGW15p+H9pG+rX/gzD5iehnHRBrDUFmAGoskhcQ==",
+      "dependencies": {
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
+    "node_modules/@material/typography": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/typography/-/typography-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-WOCdcNkD5KBRAwICcRqWBRG3cDkyrwK5USTNmG0oxnwnZAN7daOpPTdLppVAhadE7faj8d67ON+V9pH7+T62FQ==",
+      "dependencies": {
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
     "node_modules/@rollup/plugin-json": {
     "node_modules/@rollup/plugin-json": {
       "version": "4.1.0",
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz",
       "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz",
@@ -337,6 +680,11 @@
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
       "dev": true
       "dev": true
     },
     },
+    "node_modules/blocking-elements": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/blocking-elements/-/blocking-elements-0.1.1.tgz",
+      "integrity": "sha512-/SLWbEzMoVIMZACCyhD/4Ya2M1PWP1qMKuiymowPcI+PdWDARqeARBjhj73kbUBCxEmTZCUu5TAqxtwUO9C1Ig=="
+    },
     "node_modules/boxen": {
     "node_modules/boxen": {
       "version": "5.1.2",
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
       "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
@@ -639,9 +987,9 @@
       }
       }
     },
     },
     "node_modules/esp-web-flasher": {
     "node_modules/esp-web-flasher": {
-      "version": "3.2.0",
+      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/esp-web-flasher/-/esp-web-flasher-3.2.0.tgz",
+      "resolved": "https://registry.npmjs.org/esp-web-flasher/-/esp-web-flasher-4.0.0.tgz",
-      "integrity": "sha512-jcJtWb5QuENWzeasfGYcJP/MV+XmRQelNRoOVCAKXcBJFh9h9NnfPXJtpoG+RsIMqb7hDdutomz/bBoBUH6urw==",
+      "integrity": "sha512-7d23iEkEjvrYkywLZtvg69GAitRJVE73dN6nmyWNmTvCe55b0UTzndLJtTHANbAiNzpgmJ7/kYnt202A7BD75A==",
       "dependencies": {
       "dependencies": {
         "pako": "^2.0.3",
         "pako": "^2.0.3",
         "tslib": "^2.2.0"
         "tslib": "^2.2.0"
@@ -760,6 +1108,19 @@
         "node": ">=4"
         "node": ">=4"
       }
       }
     },
     },
+    "node_modules/improv-wifi-serial-sdk": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/improv-wifi-serial-sdk/-/improv-wifi-serial-sdk-1.0.0.tgz",
+      "integrity": "sha512-R3NM7Ry9DjTyT5B6iIIZjW5LMia64PwLEJnue5lfYlmqHyJuNMxkWrGomqG7AxQLLCul7CPN1qs52nkJglqYsg==",
+      "dependencies": {
+        "@material/mwc-button": "^0.25.3",
+        "@material/mwc-circular-progress": "^0.25.3",
+        "@material/mwc-dialog": "^0.25.3",
+        "@material/mwc-textfield": "^0.25.3",
+        "lit": "^2.0.0",
+        "tslib": "^2.3.1"
+      }
+    },
     "node_modules/ini": {
     "node_modules/ini": {
       "version": "1.3.8",
       "version": "1.3.8",
       "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
       "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
@@ -1469,6 +1830,11 @@
         "which": "bin/which"
         "which": "bin/which"
       }
       }
     },
     },
+    "node_modules/wicg-inert": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/wicg-inert/-/wicg-inert-3.1.1.tgz",
+      "integrity": "sha512-PhBaNh8ur9Xm4Ggy4umelwNIP6pPP1bv3EaWaKqfb/QNme2rdLjm7wIInvV4WhxVHhzA4Spgw9qNSqWtB/ca2A=="
+    },
     "node_modules/widest-line": {
     "node_modules/widest-line": {
       "version": "3.1.0",
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
       "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
@@ -1586,6 +1952,69 @@
         "tslib": "^2.1.0"
         "tslib": "^2.1.0"
       }
       }
     },
     },
+    "@material/button": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/button/-/button-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-DB0MAvdIGWKuFwlQ57hjv7ZuHIioT2mnG7RWtL7ZoCWoY45nCrsbJirmX5zZFipm9gIOJ3YnIkIrUyMVSrDX+g==",
+      "requires": {
+        "@material/density": "14.0.0-canary.261f2db59.0",
+        "@material/dom": "14.0.0-canary.261f2db59.0",
+        "@material/elevation": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/ripple": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/shape": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "@material/tokens": "14.0.0-canary.261f2db59.0",
+        "@material/touch-target": "14.0.0-canary.261f2db59.0",
+        "@material/typography": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
+    "@material/circular-progress": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-Gi6Ika8MEZQOT3Qei2NfTj+sRWxCDFjchPM7szNjIKgL2DyH03bHmodQFVcyBFiPWEcWMc/mqVYgGf/XJXs85w==",
+      "requires": {
+        "@material/animation": "14.0.0-canary.261f2db59.0",
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/progress-indicator": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
+    "@material/density": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/density/-/density-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-zOR5wISqPVr8KS/ERNC1jdRV9O832lzclyS9Ea20rDrWfuOiYsQ9bbIk12xWlxpgsn7r9fxQJyd1O2SURoHdRA==",
+      "requires": {
+        "tslib": "^2.1.0"
+      }
+    },
+    "@material/dialog": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-NfQR0fmNS/y2iRAx5YeODLLywBAnSyZI/CL9GUq4NiNj+FeSxe+5bhG1p9NxHeGMjEVrl6fG5L9ql7lqtfQaYQ==",
+      "requires": {
+        "@material/animation": "14.0.0-canary.261f2db59.0",
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/button": "14.0.0-canary.261f2db59.0",
+        "@material/dom": "14.0.0-canary.261f2db59.0",
+        "@material/elevation": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/icon-button": "14.0.0-canary.261f2db59.0",
+        "@material/ripple": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/shape": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "@material/tokens": "14.0.0-canary.261f2db59.0",
+        "@material/touch-target": "14.0.0-canary.261f2db59.0",
+        "@material/typography": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
     "@material/dom": {
     "@material/dom": {
       "version": "14.0.0-canary.261f2db59.0",
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/dom/-/dom-14.0.0-canary.261f2db59.0.tgz",
       "resolved": "https://registry.npmjs.org/@material/dom/-/dom-14.0.0-canary.261f2db59.0.tgz",
@@ -1595,6 +2024,19 @@
         "tslib": "^2.1.0"
         "tslib": "^2.1.0"
       }
       }
     },
     },
+    "@material/elevation": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-AqN/tsTGGyBzZ7CtoSMBY9bDYvCuUt98EUfiGjZGXcf4HgoHV3Cn/JSLrhru5Cq8Nx6HF6AmHh3dQCfNCQduew==",
+      "requires": {
+        "@material/animation": "14.0.0-canary.261f2db59.0",
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
     "@material/feature-targeting": {
     "@material/feature-targeting": {
       "version": "14.0.0-canary.261f2db59.0",
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-14.0.0-canary.261f2db59.0.tgz",
       "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-14.0.0-canary.261f2db59.0.tgz",
@@ -1603,6 +2045,49 @@
         "tslib": "^2.1.0"
         "tslib": "^2.1.0"
       }
       }
     },
     },
+    "@material/floating-label": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-Cp0/LngkW6/uZWbEDTe3Ox143V4kYtxl9twiM3XLKd6a67JHCzneQWFzC0qSg90b3r5O+1zOkT3ZMF2Pbu2Vwg==",
+      "requires": {
+        "@material/animation": "14.0.0-canary.261f2db59.0",
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/dom": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "@material/typography": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
+    "@material/icon-button": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-9P6cjRqKtjE6ML+r5yz0ExU/f2KLdNabHQxmO6RpKd/FnjTyP1NcWqqj8dsvo/DZ7mOtT1MIThgkQDdiMqcYLg==",
+      "requires": {
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/density": "14.0.0-canary.261f2db59.0",
+        "@material/elevation": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/ripple": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "@material/touch-target": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
+    "@material/line-ripple": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-LlyiyxpHNlFt0PZ8Q2tvOPbjNcgm3L7tUebXsM7iGyoKXfj0HwyDI31S0KgtU3Vs5DIK4U4mnRWtoAxtBW6Jfg==",
+      "requires": {
+        "@material/animation": "14.0.0-canary.261f2db59.0",
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
     "@material/linear-progress": {
     "@material/linear-progress": {
       "version": "14.0.0-canary.261f2db59.0",
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-14.0.0-canary.261f2db59.0.tgz",
       "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-14.0.0-canary.261f2db59.0.tgz",
@@ -1629,6 +2114,94 @@
         "tslib": "^2.0.1"
         "tslib": "^2.0.1"
       }
       }
     },
     },
+    "@material/mwc-button": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-button/-/mwc-button-0.25.3.tgz",
+      "integrity": "sha512-usHEKchj9hqetY7n0yebTz1Pk9Z+9W/sNZheFoSaiWQCv9XhtCdKkHH0MXTv8SpwxWuEKUf/XjtyvikGIcIn7w==",
+      "requires": {
+        "@material/mwc-icon": "^0.25.3",
+        "@material/mwc-ripple": "^0.25.3",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "@material/mwc-checkbox": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-checkbox/-/mwc-checkbox-0.25.3.tgz",
+      "integrity": "sha512-PSh9IAgQK4XiDzBwgclheejkA4cbZ3K9V1JTTl/YVRDD/OLLM+Bh8tbnAg/1kGVlPWOUfDrYCcZ0gg472ca7gw==",
+      "requires": {
+        "@material/mwc-base": "^0.25.3",
+        "@material/mwc-ripple": "^0.25.3",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "@material/mwc-circular-progress": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-circular-progress/-/mwc-circular-progress-0.25.3.tgz",
+      "integrity": "sha512-ajgSzfdRfq0/sZg0Z5W/ZpgZwD8Ioj59m5ScCPXXdkRoVHf7+8lsD/2Fh4095GfoYE4PWSkXYVlWsQCx+aJbcA==",
+      "requires": {
+        "@material/circular-progress": "=14.0.0-canary.261f2db59.0",
+        "@material/mwc-base": "^0.25.3",
+        "@material/theme": "=14.0.0-canary.261f2db59.0",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "@material/mwc-dialog": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-dialog/-/mwc-dialog-0.25.3.tgz",
+      "integrity": "sha512-UpxAYAzKXO1MW4ezpiYfEQgov08p0J8KDVKqKrMwg7lsZRkAtUMk4YJkM6qmWGqGPqd/cN++42PMPHAISJH3yA==",
+      "requires": {
+        "@material/dialog": "=14.0.0-canary.261f2db59.0",
+        "@material/dom": "=14.0.0-canary.261f2db59.0",
+        "@material/mwc-base": "^0.25.3",
+        "@material/mwc-button": "^0.25.3",
+        "blocking-elements": "^0.1.0",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1",
+        "wicg-inert": "^3.0.0"
+      }
+    },
+    "@material/mwc-floating-label": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-floating-label/-/mwc-floating-label-0.25.3.tgz",
+      "integrity": "sha512-3uFMi8Y680P0nzP5zih4YuOZJLl/C6Ux9G810Unwo44zblG/ckgJlFiM+T+oR+OH5KM8LbfNlV0ypo7FT5zYJA==",
+      "requires": {
+        "@material/floating-label": "=14.0.0-canary.261f2db59.0",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "@material/mwc-icon": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-icon/-/mwc-icon-0.25.3.tgz",
+      "integrity": "sha512-36076AWZIRSr8qYOLjuDDkxej/HA0XAosrj7TS1ZeLlUBnLUtbDtvc1S7KSa0hqez7ouzOqGaWK24yoNnTa2OA==",
+      "requires": {
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "@material/mwc-icon-button": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-icon-button/-/mwc-icon-button-0.25.3.tgz",
+      "integrity": "sha512-FexkMpK3ZSHh7NF+PIqvVhvAbBOgFDYPck/lqnxIDC3VGJ0rjD/1MqevDy2fY6IcHGlc8Ai7VuYbdQ6Cvw8WcQ==",
+      "requires": {
+        "@material/mwc-ripple": "^0.25.3",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "@material/mwc-line-ripple": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-line-ripple/-/mwc-line-ripple-0.25.3.tgz",
+      "integrity": "sha512-ANJzSyumb+shBVTIhqF1+YByPU/EpFXxI9CS26qThFqlUDpYXg5xcoZpkMSmZv3Wv/loF1rs2mJfFWOcC6nFnw==",
+      "requires": {
+        "@material/line-ripple": "=14.0.0-canary.261f2db59.0",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
     "@material/mwc-linear-progress": {
     "@material/mwc-linear-progress": {
       "version": "0.25.3",
       "version": "0.25.3",
       "resolved": "https://registry.npmjs.org/@material/mwc-linear-progress/-/mwc-linear-progress-0.25.3.tgz",
       "resolved": "https://registry.npmjs.org/@material/mwc-linear-progress/-/mwc-linear-progress-0.25.3.tgz",
@@ -1641,6 +2214,59 @@
         "tslib": "^2.0.1"
         "tslib": "^2.0.1"
       }
       }
     },
     },
+    "@material/mwc-notched-outline": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-notched-outline/-/mwc-notched-outline-0.25.3.tgz",
+      "integrity": "sha512-8jvU8GD0Pke+pfTQ0PdXpZmkU3XIHhMVY6AHM/2IQrXHkVZmAm9kbwL7ne3Ao+6f5n+DeXDGd+SG9U6ZZjD7gw==",
+      "requires": {
+        "@material/mwc-base": "^0.25.3",
+        "@material/notched-outline": "=14.0.0-canary.261f2db59.0",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "@material/mwc-ripple": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-ripple/-/mwc-ripple-0.25.3.tgz",
+      "integrity": "sha512-G/gt/csxgME6/sAku3GiuB0O2LLvoPWsRTLq/9iABpaGLJjqaKHvNg/IVzNDdF3YZT7EORgR9cBWWl7umA4i4Q==",
+      "requires": {
+        "@material/dom": "=14.0.0-canary.261f2db59.0",
+        "@material/mwc-base": "^0.25.3",
+        "@material/ripple": "=14.0.0-canary.261f2db59.0",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "@material/mwc-textfield": {
+      "version": "0.25.3",
+      "resolved": "https://registry.npmjs.org/@material/mwc-textfield/-/mwc-textfield-0.25.3.tgz",
+      "integrity": "sha512-stpZ8sEyo2Mb9fG2XCoTc1Kom8oRXZiVI5rU88GtfcBU7nH0em8S4grq9X1mVfUG6Cfi1G/T+avCSIhzbYtr0w==",
+      "requires": {
+        "@material/floating-label": "=14.0.0-canary.261f2db59.0",
+        "@material/line-ripple": "=14.0.0-canary.261f2db59.0",
+        "@material/mwc-base": "^0.25.3",
+        "@material/mwc-floating-label": "^0.25.3",
+        "@material/mwc-line-ripple": "^0.25.3",
+        "@material/mwc-notched-outline": "^0.25.3",
+        "@material/textfield": "=14.0.0-canary.261f2db59.0",
+        "lit": "^2.0.0",
+        "tslib": "^2.0.1"
+      }
+    },
+    "@material/notched-outline": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-gtn+IKAiX2rbfbX3a9aDlfUoKCEYrlAPOZifKXUaZ4UJYMNLzZuAqy7l5Ds30emtqUE22mySTEWqhzK6dePKsA==",
+      "requires": {
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/floating-label": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/shape": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
     "@material/progress-indicator": {
     "@material/progress-indicator": {
       "version": "14.0.0-canary.261f2db59.0",
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-14.0.0-canary.261f2db59.0.tgz",
       "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-14.0.0-canary.261f2db59.0.tgz",
@@ -1649,6 +2275,20 @@
         "tslib": "^2.1.0"
         "tslib": "^2.1.0"
       }
       }
     },
     },
+    "@material/ripple": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-3FLCLj8X7KrFfuYBHJg1b7Odb3V/AW7fxk3m1i1zhDnygKmlQ/abVucH1s2qbX3Y+JIiq+5/C5407h9BFtOf+A==",
+      "requires": {
+        "@material/animation": "14.0.0-canary.261f2db59.0",
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/dom": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
     "@material/rtl": {
     "@material/rtl": {
       "version": "14.0.0-canary.261f2db59.0",
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-14.0.0-canary.261f2db59.0.tgz",
       "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-14.0.0-canary.261f2db59.0.tgz",
@@ -1658,6 +2298,38 @@
         "tslib": "^2.1.0"
         "tslib": "^2.1.0"
       }
       }
     },
     },
+    "@material/shape": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/shape/-/shape-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-VjcQltd1uF9ugvLExMy00SMISjy/370o8lsZlb1T+xHyhXHL3UxeuWYLW5Amq6mbx65+c9Df9WmlXXOdebpEkw==",
+      "requires": {
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
+    "@material/textfield": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-KBPgpvvVFBfLx9nc6+wWOS2hJ40JVwh5KBjMoYbiOEFLf0O7SgCAVREHaFAXrPsC8AeTyUipx6TReONIGfMCPQ==",
+      "requires": {
+        "@material/animation": "14.0.0-canary.261f2db59.0",
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/density": "14.0.0-canary.261f2db59.0",
+        "@material/dom": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/floating-label": "14.0.0-canary.261f2db59.0",
+        "@material/line-ripple": "14.0.0-canary.261f2db59.0",
+        "@material/notched-outline": "14.0.0-canary.261f2db59.0",
+        "@material/ripple": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "@material/shape": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "@material/typography": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
     "@material/theme": {
     "@material/theme": {
       "version": "14.0.0-canary.261f2db59.0",
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/theme/-/theme-14.0.0-canary.261f2db59.0.tgz",
       "resolved": "https://registry.npmjs.org/@material/theme/-/theme-14.0.0-canary.261f2db59.0.tgz",
@@ -1667,6 +2339,35 @@
         "tslib": "^2.1.0"
         "tslib": "^2.1.0"
       }
       }
     },
     },
+    "@material/tokens": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-mgar9gsLv00HTvXIDvNR1vEEXpfKgeWhVTO8a7aWofSNyENNOVc5ImJwBgCAMb5SgLHBi6w8/c1tPzjOewBfCA==",
+      "requires": {
+        "@material/elevation": "14.0.0-canary.261f2db59.0"
+      }
+    },
+    "@material/touch-target": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-xA6TTHN7aOTXg/+c6mQJlogzTD+Sp8WPC5TK8RBXbQxEykGXGW15p+H9pG+rX/gzD5iehnHRBrDUFmAGoskhcQ==",
+      "requires": {
+        "@material/base": "14.0.0-canary.261f2db59.0",
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/rtl": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
+    "@material/typography": {
+      "version": "14.0.0-canary.261f2db59.0",
+      "resolved": "https://registry.npmjs.org/@material/typography/-/typography-14.0.0-canary.261f2db59.0.tgz",
+      "integrity": "sha512-WOCdcNkD5KBRAwICcRqWBRG3cDkyrwK5USTNmG0oxnwnZAN7daOpPTdLppVAhadE7faj8d67ON+V9pH7+T62FQ==",
+      "requires": {
+        "@material/feature-targeting": "14.0.0-canary.261f2db59.0",
+        "@material/theme": "14.0.0-canary.261f2db59.0",
+        "tslib": "^2.1.0"
+      }
+    },
     "@rollup/plugin-json": {
     "@rollup/plugin-json": {
       "version": "4.1.0",
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz",
       "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz",
@@ -1824,6 +2525,11 @@
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
       "dev": true
       "dev": true
     },
     },
+    "blocking-elements": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/blocking-elements/-/blocking-elements-0.1.1.tgz",
+      "integrity": "sha512-/SLWbEzMoVIMZACCyhD/4Ya2M1PWP1qMKuiymowPcI+PdWDARqeARBjhj73kbUBCxEmTZCUu5TAqxtwUO9C1Ig=="
+    },
     "boxen": {
     "boxen": {
       "version": "5.1.2",
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
       "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
@@ -2061,9 +2767,9 @@
       "dev": true
       "dev": true
     },
     },
     "esp-web-flasher": {
     "esp-web-flasher": {
-      "version": "3.2.0",
+      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/esp-web-flasher/-/esp-web-flasher-3.2.0.tgz",
+      "resolved": "https://registry.npmjs.org/esp-web-flasher/-/esp-web-flasher-4.0.0.tgz",
-      "integrity": "sha512-jcJtWb5QuENWzeasfGYcJP/MV+XmRQelNRoOVCAKXcBJFh9h9NnfPXJtpoG+RsIMqb7hDdutomz/bBoBUH6urw==",
+      "integrity": "sha512-7d23iEkEjvrYkywLZtvg69GAitRJVE73dN6nmyWNmTvCe55b0UTzndLJtTHANbAiNzpgmJ7/kYnt202A7BD75A==",
       "requires": {
       "requires": {
         "pako": "^2.0.3",
         "pako": "^2.0.3",
         "tslib": "^2.2.0"
         "tslib": "^2.2.0"
@@ -2163,6 +2869,19 @@
       "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
       "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
       "dev": true
       "dev": true
     },
     },
+    "improv-wifi-serial-sdk": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/improv-wifi-serial-sdk/-/improv-wifi-serial-sdk-1.0.0.tgz",
+      "integrity": "sha512-R3NM7Ry9DjTyT5B6iIIZjW5LMia64PwLEJnue5lfYlmqHyJuNMxkWrGomqG7AxQLLCul7CPN1qs52nkJglqYsg==",
+      "requires": {
+        "@material/mwc-button": "^0.25.3",
+        "@material/mwc-circular-progress": "^0.25.3",
+        "@material/mwc-dialog": "^0.25.3",
+        "@material/mwc-textfield": "^0.25.3",
+        "lit": "^2.0.0",
+        "tslib": "^2.3.1"
+      }
+    },
     "ini": {
     "ini": {
       "version": "1.3.8",
       "version": "1.3.8",
       "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
       "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
@@ -2747,6 +3466,11 @@
         "isexe": "^2.0.0"
         "isexe": "^2.0.0"
       }
       }
     },
     },
+    "wicg-inert": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/wicg-inert/-/wicg-inert-3.1.1.tgz",
+      "integrity": "sha512-PhBaNh8ur9Xm4Ggy4umelwNIP6pPP1bv3EaWaKqfb/QNme2rdLjm7wIInvV4WhxVHhzA4Spgw9qNSqWtB/ca2A=="
+    },
     "widest-line": {
     "widest-line": {
       "version": "3.1.0",
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
       "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",

+ 9 - 2
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "esp-web-tools",
   "name": "esp-web-tools",
-  "version": "3.6.0",
+  "version": "4.0.0",
   "description": "Web tools for ESP devices",
   "description": "Web tools for ESP devices",
   "main": "dist/install-button.js",
   "main": "dist/install-button.js",
   "repository": "https://github.com/esphome/web",
   "repository": "https://github.com/esphome/web",
@@ -21,8 +21,15 @@
     "typescript": "^4.3.2"
     "typescript": "^4.3.2"
   },
   },
   "dependencies": {
   "dependencies": {
+    "@material/mwc-button": "^0.25.3",
+    "@material/mwc-checkbox": "^0.25.3",
+    "@material/mwc-circular-progress": "^0.25.3",
+    "@material/mwc-dialog": "^0.25.3",
+    "@material/mwc-icon-button": "^0.25.3",
     "@material/mwc-linear-progress": "^0.25.1",
     "@material/mwc-linear-progress": "^0.25.1",
-    "esp-web-flasher": "^3.2.0",
+    "@material/mwc-textfield": "^0.25.3",
+    "esp-web-flasher": "^4.0.0",
+    "improv-wifi-serial-sdk": "^1.0.0",
     "lit": "^2.0.0",
     "lit": "^2.0.0",
     "tslib": "^2.3.1"
     "tslib": "^2.3.1"
   }
   }

+ 1 - 1
script/develop

@@ -11,7 +11,7 @@ trap "kill 0" EXIT
 # Run tsc once as rollup expects those files
 # Run tsc once as rollup expects those files
 tsc || true
 tsc || true
 
 
-npm exec -- serve &
+npm exec -- serve -p 5001 &
 npm exec -- tsc --watch &
 npm exec -- tsc --watch &
 npm exec -- rollup -c --watch &
 npm exec -- rollup -c --watch &
 wait
 wait

+ 14 - 0
src/components/ewt-button.ts

@@ -0,0 +1,14 @@
+import { ButtonBase } from "@material/mwc-button/mwc-button-base";
+import { styles } from "@material/mwc-button/styles.css";
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "ewt-button": EwtButton;
+  }
+}
+
+export class EwtButton extends ButtonBase {
+  static override styles = [styles];
+}
+
+customElements.define("ewt-button", EwtButton);

+ 14 - 0
src/components/ewt-circular-progress.ts

@@ -0,0 +1,14 @@
+import { CircularProgressBase } from "@material/mwc-circular-progress/mwc-circular-progress-base";
+import { styles } from "@material/mwc-circular-progress/mwc-circular-progress.css";
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "ewt-circular-progress": EwtCircularProgress;
+  }
+}
+
+export class EwtCircularProgress extends CircularProgressBase {
+  static override styles = [styles];
+}
+
+customElements.define("ewt-circular-progress", EwtCircularProgress);

+ 227 - 0
src/components/ewt-console.ts

@@ -0,0 +1,227 @@
+import { ColoredConsole } from "../util/console-color";
+import { sleep } from "../util/sleep";
+import { LineBreakTransformer } from "../util/line-break-transformer";
+import { Logger } from "../const";
+
+export class EwtConsole extends HTMLElement {
+  public port!: SerialPort;
+  public logger!: Logger;
+
+  private _console?: ColoredConsole;
+  private _cancelConnection?: () => Promise<void>;
+
+  public connectedCallback() {
+    if (this._console) {
+      return;
+    }
+    const shadowRoot = this.attachShadow({ mode: "open" });
+
+    shadowRoot.innerHTML = `
+      <style>
+        :host, input {
+          background-color: #1c1c1c;
+          color: #ddd;
+          font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
+            monospace;
+          line-height: 1.45;
+        }
+        .log {
+          box-sizing: border-box;
+          height: calc(100% - 28px);
+          font-size: 12px;
+          padding: 16px;
+          overflow: auto;
+          white-space: pre-wrap;
+          overflow-wrap: break-word;
+        }
+
+        .log-bold {
+          font-weight: bold;
+        }
+        .log-italic {
+          font-style: italic;
+        }
+        .log-underline {
+          text-decoration: underline;
+        }
+        .log-strikethrough {
+          text-decoration: line-through;
+        }
+        .log-underline.log-strikethrough {
+          text-decoration: underline line-through;
+        }
+        .log-secret {
+          -webkit-user-select: none;
+          -moz-user-select: none;
+          -ms-user-select: none;
+          user-select: none;
+        }
+        .log-secret-redacted {
+          opacity: 0;
+          width: 1px;
+          font-size: 1px;
+        }
+        .log-fg-black {
+          color: rgb(128, 128, 128);
+        }
+        .log-fg-red {
+          color: rgb(255, 0, 0);
+        }
+        .log-fg-green {
+          color: rgb(0, 255, 0);
+        }
+        .log-fg-yellow {
+          color: rgb(255, 255, 0);
+        }
+        .log-fg-blue {
+          color: rgb(0, 0, 255);
+        }
+        .log-fg-magenta {
+          color: rgb(255, 0, 255);
+        }
+        .log-fg-cyan {
+          color: rgb(0, 255, 255);
+        }
+        .log-fg-white {
+          color: rgb(187, 187, 187);
+        }
+        .log-bg-black {
+          background-color: rgb(0, 0, 0);
+        }
+        .log-bg-red {
+          background-color: rgb(255, 0, 0);
+        }
+        .log-bg-green {
+          background-color: rgb(0, 255, 0);
+        }
+        .log-bg-yellow {
+          background-color: rgb(255, 255, 0);
+        }
+        .log-bg-blue {
+          background-color: rgb(0, 0, 255);
+        }
+        .log-bg-magenta {
+          background-color: rgb(255, 0, 255);
+        }
+        .log-bg-cyan {
+          background-color: rgb(0, 255, 255);
+        }
+        .log-bg-white {
+          background-color: rgb(255, 255, 255);
+        }
+        form {
+          display: flex;
+          align-items: center;
+          padding: 0 8px 0 16px;
+        }
+        input {
+          flex: 1;
+          padding: 4px;
+          margin: 0 8px;
+          border: 0;
+          outline: none;
+        }
+      </style>
+      <div class="log"></div>
+      <form>
+        >
+        <input autofocus>
+        <button type="button">Send</button>
+      </form>
+    `;
+
+    this._console = new ColoredConsole(this.shadowRoot!.querySelector("div")!);
+    const input = this.shadowRoot!.querySelector("input")!;
+
+    this.addEventListener("click", () => input.focus());
+
+    input.addEventListener("keydown", (ev) => {
+      if (ev.key === "Enter") {
+        ev.preventDefault();
+        ev.stopPropagation();
+        this._sendCommand();
+      }
+    });
+
+    const abortController = new AbortController();
+    const connection = this._connect(abortController.signal);
+    this._cancelConnection = () => {
+      abortController.abort();
+      return connection;
+    };
+  }
+
+  private async _connect(abortSignal: AbortSignal) {
+    this.logger.debug("Starting console read loop");
+    try {
+      await this.port
+        .readable!.pipeThrough(new TextDecoderStream(), {
+          signal: abortSignal,
+        })
+        .pipeThrough(new TransformStream(new LineBreakTransformer()))
+        .pipeTo(
+          new WritableStream({
+            write: (chunk) => {
+              this._console!.addLine(chunk);
+            },
+          })
+        );
+      if (!abortSignal.aborted) {
+        this._console!.addLine("");
+        this._console!.addLine("");
+        this._console!.addLine("Terminal disconnected");
+      }
+    } catch (e) {
+      this._console!.addLine("");
+      this._console!.addLine("");
+      this._console!.addLine(`Terminal disconnected: ${e}`);
+    } finally {
+      await sleep(100);
+      this.logger.debug("Finished console read loop");
+    }
+  }
+
+  private async _sendCommand() {
+    const input = this.shadowRoot!.querySelector("input")!;
+    const command = input.value;
+    const encoder = new TextEncoder();
+    const writer = this.port.writable!.getWriter();
+    await writer.write(encoder.encode(command));
+    this._console!.addLine(`> ${command}\n`);
+    input.value = "";
+    input.focus();
+    try {
+      writer.releaseLock();
+    } catch (err) {
+      console.error("Ignoring release lock error", err);
+    }
+  }
+
+  public async disconnect() {
+    if (this._cancelConnection) {
+      await this._cancelConnection();
+      this._cancelConnection = undefined;
+    }
+  }
+
+  public async reset() {
+    this.logger.debug("Triggering reset.");
+    await this.port.setSignals({
+      dataTerminalReady: false,
+      requestToSend: true,
+    });
+    await this.port.setSignals({
+      dataTerminalReady: false,
+      requestToSend: false,
+    });
+    await new Promise((resolve) => setTimeout(resolve, 1000));
+  }
+}
+
+customElements.define("ewt-console", EwtConsole);
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "ewt-console": EwtConsole;
+  }
+}

+ 22 - 0
src/components/ewt-dialog.ts

@@ -0,0 +1,22 @@
+import { DialogBase } from "@material/mwc-dialog/mwc-dialog-base";
+import { styles } from "@material/mwc-dialog/mwc-dialog.css";
+import { css } from "lit";
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "ewt-dialog": EwtDialog;
+  }
+}
+
+export class EwtDialog extends DialogBase {
+  static override styles = [
+    styles,
+    css`
+      .mdc-dialog__title {
+        padding-right: 52px;
+      }
+    `,
+  ];
+}
+
+customElements.define("ewt-dialog", EwtDialog);

+ 14 - 0
src/components/ewt-icon-button.ts

@@ -0,0 +1,14 @@
+import { IconButtonBase } from "@material/mwc-icon-button/mwc-icon-button-base";
+import { styles } from "@material/mwc-icon-button/mwc-icon-button.css";
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "ewt-icon-button": EwtIconButton;
+  }
+}
+
+export class EwtIconButton extends IconButtonBase {
+  static override styles = [styles];
+}
+
+customElements.define("ewt-icon-button", EwtIconButton);

+ 14 - 0
src/components/ewt-textfield.ts

@@ -0,0 +1,14 @@
+import { TextFieldBase } from "@material/mwc-textfield/mwc-textfield-base";
+import { styles } from "@material/mwc-textfield/mwc-textfield.css";
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "ewt-textfield": EwtTextfield;
+  }
+}
+
+export class EwtTextfield extends TextFieldBase {
+  static override styles = [styles];
+}
+
+customElements.define("ewt-textfield", EwtTextfield);

+ 30 - 0
src/connect.ts

@@ -0,0 +1,30 @@
+import type { InstallButton } from "./install-button.js";
+import "./install-dialog.js";
+
+export const connect = async (button: InstallButton) => {
+  let port: SerialPort | undefined;
+  try {
+    port = await navigator.serial.requestPort();
+  } catch (err) {
+    console.error("User cancelled request", err);
+    return;
+  }
+
+  if (!port) {
+    return;
+  }
+
+  await port.open({ baudRate: 115200 });
+
+  const el = document.createElement("ewt-install-dialog");
+  el.port = port;
+  el.manifestPath = button.getAttribute("manifest")!;
+  el.addEventListener(
+    "closed",
+    () => {
+      port!.close();
+    },
+    { once: true }
+  );
+  document.body.appendChild(el);
+};

+ 16 - 10
src/const.ts

@@ -1,6 +1,11 @@
+export interface Logger {
+  log(msg: string, ...args: any[]): void;
+  error(msg: string, ...args: any[]): void;
+  debug(msg: string, ...args: any[]): void;
+}
+
 export interface Build {
 export interface Build {
   chipFamily: "ESP32" | "ESP8266" | "ESP32-S2" | "ESP32-C3";
   chipFamily: "ESP32" | "ESP8266" | "ESP32-S2" | "ESP32-C3";
-  improv: boolean;
   parts: {
   parts: {
     path: string;
     path: string;
     offset: number;
     offset: number;
@@ -9,11 +14,12 @@ export interface Build {
 
 
 export interface Manifest {
 export interface Manifest {
   name: string;
   name: string;
+  version: string;
   builds: Build[];
   builds: Build[];
 }
 }
 
 
 export interface BaseFlashState {
 export interface BaseFlashState {
-  state: State;
+  state: FlashStateType;
   message: string;
   message: string;
   manifest?: Manifest;
   manifest?: Manifest;
   build?: Build;
   build?: Build;
@@ -21,36 +27,36 @@ export interface BaseFlashState {
 }
 }
 
 
 export interface InitializingState extends BaseFlashState {
 export interface InitializingState extends BaseFlashState {
-  state: State.INITIALIZING;
+  state: FlashStateType.INITIALIZING;
   details: { done: boolean };
   details: { done: boolean };
 }
 }
 
 
 export interface ManifestState extends BaseFlashState {
 export interface ManifestState extends BaseFlashState {
-  state: State.MANIFEST;
+  state: FlashStateType.MANIFEST;
   details: { done: boolean };
   details: { done: boolean };
 }
 }
 
 
 export interface PreparingState extends BaseFlashState {
 export interface PreparingState extends BaseFlashState {
-  state: State.PREPARING;
+  state: FlashStateType.PREPARING;
   details: { done: boolean };
   details: { done: boolean };
 }
 }
 
 
 export interface ErasingState extends BaseFlashState {
 export interface ErasingState extends BaseFlashState {
-  state: State.ERASING;
+  state: FlashStateType.ERASING;
   details: { done: boolean };
   details: { done: boolean };
 }
 }
 
 
 export interface WritingState extends BaseFlashState {
 export interface WritingState extends BaseFlashState {
-  state: State.WRITING;
+  state: FlashStateType.WRITING;
   details: { bytesTotal: number; bytesWritten: number; percentage: number };
   details: { bytesTotal: number; bytesWritten: number; percentage: number };
 }
 }
 
 
 export interface FinishedState extends BaseFlashState {
 export interface FinishedState extends BaseFlashState {
-  state: State.FINISHED;
+  state: FlashStateType.FINISHED;
 }
 }
 
 
 export interface ErrorState extends BaseFlashState {
 export interface ErrorState extends BaseFlashState {
-  state: State.ERROR;
+  state: FlashStateType.ERROR;
   details: { error: FlashError; details: string | Error };
   details: { error: FlashError; details: string | Error };
 }
 }
 
 
@@ -63,7 +69,7 @@ export type FlashState =
   | FinishedState
   | FinishedState
   | ErrorState;
   | ErrorState;
 
 
-export const enum State {
+export const enum FlashStateType {
   INITIALIZING = "initializing",
   INITIALIZING = "initializing",
   MANIFEST = "manifest",
   MANIFEST = "manifest",
   PREPARING = "preparing",
   PREPARING = "preparing",

+ 0 - 138
src/flash-log.ts

@@ -1,138 +0,0 @@
-import { css, html, HTMLTemplateResult, LitElement } from "lit";
-import { customElement, state } from "lit/decorators.js";
-import { classMap } from "lit/directives/class-map.js";
-import { FlashState, State } from "./const";
-
-interface Row {
-  state?: State;
-  message: HTMLTemplateResult | string;
-  error?: boolean;
-  action?: boolean;
-}
-
-@customElement("esp-web-flash-log")
-export class FlashLog extends LitElement {
-  @state() private _rows: Row[] = [];
-
-  protected render() {
-    return html`${this._rows.map(
-      (row) =>
-        html`<div
-          class=${classMap({
-            error: row.error === true,
-            action: row.action === true,
-          })}
-        >
-          ${row.message}
-        </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.
-   */
-  public addRow(row: Row) {
-    // If last entry has same ID, replace it.
-    if (
-      row.state &&
-      this._rows.length > 0 &&
-      this._rows[this._rows.length - 1].state === row.state
-    ) {
-      const newRows = this._rows.slice(0, -1);
-      newRows.push(row);
-      this._rows = newRows;
-    } else {
-      this._rows = [...this._rows, row];
-    }
-  }
-
-  /**
-   * Add an error row
-   */
-  public addError(message: Row["message"]) {
-    this.addRow({ message, error: true });
-  }
-
-  /**
-   * Add an action row
-   */
-  public addAction(message: Row["message"]) {
-    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);
-    }
-  }
-
-  static styles = css`
-    :host {
-      display: block;
-      margin-top: 16px;
-      padding: 12px 16px;
-      font-family: monospace;
-      background: var(--esp-tools-log-background, black);
-      color: var(--esp-tools-log-text-color, greenyellow);
-      font-size: 14px;
-      line-height: 19px;
-    }
-
-    :host([hidden]) {
-      display: none;
-    }
-
-    button {
-      background: none;
-      color: inherit;
-      border: none;
-      padding: 0;
-      font: inherit;
-      text-align: left;
-      text-decoration: underline;
-      cursor: pointer;
-    }
-
-    .error {
-      color: var(--esp-tools-error-color, #dc3545);
-    }
-
-    .error,
-    .action {
-      margin-top: 1em;
-    }
-  `;
-}
-
-declare global {
-  interface HTMLElementTagNameMap {
-    "esp-web-flash-log": FlashLog;
-  }
-}

+ 0 - 88
src/flash-progress.ts

@@ -1,88 +0,0 @@
-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`&nbsp;`}
-      </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);
-    }
-    mwc-linear-progress {
-      text-align: left;
-    }
-    h2 {
-      margin: 16px 0 0;
-    }
-    p {
-      margin: 4px 0;
-    }
-  `;
-}
-
-declare global {
-  interface HTMLElementTagNameMap {
-    "esp-web-flash-progress": FlashProgress;
-  }
-}

+ 47 - 42
src/flash.ts

@@ -1,9 +1,17 @@
-import { connect, ESPLoader, Logger } from "esp-web-flasher";
+import { ESPLoader, Logger } from "esp-web-flasher";
-import { Build, FlashError, FlashState, Manifest, State } from "./const";
+import {
-import { fireEvent, getChipFamilyName, sleep } from "./util";
+  Build,
+  FlashError,
+  FlashState,
+  Manifest,
+  FlashStateType,
+} from "./const";
+import { getChipFamilyName } from "./util/chip-family-name";
+import { sleep } from "./util/sleep";
 
 
 export const flash = async (
 export const flash = async (
-  eventTarget: EventTarget,
+  onEvent: (state: FlashState) => void,
+  port: SerialPort,
   logger: Logger,
   logger: Logger,
   manifestPath: string,
   manifestPath: string,
   eraseFirst: boolean
   eraseFirst: boolean
@@ -12,47 +20,39 @@ export const flash = async (
   let build: Build | undefined;
   let build: Build | undefined;
   let chipFamily: ReturnType<typeof getChipFamilyName>;
   let chipFamily: ReturnType<typeof getChipFamilyName>;
 
 
-  const fireStateEvent = (stateUpdate: FlashState) => {
+  const fireStateEvent = (stateUpdate: FlashState) =>
-    fireEvent(eventTarget, "state-changed", {
+    onEvent({
       ...stateUpdate,
       ...stateUpdate,
       manifest,
       manifest,
       build,
       build,
       chipFamily,
       chipFamily,
     });
     });
-  };
 
 
   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 esploader: ESPLoader | undefined;
+  const esploader = new ESPLoader(port, logger);
-
-  try {
-    esploader = await connect(logger);
-  } catch (err) {
-    // User pressed cancel on web serial
-    return;
-  }
 
 
   // For debugging
   // For debugging
   (window as any).esploader = esploader;
   (window as any).esploader = esploader;
 
 
   fireStateEvent({
   fireStateEvent({
-    state: State.INITIALIZING,
+    state: FlashStateType.INITIALIZING,
     message: "Initializing...",
     message: "Initializing...",
     details: { done: false },
     details: { done: false },
   });
   });
 
 
   try {
   try {
     await esploader.initialize();
     await esploader.initialize();
-  } catch (err) {
+  } catch (err: any) {
     logger.error(err);
     logger.error(err);
     if (esploader.connected) {
     if (esploader.connected) {
       fireStateEvent({
       fireStateEvent({
-        state: State.ERROR,
+        state: FlashStateType.ERROR,
         message:
         message:
-          "Failed to initialize. Try resetting your device or holding the BOOT button while selecting your serial port.",
+          "Failed to initialize. Try resetting your device or holding the BOOT button while clicking INSTALL.",
         details: { error: FlashError.FAILED_INITIALIZING, details: err },
         details: { error: FlashError.FAILED_INITIALIZING, details: err },
       });
       });
       await esploader.disconnect();
       await esploader.disconnect();
@@ -63,22 +63,22 @@ export const flash = async (
   chipFamily = getChipFamilyName(esploader);
   chipFamily = getChipFamilyName(esploader);
 
 
   fireStateEvent({
   fireStateEvent({
-    state: State.INITIALIZING,
+    state: FlashStateType.INITIALIZING,
     message: `Initialized. Found ${chipFamily}`,
     message: `Initialized. Found ${chipFamily}`,
     details: { done: true },
     details: { done: true },
   });
   });
   fireStateEvent({
   fireStateEvent({
-    state: State.MANIFEST,
+    state: FlashStateType.MANIFEST,
     message: "Fetching manifest...",
     message: "Fetching manifest...",
     details: { done: false },
     details: { done: false },
   });
   });
 
 
   try {
   try {
     manifest = await manifestProm;
     manifest = await manifestProm;
-  } catch (err) {
+  } catch (err: any) {
     fireStateEvent({
     fireStateEvent({
-      state: State.ERROR,
+      state: FlashStateType.ERROR,
-      message: `Unable to fetch manifest: ${err.message}`,
+      message: `Unable to fetch manifest: ${err}`,
       details: { error: FlashError.FAILED_MANIFEST_FETCH, details: err },
       details: { error: FlashError.FAILED_MANIFEST_FETCH, details: err },
     });
     });
     await esploader.disconnect();
     await esploader.disconnect();
@@ -88,14 +88,14 @@ export const flash = async (
   build = manifest.builds.find((b) => b.chipFamily === chipFamily);
   build = manifest.builds.find((b) => b.chipFamily === chipFamily);
 
 
   fireStateEvent({
   fireStateEvent({
-    state: State.MANIFEST,
+    state: FlashStateType.MANIFEST,
     message: `Found manifest for ${manifest.name}`,
     message: `Found manifest for ${manifest.name}`,
     details: { done: true },
     details: { done: true },
   });
   });
 
 
   if (!build) {
   if (!build) {
     fireStateEvent({
     fireStateEvent({
-      state: State.ERROR,
+      state: FlashStateType.ERROR,
       message: `Your ${chipFamily} board is not supported.`,
       message: `Your ${chipFamily} board is not supported.`,
       details: { error: FlashError.NOT_SUPPORTED, details: chipFamily },
       details: { error: FlashError.NOT_SUPPORTED, details: chipFamily },
     });
     });
@@ -104,7 +104,7 @@ export const flash = async (
   }
   }
 
 
   fireStateEvent({
   fireStateEvent({
-    state: State.PREPARING,
+    state: FlashStateType.PREPARING,
     message: "Preparing installation...",
     message: "Preparing installation...",
     details: { done: false },
     details: { done: false },
   });
   });
@@ -131,11 +131,14 @@ export const flash = async (
       const data = await prom;
       const data = await prom;
       files.push(data);
       files.push(data);
       totalSize += data.byteLength;
       totalSize += data.byteLength;
-    } catch (err) {
+    } catch (err: any) {
       fireStateEvent({
       fireStateEvent({
-        state: State.ERROR,
+        state: FlashStateType.ERROR,
-        message: err,
+        message: err.message,
-        details: { error: FlashError.FAILED_FIRMWARE_DOWNLOAD, details: err },
+        details: {
+          error: FlashError.FAILED_FIRMWARE_DOWNLOAD,
+          details: err.message,
+        },
       });
       });
       await esploader.disconnect();
       await esploader.disconnect();
       return;
       return;
@@ -143,20 +146,20 @@ export const flash = async (
   }
   }
 
 
   fireStateEvent({
   fireStateEvent({
-    state: State.PREPARING,
+    state: FlashStateType.PREPARING,
     message: "Installation prepared",
     message: "Installation prepared",
     details: { done: true },
     details: { done: true },
   });
   });
 
 
   if (eraseFirst) {
   if (eraseFirst) {
     fireStateEvent({
     fireStateEvent({
-      state: State.ERASING,
+      state: FlashStateType.ERASING,
       message: "Erasing device...",
       message: "Erasing device...",
       details: { done: false },
       details: { done: false },
     });
     });
     await espStub.eraseFlash();
     await espStub.eraseFlash();
     fireStateEvent({
     fireStateEvent({
-      state: State.ERASING,
+      state: FlashStateType.ERASING,
       message: "Device erased",
       message: "Device erased",
       details: { done: true },
       details: { done: true },
     });
     });
@@ -165,7 +168,7 @@ export const flash = async (
   let lastPct = 0;
   let lastPct = 0;
 
 
   fireStateEvent({
   fireStateEvent({
-    state: State.WRITING,
+    state: FlashStateType.WRITING,
     message: `Writing progress: ${lastPct}%`,
     message: `Writing progress: ${lastPct}%`,
     details: {
     details: {
       bytesTotal: totalSize,
       bytesTotal: totalSize,
@@ -190,7 +193,7 @@ export const flash = async (
           }
           }
           lastPct = newPct;
           lastPct = newPct;
           fireStateEvent({
           fireStateEvent({
-            state: State.WRITING,
+            state: FlashStateType.WRITING,
             message: `Writing progress: ${newPct}%`,
             message: `Writing progress: ${newPct}%`,
             details: {
             details: {
               bytesTotal: totalSize,
               bytesTotal: totalSize,
@@ -202,10 +205,10 @@ export const flash = async (
         part.offset,
         part.offset,
         true
         true
       );
       );
-    } catch (err) {
+    } catch (err: any) {
       fireStateEvent({
       fireStateEvent({
-        state: State.ERROR,
+        state: FlashStateType.ERROR,
-        message: err,
+        message: err.message,
         details: { error: FlashError.WRITE_FAILED, details: err },
         details: { error: FlashError.WRITE_FAILED, details: err },
       });
       });
       await esploader.disconnect();
       await esploader.disconnect();
@@ -215,7 +218,7 @@ export const flash = async (
   }
   }
 
 
   fireStateEvent({
   fireStateEvent({
-    state: State.WRITING,
+    state: FlashStateType.WRITING,
     message: "Writing complete",
     message: "Writing complete",
     details: {
     details: {
       bytesTotal: totalSize,
       bytesTotal: totalSize,
@@ -225,11 +228,13 @@ export const flash = async (
   });
   });
 
 
   await sleep(100);
   await sleep(100);
-  await esploader.hardReset();
+  console.log("DISCONNECT");
   await esploader.disconnect();
   await esploader.disconnect();
+  console.log("HARD RESET");
+  await esploader.hardReset();
 
 
   fireStateEvent({
   fireStateEvent({
-    state: State.FINISHED,
+    state: FlashStateType.FINISHED,
     message: "All done!",
     message: "All done!",
   });
   });
 };
 };

+ 3 - 3
src/install-button.ts

@@ -72,7 +72,7 @@ export class InstallButton extends HTMLElement {
   public renderRoot?: ShadowRoot;
   public renderRoot?: ShadowRoot;
 
 
   public static preload() {
   public static preload() {
-    import("./start-flash");
+    import("./connect");
   }
   }
 
 
   public connectedCallback() {
   public connectedCallback() {
@@ -98,8 +98,8 @@ export class InstallButton extends HTMLElement {
 
 
     slot.addEventListener("click", async (ev) => {
     slot.addEventListener("click", async (ev) => {
       ev.preventDefault();
       ev.preventDefault();
-      const mod = await import("./start-flash");
+      const mod = await import("./connect");
-      mod.startFlash(this);
+      mod.connect(this);
     });
     });
 
 
     slot.name = "activate";
     slot.name = "activate";

+ 718 - 0
src/install-dialog.ts

@@ -0,0 +1,718 @@
+import { LitElement, html, PropertyValues, css, TemplateResult } from "lit";
+import { state } from "lit/decorators.js";
+import { ifDefined } from "lit/directives/if-defined.js";
+import "./components/ewt-dialog";
+import "./components/ewt-textfield";
+import "./components/ewt-button";
+import "./components/ewt-icon-button";
+import "./components/ewt-circular-progress";
+import type { EwtTextfield } from "./components/ewt-textfield";
+import { Logger, Manifest, FlashStateType, FlashState } from "./const.js";
+import { ImprovSerial } from "improv-wifi-serial-sdk/dist/serial";
+import {
+  ImprovSerialCurrentState,
+  ImprovSerialErrorState,
+  PortNotReady,
+} from "improv-wifi-serial-sdk/dist/const";
+import { fireEvent } from "./util/fire-event";
+import { flash } from "./flash";
+import "./components/ewt-console";
+import { sleep } from "./util/sleep";
+
+const ERROR_ICON = "⚠️";
+const OK_ICON = "🎉";
+
+const messageTemplate = (icon: string, label: string) => html`
+  <div class="center">
+    <div class="icon">${icon}</div>
+    ${label}
+  </div>
+`;
+
+class EwtInstallDialog extends LitElement {
+  public port!: SerialPort;
+
+  public manifestPath!: string;
+
+  public logger: Logger = console;
+
+  private _manifest!: Manifest;
+
+  private _info?: ImprovSerial["info"];
+
+  // null = NOT_SUPPORTED
+  @state() private _client?: ImprovSerial | null;
+
+  @state() private _state:
+    | "ERROR"
+    | "DASHBOARD"
+    | "PROVISION"
+    | "INSTALL"
+    | "LOGS" = "DASHBOARD";
+
+  @state() private _installErase = false;
+  @state() private _installConfirmed = false;
+  @state() private _installState?: FlashState;
+
+  @state() private _provisionForce = false;
+
+  @state() private _error?: string;
+
+  @state() private _busy = false;
+
+  private _progressFeedback?: {
+    resolve: (_: unknown) => void;
+    reject: () => void;
+  };
+
+  protected render() {
+    if (!this.port) {
+      return html``;
+    }
+    let heading: string | undefined;
+    let content: TemplateResult;
+    let hideActions = false;
+    let allowClosing = false;
+
+    // During installation phase we temporarily remove the client
+    if (
+      this._client === undefined &&
+      this._state !== "INSTALL" &&
+      this._state !== "LOGS"
+    ) {
+      if (this._error) {
+        content = this._renderMessage(ERROR_ICON, this._error, true);
+      } else {
+        content = this._renderProgress("Connecting");
+        hideActions = true;
+      }
+    } else if (this._state === "INSTALL") {
+      [heading, content, hideActions, allowClosing] = this._renderInstall();
+    } else if (this._state === "ERROR") {
+      heading = "Error";
+      content = this._renderMessage(ERROR_ICON, this._error!, true);
+    } else if (this._state === "DASHBOARD") {
+      [heading, content, hideActions, allowClosing] = this._renderDashboard();
+    } else if (this._state === "PROVISION") {
+      [heading, content, hideActions] = this._renderProvision();
+    } else if (this._state === "LOGS") {
+      [heading, content, hideActions] = this._renderLogs();
+    }
+
+    return html`
+      <ewt-dialog
+        open
+        .heading=${heading!}
+        scrimClickAction
+        @closed=${this._handleClose}
+        .hideActions=${hideActions}
+      >
+        ${heading && allowClosing
+          ? html`
+              <ewt-icon-button dialogAction="close">
+                <svg width="24" height="24" viewBox="0 0 24 24">
+                  <path
+                    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"
+                  />
+                </svg>
+              </ewt-icon-button>
+            `
+          : ""}
+        ${content!}
+      </ewt-dialog>
+    `;
+  }
+
+  _renderProgress(label: string | TemplateResult, progress?: number) {
+    return html`
+      <div class="center">
+        <div>
+          <ewt-circular-progress
+            active
+            ?indeterminate=${progress === undefined}
+            .progress=${progress !== undefined ? progress / 100 : undefined}
+            density="8"
+          ></ewt-circular-progress>
+          ${progress !== undefined
+            ? html`<div class="progress-pct">${progress}%</div>`
+            : ""}
+        </div>
+        ${label}
+      </div>
+    `;
+  }
+  _renderMessage(icon: string, label: string, showClose: boolean) {
+    return html`
+      ${messageTemplate(icon, label)}
+      ${showClose &&
+      html`
+        <ewt-button
+          slot="primaryAction"
+          dialogAction="ok"
+          label="Close"
+        ></ewt-button>
+      `}
+    `;
+  }
+
+  _renderDashboard(): [string, TemplateResult, boolean, boolean] {
+    const heading = this._info!.name;
+    let content: TemplateResult;
+    let hideActions = true;
+    let allowClosing = true;
+
+    const isSameFirmware = this._info!.firmware === this._manifest!.name;
+    const isSameVersion =
+      isSameFirmware && this._info!.version === this._manifest!.version;
+
+    content = html`
+      <div class="device-info">
+        ${this._info!.firmware}&nbsp;${this._info!.version}
+      </div>
+      <div class="dashboard-buttons">
+        ${this._client!.nextUrl === undefined
+          ? ""
+          : html`
+              <div>
+                <a
+                  href=${this._client!.nextUrl}
+                  class="has-button"
+                  target="_blank"
+                >
+                  <ewt-button label="Set up Device"></ewt-button>
+                </a>
+              </div>
+            `}
+        <div>
+          <ewt-button
+            .label=${this._client!.state === ImprovSerialCurrentState.READY
+              ? "Connect to Wi-Fi"
+              : "Change Wi-Fi"}
+            @click=${() => {
+              this._state = "PROVISION";
+              if (
+                this._client!.state === ImprovSerialCurrentState.PROVISIONED
+              ) {
+                this._provisionForce = true;
+              }
+            }}
+          ></ewt-button>
+        </div>
+        <div>
+          <ewt-button
+            .label=${!isSameFirmware
+              ? `Install ${this._manifest!.name}`
+              : isSameVersion
+              ? "Up to date"
+              : "Update"}
+            @click=${() => this._startInstall(!isSameFirmware)}
+            .disabled=${isSameVersion}
+          ></ewt-button>
+        </div>
+        <div>
+          <ewt-button
+            label="Logs"
+            @click=${async () => {
+              const client = this._client;
+              if (client) {
+                await this._closeClientWithoutEvents(client);
+                await sleep(100);
+              }
+              // Also set `null` back to undefined.
+              this._client = undefined;
+              this._state = "LOGS";
+            }}
+          ></ewt-button>
+        </div>
+      </div>
+    `;
+
+    return [heading, content, hideActions, allowClosing];
+  }
+
+  _renderProvision(): [string | undefined, TemplateResult, boolean] {
+    let heading: string | undefined = "Configure Wi-Fi";
+    let content: TemplateResult;
+    let hideActions = false;
+
+    if (this._busy) {
+      return [heading, this._renderProgress("Trying to connect"), true];
+    }
+
+    if (
+      !this._provisionForce &&
+      this._client!.state === ImprovSerialCurrentState.PROVISIONED
+    ) {
+      heading = undefined;
+      content = html`
+        ${messageTemplate(OK_ICON, "Device connected to the network!")}
+        ${
+          // If we went to provision after installing the firmware with a full erase,
+          // there is nothing left for the user, let them go to the device dashboard
+          // if available
+          this._installState?.state === FlashStateType.FINISHED &&
+          this._installErase &&
+          this._client!.nextUrl !== undefined
+            ? html`
+                <a
+                  slot="primaryAction"
+                  href=${this._client!.nextUrl}
+                  class="has-button"
+                  target="_blank"
+                  @click=${() => {
+                    this._state = "DASHBOARD";
+                  }}
+                >
+                  <ewt-button label="Set up Device"></ewt-button>
+                </a>
+                <ewt-button
+                  slot="secondaryAction"
+                  label="Skip"
+                  @click=${() => {
+                    this._state = "DASHBOARD";
+                    this._installState = undefined;
+                  }}
+                ></ewt-button>
+              `
+            : html`
+                <ewt-button
+                  slot="primaryAction"
+                  label="Continue"
+                  @click=${() => {
+                    this._state = "DASHBOARD";
+                  }}
+                ></ewt-button>
+              `
+        }
+      `;
+    } else {
+      let error: string | undefined;
+
+      switch (this._client!.error) {
+        case ImprovSerialErrorState.UNABLE_TO_CONNECT:
+          error = "Unable to connect";
+          break;
+
+        case ImprovSerialErrorState.NO_ERROR:
+          break;
+
+        default:
+          error = `Unknown error (${this._client!.error})`;
+      }
+      content = html`
+        <div>
+          Enter the credentials of the Wi-Fi network that you want your device
+          to connect to.
+        </div>
+        ${error ? html`<p class="error">${error}</p>` : ""}
+        <ewt-textfield label="Network Name" name="ssid"></ewt-textfield>
+        <ewt-textfield
+          label="Password"
+          name="password"
+          type="password"
+        ></ewt-textfield>
+        <ewt-button
+          slot="primaryAction"
+          label="Connect"
+          @click="${this._doProvision}"
+        ></ewt-button>
+        <ewt-button
+          slot="secondaryAction"
+          .label=${this._installState && this._installErase ? "Skip" : "Back"}
+          @click=${() => {
+            this._installState = undefined;
+            this._state = "DASHBOARD";
+          }}
+        ></ewt-button>
+      `;
+    }
+    return [heading, content, hideActions];
+  }
+
+  _renderInstall(): [string | undefined, TemplateResult, boolean, boolean] {
+    let heading: string | undefined = `Install ${this._manifest!.name}`;
+    let content: TemplateResult;
+    let hideActions = false;
+    let allowClosing = false;
+
+    const isUpdate = !this._installErase && this._isUpdate;
+
+    if (!this._installConfirmed) {
+      const action = isUpdate ? "update to" : "install";
+      content = html`
+        ${isUpdate
+          ? html`Your device is running
+              ${this._info!.firmware}&nbsp;${this._info!.version}.<br /><br />`
+          : ""}
+        Do you want to ${action}
+        ${this._manifest!.name}&nbsp;${this._manifest!.version}?
+        ${this._installErase
+          ? "All existing data will be erased from your device."
+          : ""}
+        <ewt-button
+          slot="primaryAction"
+          label="Install"
+          @click=${this._confirmInstall}
+        ></ewt-button>
+        ${this._client
+          ? html`
+              <ewt-button
+                slot="secondaryAction"
+                label="Back"
+                @click=${() => {
+                  this._state = "DASHBOARD";
+                }}
+              ></ewt-button>
+            `
+          : html`
+              <ewt-button
+                slot="secondaryAction"
+                label="Logs"
+                @click=${async () => {
+                  // In case it was null
+                  this._client = undefined;
+                  this._state = "LOGS";
+                }}
+              ></ewt-button>
+            `}
+      `;
+      allowClosing = !this._client;
+    } else if (
+      !this._installState ||
+      this._installState.state === FlashStateType.INITIALIZING ||
+      this._installState.state === FlashStateType.MANIFEST ||
+      this._installState.state === FlashStateType.PREPARING
+    ) {
+      content = this._renderProgress("Preparing installation");
+      hideActions = true;
+    } else if (this._installState.state === FlashStateType.ERASING) {
+      content = this._renderProgress("Erasing");
+      hideActions = true;
+    } else if (this._installState.state === FlashStateType.WRITING) {
+      content = this._renderProgress(
+        html`
+          ${this._installState.details.percentage > 3
+            ? ""
+            : html`Installing<br />`}
+          <br />
+          This will take
+          ${this._installState.chipFamily === "ESP8266"
+            ? "a minute"
+            : "2 minutes"}.<br />
+          Keep this page visible to prevent slow down
+        `,
+        // Show as undeterminate under 3% or else we don't show any pixels
+        this._installState.details.percentage > 3
+          ? this._installState.details.percentage
+          : undefined
+      );
+      hideActions = true;
+    } else if (this._installState.state === FlashStateType.FINISHED) {
+      heading = undefined;
+      const supportsImprov = this._client !== null;
+      content = html`
+        ${messageTemplate(OK_ICON, "Installation complete!")}
+        <ewt-button
+          slot="primaryAction"
+          .label=${supportsImprov ? "Next" : "Close"}
+          dialogAction=${ifDefined(supportsImprov ? undefined : "close")}
+          @click=${!supportsImprov
+            ? undefined
+            : () => {
+                this._state = this._installErase ? "PROVISION" : "DASHBOARD";
+              }}
+        ></ewt-button>
+      `;
+    } else if (this._installState.state === FlashStateType.ERROR) {
+      content = html`
+        ${messageTemplate(ERROR_ICON, this._installState.message)}
+        <ewt-button
+          slot="primaryAction"
+          label="Back"
+          @click=${async () => {
+            this._initialize();
+            this._state = "DASHBOARD";
+            this._installState = undefined;
+          }}
+        ></ewt-button>
+      `;
+    }
+    return [heading, content!, hideActions, allowClosing];
+  }
+
+  _renderLogs(): [string | undefined, TemplateResult, boolean] {
+    let heading: string | undefined = `Logs`;
+    let content: TemplateResult;
+    let hideActions = false;
+
+    content = html`
+      <ewt-console .port=${this.port} .logger=${this.logger}></ewt-console>
+      <ewt-button
+        slot="primaryAction"
+        label="Back"
+        @click=${async () => {
+          await this.shadowRoot!.querySelector("ewt-console")!.disconnect();
+          this._state = "DASHBOARD";
+          this._initialize();
+        }}
+      ></ewt-button>
+      <ewt-button
+        slot="secondaryAction"
+        label="Reset Device"
+        @click=${async () => {
+          await this.shadowRoot!.querySelector("ewt-console")!.reset();
+        }}
+      ></ewt-button>
+    `;
+
+    return [heading, content!, hideActions];
+  }
+
+  public override willUpdate(changedProps: PropertyValues) {
+    if (!changedProps.has("_state")) {
+      return;
+    }
+    // Clear errors when changing between pages unless we change
+    // to the error page.
+    if (this._state !== "ERROR") {
+      this._error = undefined;
+    }
+    if (this._state !== "PROVISION") {
+      this._provisionForce = false;
+    }
+  }
+
+  protected override firstUpdated(changedProps: PropertyValues) {
+    super.firstUpdated(changedProps);
+    this._initialize();
+  }
+
+  protected override updated(changedProps: PropertyValues) {
+    super.updated(changedProps);
+
+    if (!changedProps.has("_state")) {
+      return;
+    }
+
+    this.setAttribute("state", this._state);
+
+    if (this._state === "PROVISION") {
+      const textfield = this.shadowRoot!.querySelector("ewt-textfield");
+      if (textfield) {
+        textfield.updateComplete.then(() => textfield.focus());
+      }
+    } else if (this._state === "INSTALL") {
+      this._installConfirmed = false;
+      this._installState = undefined;
+    }
+  }
+
+  private async _fetchManifest() {
+    if (this._manifest) {
+      return;
+    }
+
+    const manifestURL = new URL(
+      this.manifestPath,
+      location.toString()
+    ).toString();
+    this._manifest = await fetch(manifestURL).then(
+      (resp): Promise<Manifest> => resp.json()
+    );
+  }
+
+  private async _initialize() {
+    if (this.port.readable === null || this.port.writable === null) {
+      this._state = "ERROR";
+      this._error =
+        "Serial port is not readable/writable. Close any other application using it and try again.";
+    }
+
+    const manifestProm = this._fetchManifest();
+
+    const client = new ImprovSerial(this.port!, this.logger);
+    client.addEventListener("state-changed", () => {
+      this.requestUpdate();
+    });
+    client.addEventListener("error-changed", () => this.requestUpdate());
+    try {
+      this._info = await client.initialize();
+      this._client = client;
+      client.addEventListener("disconnect", this._handleDisconnect);
+    } catch (err: any) {
+      // Clear old value
+      this._info = undefined;
+      if (err instanceof PortNotReady) {
+        this._state = "ERROR";
+        this._error =
+          "Serial port is not ready. Close any other application using it and try again.";
+      } else {
+        this._client = null; // not supported
+        this.logger.error("Improv initialization failed.", err);
+        // initialize is also called at the end of an installation
+        // When it can't detect improv (ie because install failed)
+        // We shouldn't reset settings but instead show the error
+        if (this._state !== "INSTALL") {
+          this._startInstall(true);
+        }
+      }
+    }
+
+    try {
+      await manifestProm;
+    } catch (err: any) {
+      this._state = "ERROR";
+      this._error = "Failed to download manifest";
+    }
+  }
+
+  private _startInstall(erase: boolean) {
+    this._state = "INSTALL";
+    this._installErase = erase;
+    this._installConfirmed = false;
+  }
+
+  private async _confirmInstall() {
+    this._installConfirmed = true;
+    this._installState = undefined;
+    if (this._client) {
+      await this._closeClientWithoutEvents(this._client);
+    }
+    this._client = undefined;
+
+    flash(
+      (state) => {
+        this._installState = state;
+
+        if (state.state === FlashStateType.FINISHED) {
+          this._initialize().then(() => this.requestUpdate());
+        }
+      },
+      this.port,
+      this.logger,
+      this.manifestPath,
+      this._installErase
+    );
+  }
+
+  private async _doProvision() {
+    this._busy = true;
+    const ssid = (
+      this.shadowRoot!.querySelector("ewt-textfield[name=ssid]") as EwtTextfield
+    ).value;
+    const password = (
+      this.shadowRoot!.querySelector(
+        "ewt-textfield[name=password]"
+      ) as EwtTextfield
+    ).value;
+    try {
+      await this._client!.provision(ssid, password);
+    } catch (err: any) {
+      return;
+    } finally {
+      this._busy = false;
+      this._provisionForce = false;
+    }
+  }
+
+  private _handleDisconnect = () => {
+    this._state = "ERROR";
+    this._error = "Disconnected";
+  };
+
+  private async _handleClose() {
+    if (this._progressFeedback) {
+      this._progressFeedback.reject();
+    }
+    if (this._client) {
+      await this._closeClientWithoutEvents(this._client);
+    }
+    fireEvent(this, "closed" as any);
+    this.parentNode!.removeChild(this);
+  }
+
+  private get _isUpdate() {
+    return this._info?.firmware === this._manifest!.name;
+  }
+
+  private async _closeClientWithoutEvents(client: ImprovSerial) {
+    client.removeEventListener("disconnect", this._handleDisconnect);
+    await client.close();
+  }
+
+  static styles = css`
+    :host {
+      --mdc-dialog-max-width: 390px;
+      --mdc-theme-primary: var(--improv-primary-color, #03a9f4);
+      --mdc-theme-on-primary: var(--improv-on-primary-color, #fff);
+    }
+    ewt-icon-button {
+      position: absolute;
+      right: 4px;
+      top: 10px;
+    }
+    ewt-textfield {
+      display: block;
+      margin-top: 16px;
+    }
+    .center {
+      text-align: center;
+    }
+    .flash {
+      font-weight: bold;
+      margin-bottom: 1em;
+      background-color: var(--mdc-theme-primary);
+      padding: 8px 4px;
+      color: var(--mdc-theme-on-primary);
+      border-radius: 4px;
+      text-align: center;
+    }
+    .dashboard-buttons {
+      margin: 16px 0 -16px -8px;
+    }
+    .dashboard-buttons div {
+      display: block;
+      margin: 4px 0;
+    }
+    ewt-circular-progress {
+      margin-bottom: 16px;
+    }
+    a.has-button {
+      text-decoration: none;
+    }
+    .icon {
+      font-size: 50px;
+      line-height: 80px;
+      color: black;
+    }
+    .error {
+      color: #db4437;
+    }
+    button.link {
+      background: none;
+      color: inherit;
+      border: none;
+      padding: 0;
+      font: inherit;
+      text-align: left;
+      text-decoration: underline;
+      cursor: pointer;
+    }
+    :host([state="LOGS"]) ewt-dialog {
+      --mdc-dialog-max-width: 90vw;
+    }
+    ewt-console {
+      display: block;
+      width: calc(80vw - 48px);
+      height: 80vh;
+    }
+  `;
+}
+
+customElements.define("ewt-install-dialog", EwtInstallDialog);
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "ewt-install-dialog": EwtInstallDialog;
+  }
+}

+ 0 - 154
src/start-flash.ts

@@ -1,154 +0,0 @@
-import { flash } from "./flash";
-import "./flash-log";
-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";
-
-interface FlashData {
-  stateListenerAdded: boolean;
-  logEl: FlashLog | undefined;
-  progressEl: FlashProgress | undefined;
-  improvEl: HTMLElement | undefined;
-}
-
-const getData = (button: InstallButton): FlashData => {
-  if (!("_flashData" in button)) {
-    (button as any)._flashData = {
-      stateListenerAdded: false,
-      logEl: undefined,
-      progressEl: undefined,
-      improvEl: undefined,
-    } as FlashData;
-  }
-
-  return (button as any)._flashData as FlashData;
-};
-
-const addElement = <T extends HTMLElement>(
-  button: InstallButton,
-  element: T
-): T => {
-  button.renderRoot!.append(element);
-  return element;
-};
-
-export const startFlash = async (button: InstallButton) => {
-  if (button.hasAttribute("active")) {
-    return;
-  }
-
-  const manifest = button.manifest || button.getAttribute("manifest");
-  if (!manifest) {
-    alert("No manifest defined!");
-    return;
-  }
-
-  const data = getData(button);
-
-  let hasImprov = false;
-
-  if (!data.stateListenerAdded) {
-    data.stateListenerAdded = true;
-    button.addEventListener("state-changed", (ev) => {
-      const state = (button.state = ev.detail);
-      if (state.state === State.INITIALIZING) {
-        button.toggleAttribute("active", true);
-      } else if (state.state === State.MANIFEST && state.build?.improv) {
-        hasImprov = true;
-        // @ts-ignore
-        // preload improv button
-        import("https://www.improv-wifi.com/sdk-js/launch-button.js");
-      } else if (state.state === State.FINISHED) {
-        button.toggleAttribute("active", false);
-        if (hasImprov) {
-          startImprov(button);
-        }
-      } else if (state.state === State.ERROR) {
-        button.toggleAttribute("active", false);
-      }
-      data.progressEl?.processState(ev.detail);
-      data.logEl?.processState(ev.detail);
-    });
-  }
-
-  const logConsole = button.logConsole || button.hasAttribute("log-console");
-  const showLog = button.showLog || button.hasAttribute("show-log");
-  const showProgress =
-    !showLog &&
-    button.hideProgress !== true &&
-    !button.hasAttribute("hide-progress");
-
-  if (showLog && !data.logEl) {
-    data.logEl = addElement<FlashLog>(
-      button,
-      document.createElement("esp-web-flash-log")
-    );
-  } else if (!showLog && data.logEl) {
-    data.logEl.remove();
-    data.logEl = undefined;
-  }
-
-  if (showProgress && !data.progressEl) {
-    data.progressEl = addElement<FlashProgress>(
-      button,
-      document.createElement("esp-web-flash-progress")
-    );
-  } else if (!showProgress && data.progressEl) {
-    data.progressEl.remove();
-    data.progressEl = undefined;
-  }
-
-  data.logEl?.clear();
-  data.progressEl?.clear();
-  data.improvEl?.classList.toggle("hidden", true);
-
-  flash(
-    button,
-    logConsole
-      ? console
-      : {
-          log: () => {},
-          error: () => {},
-          debug: () => {},
-        },
-    manifest,
-    button.eraseFirst !== undefined
-      ? button.eraseFirst
-      : button.hasAttribute("erase-first")
-  );
-};
-
-const startImprov = async (button: InstallButton) => {
-  // @ts-ignore
-  await import("https://www.improv-wifi.com/sdk-js/launch-button.js");
-
-  const improvButtonConstructor = customElements.get(
-    "improv-wifi-launch-button"
-  );
-
-  if (
-    !improvButtonConstructor.isSupported ||
-    !improvButtonConstructor.isAllowed
-  ) {
-    return;
-  }
-
-  const data = getData(button);
-
-  if (!data.improvEl) {
-    data.improvEl = document.createElement("improv-wifi-launch-button");
-    data.improvEl.addEventListener("state-changed", (ev: any) => {
-      if (ev.detail.state === "PROVISIONED") {
-        data.improvEl!.classList.toggle("hidden", true);
-      }
-    });
-    const improvButton = document.createElement("button");
-    improvButton.slot = "activate";
-    improvButton.textContent = "CLICK HERE TO FINISH SETTING UP YOUR DEVICE";
-    data.improvEl.appendChild(improvButton);
-    addElement(button, data.improvEl);
-  }
-  data.improvEl.classList.toggle("hidden", false);
-};

+ 0 - 49
src/util.ts

@@ -1,49 +0,0 @@
-import {
-  CHIP_FAMILY_ESP32,
-  CHIP_FAMILY_ESP32S2,
-  CHIP_FAMILY_ESP8266,
-  CHIP_FAMILY_ESP32C3,
-  ESPLoader,
-} from "esp-web-flasher";
-import type { BaseFlashState } from "./const";
-
-export const getChipFamilyName = (
-  esploader: ESPLoader
-): NonNullable<BaseFlashState["chipFamily"]> => {
-  switch (esploader.chipFamily) {
-    case CHIP_FAMILY_ESP32:
-      return "ESP32";
-    case CHIP_FAMILY_ESP8266:
-      return "ESP8266";
-    case CHIP_FAMILY_ESP32S2:
-      return "ESP32-S2";
-    case CHIP_FAMILY_ESP32C3:
-      return "ESP32-C3";
-    default:
-      return "Unknown Chip";
-  }
-};
-
-export const sleep = (time: number) =>
-  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);
-};

+ 25 - 0
src/util/chip-family-name.ts

@@ -0,0 +1,25 @@
+import {
+  CHIP_FAMILY_ESP32,
+  CHIP_FAMILY_ESP32S2,
+  CHIP_FAMILY_ESP8266,
+  CHIP_FAMILY_ESP32C3,
+  ESPLoader,
+} from "esp-web-flasher";
+import type { BaseFlashState } from "../const";
+
+export const getChipFamilyName = (
+  esploader: ESPLoader
+): NonNullable<BaseFlashState["chipFamily"]> => {
+  switch (esploader.chipFamily) {
+    case CHIP_FAMILY_ESP32:
+      return "ESP32";
+    case CHIP_FAMILY_ESP8266:
+      return "ESP8266";
+    case CHIP_FAMILY_ESP32S2:
+      return "ESP32-S2";
+    case CHIP_FAMILY_ESP32C3:
+      return "ESP32-C3";
+    default:
+      return "Unknown Chip";
+  }
+};

+ 188 - 0
src/util/console-color.ts

@@ -0,0 +1,188 @@
+interface ConsoleState {
+  bold: boolean;
+  italic: boolean;
+  underline: boolean;
+  strikethrough: boolean;
+  foregroundColor: string | null;
+  backgroundColor: string | null;
+  // carriageReturn: boolean;
+  secret: boolean;
+}
+
+export class ColoredConsole {
+  public state: ConsoleState = {
+    bold: false,
+    italic: false,
+    underline: false,
+    strikethrough: false,
+    foregroundColor: null,
+    backgroundColor: null,
+    // carriageReturn: false,
+    secret: false,
+  };
+
+  constructor(public targetElement: HTMLElement) {}
+
+  addLine(line: string) {
+    const re = /(?:\033|\\033)(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
+    let i = 0;
+
+    // This doesn't work for some reason
+    // if (this.state.carriageReturn) {
+    //   if (line !== "\n") {
+    //     // don't remove if \r\n
+    //     this.targetElement.removeChild(this.targetElement.lastChild!);
+    //   }
+    //   this.state.carriageReturn = false;
+    // }
+
+    // if (line.includes("\r")) {
+    //   this.state.carriageReturn = true;
+    // }
+
+    const lineSpan = document.createElement("span");
+    lineSpan.classList.add("line");
+    this.targetElement.appendChild(lineSpan);
+
+    const addSpan = (content: string) => {
+      if (content === "") return;
+
+      const span = document.createElement("span");
+      if (this.state.bold) span.classList.add("log-bold");
+      if (this.state.italic) span.classList.add("log-italic");
+      if (this.state.underline) span.classList.add("log-underline");
+      if (this.state.strikethrough) span.classList.add("log-strikethrough");
+      if (this.state.secret) span.classList.add("log-secret");
+      if (this.state.foregroundColor !== null)
+        span.classList.add(`log-fg-${this.state.foregroundColor}`);
+      if (this.state.backgroundColor !== null)
+        span.classList.add(`log-bg-${this.state.backgroundColor}`);
+      span.appendChild(document.createTextNode(content));
+      lineSpan.appendChild(span);
+
+      if (this.state.secret) {
+        const redacted = document.createElement("span");
+        redacted.classList.add("log-secret-redacted");
+        redacted.appendChild(document.createTextNode("[redacted]"));
+        lineSpan.appendChild(redacted);
+      }
+    };
+
+    while (true) {
+      const match = re.exec(line);
+      if (match === null) break;
+
+      const j = match.index;
+      addSpan(line.substring(i, j));
+      i = j + match[0].length;
+
+      if (match[1] === undefined) continue;
+
+      for (const colorCode of match[1].split(";")) {
+        switch (parseInt(colorCode)) {
+          case 0:
+            // reset
+            this.state.bold = false;
+            this.state.italic = false;
+            this.state.underline = false;
+            this.state.strikethrough = false;
+            this.state.foregroundColor = null;
+            this.state.backgroundColor = null;
+            this.state.secret = false;
+            break;
+          case 1:
+            this.state.bold = true;
+            break;
+          case 3:
+            this.state.italic = true;
+            break;
+          case 4:
+            this.state.underline = true;
+            break;
+          case 5:
+            this.state.secret = true;
+            break;
+          case 6:
+            this.state.secret = false;
+            break;
+          case 9:
+            this.state.strikethrough = true;
+            break;
+          case 22:
+            this.state.bold = false;
+            break;
+          case 23:
+            this.state.italic = false;
+            break;
+          case 24:
+            this.state.underline = false;
+            break;
+          case 29:
+            this.state.strikethrough = false;
+            break;
+          case 30:
+            this.state.foregroundColor = "black";
+            break;
+          case 31:
+            this.state.foregroundColor = "red";
+            break;
+          case 32:
+            this.state.foregroundColor = "green";
+            break;
+          case 33:
+            this.state.foregroundColor = "yellow";
+            break;
+          case 34:
+            this.state.foregroundColor = "blue";
+            break;
+          case 35:
+            this.state.foregroundColor = "magenta";
+            break;
+          case 36:
+            this.state.foregroundColor = "cyan";
+            break;
+          case 37:
+            this.state.foregroundColor = "white";
+            break;
+          case 39:
+            this.state.foregroundColor = null;
+            break;
+          case 41:
+            this.state.backgroundColor = "red";
+            break;
+          case 42:
+            this.state.backgroundColor = "green";
+            break;
+          case 43:
+            this.state.backgroundColor = "yellow";
+            break;
+          case 44:
+            this.state.backgroundColor = "blue";
+            break;
+          case 45:
+            this.state.backgroundColor = "magenta";
+            break;
+          case 46:
+            this.state.backgroundColor = "cyan";
+            break;
+          case 47:
+            this.state.backgroundColor = "white";
+            break;
+          case 40:
+          case 49:
+            this.state.backgroundColor = null;
+            break;
+        }
+      }
+    }
+    addSpan(line.substring(i));
+
+    if (
+      this.targetElement.scrollTop + 56 >=
+      this.targetElement.scrollHeight - this.targetElement.offsetHeight
+    ) {
+      // at bottom
+      this.targetElement.scrollTop = this.targetElement.scrollHeight;
+    }
+  }
+}

+ 20 - 0
src/util/fire-event.ts

@@ -0,0 +1,20 @@
+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);
+};

+ 20 - 0
src/util/line-break-transformer.ts

@@ -0,0 +1,20 @@
+export class LineBreakTransformer implements Transformer<string, string> {
+  private chunks = "";
+
+  transform(
+    chunk: string,
+    controller: TransformStreamDefaultController<string>
+  ) {
+    // Append new chunks to existing chunks.
+    this.chunks += chunk;
+    // For each line breaks in chunks, send the parsed lines out.
+    const lines = this.chunks.split("\r\n");
+    this.chunks = lines.pop()!;
+    lines.forEach((line) => controller.enqueue(line + "\r\n"));
+  }
+
+  flush(controller: TransformStreamDefaultController<string>) {
+    // When the stream is closed, flush any remaining chunks out.
+    controller.enqueue(this.chunks);
+  }
+}

+ 2 - 0
src/util/sleep.ts

@@ -0,0 +1,2 @@
+export const sleep = (time: number) =>
+  new Promise((resolve) => setTimeout(resolve, time));

TEMPAT SAMPAH
static/firmware_build/esp8266.bin


TEMPAT SAMPAH
static/firmware_build/firmware.bin


+ 2 - 2
static/firmware_build/manifest.json

@@ -1,9 +1,9 @@
 {
 {
-  "name": "ESP Web Tools demo powered by ESPHome",
+  "name": "ESPHome",
+  "version": "2021.11.0-dev",
   "builds": [
   "builds": [
     {
     {
       "chipFamily": "ESP32",
       "chipFamily": "ESP32",
-      "improv": true,
       "parts": [
       "parts": [
         { "path": "bootloader.bin", "offset": 4096 },
         { "path": "bootloader.bin", "offset": 4096 },
         { "path": "partitions.bin", "offset": 32768 },
         { "path": "partitions.bin", "offset": 32768 },

TEMPAT SAMPAH
static/firmware_build/partitions.bin