Kaynağa Gözat

v4.0.0 - Next Gen (#87)

Paulus Schoutsen 3 yıl önce
ebeveyn
işleme
74187d9f44

+ 2 - 21
README.md

@@ -13,10 +13,10 @@ Manifest definition:
 ```json
 {
   "name": "ESPHome",
+  "version": "2021.10.3",
   "builds": [
     {
       "chipFamily": "ESP32",
-      "improv": true,
       "parts": [
         { "path": "bootloader.bin", "offset": 4096 },
         { "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
 
 ### 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-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)
 
@@ -115,4 +96,4 @@ details | An optional extra field that is different [per state](https://github.c
 
 ## 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;
         border-bottom: 1px solid #ccc;
       }
+      @media (prefers-color-scheme: dark) {
+        body {
+          background-color: #333;
+          color: #fff;
+        }
+        a {
+          color: #58a6ff;
+        }
+      }
     </style>
     <script module>
       import(
         // In development we import locally.
         window.location.hostname === "localhost"
           ? "/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>
   </head>
@@ -112,8 +121,8 @@
       </p>
       <p>
         To try it out and install
-        <a href="https://esphome.io">the ESPHome firmware</a>, connect an ESP to
-        your computer and hit the button:
+        <a href="https://esphome.io">ESPHome</a> on an ESP, connect it to your
+        computer and hit the button:
       </p>
       <esp-web-install-button
         log-console
@@ -209,7 +218,7 @@
       <pre>
 &lt;script
   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;esp-web-install-button
@@ -243,17 +252,17 @@
         ESP Web Tools manifest describe the firmware that you want to install.
         It allows specifying different builds for the different types of ESP
         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
         ESP device we detect via the serial port.
       </p>
       <pre>
 {
   "name": "ESPHome",
+  "version": "2021.11.0",
   "builds": [
     {
       "chipFamily": "ESP32",
-      "improv": true,
       "parts": [
         { "path": "bootloader.bin", "offset": 4096 },
         { "path": "partitions.bin", "offset": 32768 },
@@ -276,7 +285,22 @@
         where it should be installed. Part paths are resolved relative to the
         path of the manifest, but can also be URLs to other hosts.
       </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>
+        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
         <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
@@ -292,7 +316,7 @@
           allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
           allowfullscreen
         ></iframe>
-      </div>
+      </div> -->
 
       <h3 id="customize">Customizing the look and feel</h3>
       <p>

+ 733 - 9
package-lock.json

@@ -1,15 +1,23 @@
 {
   "name": "esp-web-tools",
-  "version": "3.6.0",
+  "version": "4.0.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
-      "version": "3.6.0",
+      "name": "esp-web-tools",
+      "version": "4.0.0",
       "license": "Apache-2.0",
       "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",
-        "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",
         "tslib": "^2.3.1"
       },
@@ -72,6 +80,69 @@
         "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": {
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/dom/-/dom-14.0.0-canary.261f2db59.0.tgz",
@@ -81,6 +152,19 @@
         "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": {
       "version": "14.0.0-canary.261f2db59.0",
       "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"
       }
     },
+    "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": {
       "version": "14.0.0-canary.261f2db59.0",
       "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"
       }
     },
+    "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": {
       "version": "0.25.3",
       "resolved": "https://registry.npmjs.org/@material/mwc-linear-progress/-/mwc-linear-progress-0.25.3.tgz",
@@ -127,6 +342,59 @@
         "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": {
       "version": "14.0.0-canary.261f2db59.0",
       "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"
       }
     },
+    "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": {
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-14.0.0-canary.261f2db59.0.tgz",
@@ -144,6 +426,38 @@
         "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": {
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/theme/-/theme-14.0.0-canary.261f2db59.0.tgz",
@@ -153,6 +467,35 @@
         "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": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz",
@@ -337,6 +680,11 @@
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
       "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": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
@@ -639,9 +987,9 @@
       }
     },
     "node_modules/esp-web-flasher": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/esp-web-flasher/-/esp-web-flasher-3.2.0.tgz",
-      "integrity": "sha512-jcJtWb5QuENWzeasfGYcJP/MV+XmRQelNRoOVCAKXcBJFh9h9NnfPXJtpoG+RsIMqb7hDdutomz/bBoBUH6urw==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/esp-web-flasher/-/esp-web-flasher-4.0.0.tgz",
+      "integrity": "sha512-7d23iEkEjvrYkywLZtvg69GAitRJVE73dN6nmyWNmTvCe55b0UTzndLJtTHANbAiNzpgmJ7/kYnt202A7BD75A==",
       "dependencies": {
         "pako": "^2.0.3",
         "tslib": "^2.2.0"
@@ -760,6 +1108,19 @@
         "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": {
       "version": "1.3.8",
       "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
@@ -1469,6 +1830,11 @@
         "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": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
@@ -1586,6 +1952,69 @@
         "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": {
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/dom/-/dom-14.0.0-canary.261f2db59.0.tgz",
@@ -1595,6 +2024,19 @@
         "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": {
       "version": "14.0.0-canary.261f2db59.0",
       "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"
       }
     },
+    "@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": {
       "version": "14.0.0-canary.261f2db59.0",
       "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"
       }
     },
+    "@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": {
       "version": "0.25.3",
       "resolved": "https://registry.npmjs.org/@material/mwc-linear-progress/-/mwc-linear-progress-0.25.3.tgz",
@@ -1641,6 +2214,59 @@
         "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": {
       "version": "14.0.0-canary.261f2db59.0",
       "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"
       }
     },
+    "@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": {
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-14.0.0-canary.261f2db59.0.tgz",
@@ -1658,6 +2298,38 @@
         "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": {
       "version": "14.0.0-canary.261f2db59.0",
       "resolved": "https://registry.npmjs.org/@material/theme/-/theme-14.0.0-canary.261f2db59.0.tgz",
@@ -1667,6 +2339,35 @@
         "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": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz",
@@ -1824,6 +2525,11 @@
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
       "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": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
@@ -2061,9 +2767,9 @@
       "dev": true
     },
     "esp-web-flasher": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/esp-web-flasher/-/esp-web-flasher-3.2.0.tgz",
-      "integrity": "sha512-jcJtWb5QuENWzeasfGYcJP/MV+XmRQelNRoOVCAKXcBJFh9h9NnfPXJtpoG+RsIMqb7hDdutomz/bBoBUH6urw==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/esp-web-flasher/-/esp-web-flasher-4.0.0.tgz",
+      "integrity": "sha512-7d23iEkEjvrYkywLZtvg69GAitRJVE73dN6nmyWNmTvCe55b0UTzndLJtTHANbAiNzpgmJ7/kYnt202A7BD75A==",
       "requires": {
         "pako": "^2.0.3",
         "tslib": "^2.2.0"
@@ -2163,6 +2869,19 @@
       "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
       "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": {
       "version": "1.3.8",
       "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
@@ -2747,6 +3466,11 @@
         "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": {
       "version": "3.1.0",
       "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",
-  "version": "3.6.0",
+  "version": "4.0.0",
   "description": "Web tools for ESP devices",
   "main": "dist/install-button.js",
   "repository": "https://github.com/esphome/web",
@@ -21,8 +21,15 @@
     "typescript": "^4.3.2"
   },
   "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",
-    "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",
     "tslib": "^2.3.1"
   }

+ 1 - 1
script/develop

@@ -11,7 +11,7 @@ trap "kill 0" EXIT
 # Run tsc once as rollup expects those files
 tsc || true
 
-npm exec -- serve &
+npm exec -- serve -p 5001 &
 npm exec -- tsc --watch &
 npm exec -- rollup -c --watch &
 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 {
   chipFamily: "ESP32" | "ESP8266" | "ESP32-S2" | "ESP32-C3";
-  improv: boolean;
   parts: {
     path: string;
     offset: number;
@@ -9,11 +14,12 @@ export interface Build {
 
 export interface Manifest {
   name: string;
+  version: string;
   builds: Build[];
 }
 
 export interface BaseFlashState {
-  state: State;
+  state: FlashStateType;
   message: string;
   manifest?: Manifest;
   build?: Build;
@@ -21,36 +27,36 @@ export interface BaseFlashState {
 }
 
 export interface InitializingState extends BaseFlashState {
-  state: State.INITIALIZING;
+  state: FlashStateType.INITIALIZING;
   details: { done: boolean };
 }
 
 export interface ManifestState extends BaseFlashState {
-  state: State.MANIFEST;
+  state: FlashStateType.MANIFEST;
   details: { done: boolean };
 }
 
 export interface PreparingState extends BaseFlashState {
-  state: State.PREPARING;
+  state: FlashStateType.PREPARING;
   details: { done: boolean };
 }
 
 export interface ErasingState extends BaseFlashState {
-  state: State.ERASING;
+  state: FlashStateType.ERASING;
   details: { done: boolean };
 }
 
 export interface WritingState extends BaseFlashState {
-  state: State.WRITING;
+  state: FlashStateType.WRITING;
   details: { bytesTotal: number; bytesWritten: number; percentage: number };
 }
 
 export interface FinishedState extends BaseFlashState {
-  state: State.FINISHED;
+  state: FlashStateType.FINISHED;
 }
 
 export interface ErrorState extends BaseFlashState {
-  state: State.ERROR;
+  state: FlashStateType.ERROR;
   details: { error: FlashError; details: string | Error };
 }
 
@@ -63,7 +69,7 @@ export type FlashState =
   | FinishedState
   | ErrorState;
 
-export const enum State {
+export const enum FlashStateType {
   INITIALIZING = "initializing",
   MANIFEST = "manifest",
   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 { Build, FlashError, FlashState, Manifest, State } from "./const";
-import { fireEvent, getChipFamilyName, sleep } from "./util";
+import { ESPLoader, Logger } from "esp-web-flasher";
+import {
+  Build,
+  FlashError,
+  FlashState,
+  Manifest,
+  FlashStateType,
+} from "./const";
+import { getChipFamilyName } from "./util/chip-family-name";
+import { sleep } from "./util/sleep";
 
 export const flash = async (
-  eventTarget: EventTarget,
+  onEvent: (state: FlashState) => void,
+  port: SerialPort,
   logger: Logger,
   manifestPath: string,
   eraseFirst: boolean
@@ -12,47 +20,39 @@ export const flash = async (
   let build: Build | undefined;
   let chipFamily: ReturnType<typeof getChipFamilyName>;
 
-  const fireStateEvent = (stateUpdate: FlashState) => {
-    fireEvent(eventTarget, "state-changed", {
+  const fireStateEvent = (stateUpdate: FlashState) =>
+    onEvent({
       ...stateUpdate,
       manifest,
       build,
       chipFamily,
     });
-  };
 
   const manifestURL = new URL(manifestPath, location.toString()).toString();
   const manifestProm = fetch(manifestURL).then(
     (resp): Promise<Manifest> => resp.json()
   );
 
-  let esploader: ESPLoader | undefined;
-
-  try {
-    esploader = await connect(logger);
-  } catch (err) {
-    // User pressed cancel on web serial
-    return;
-  }
+  const esploader = new ESPLoader(port, logger);
 
   // For debugging
   (window as any).esploader = esploader;
 
   fireStateEvent({
-    state: State.INITIALIZING,
+    state: FlashStateType.INITIALIZING,
     message: "Initializing...",
     details: { done: false },
   });
 
   try {
     await esploader.initialize();
-  } catch (err) {
+  } catch (err: any) {
     logger.error(err);
     if (esploader.connected) {
       fireStateEvent({
-        state: State.ERROR,
+        state: FlashStateType.ERROR,
         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 },
       });
       await esploader.disconnect();
@@ -63,22 +63,22 @@ export const flash = async (
   chipFamily = getChipFamilyName(esploader);
 
   fireStateEvent({
-    state: State.INITIALIZING,
+    state: FlashStateType.INITIALIZING,
     message: `Initialized. Found ${chipFamily}`,
     details: { done: true },
   });
   fireStateEvent({
-    state: State.MANIFEST,
+    state: FlashStateType.MANIFEST,
     message: "Fetching manifest...",
     details: { done: false },
   });
 
   try {
     manifest = await manifestProm;
-  } catch (err) {
+  } catch (err: any) {
     fireStateEvent({
-      state: State.ERROR,
-      message: `Unable to fetch manifest: ${err.message}`,
+      state: FlashStateType.ERROR,
+      message: `Unable to fetch manifest: ${err}`,
       details: { error: FlashError.FAILED_MANIFEST_FETCH, details: err },
     });
     await esploader.disconnect();
@@ -88,14 +88,14 @@ export const flash = async (
   build = manifest.builds.find((b) => b.chipFamily === chipFamily);
 
   fireStateEvent({
-    state: State.MANIFEST,
+    state: FlashStateType.MANIFEST,
     message: `Found manifest for ${manifest.name}`,
     details: { done: true },
   });
 
   if (!build) {
     fireStateEvent({
-      state: State.ERROR,
+      state: FlashStateType.ERROR,
       message: `Your ${chipFamily} board is not supported.`,
       details: { error: FlashError.NOT_SUPPORTED, details: chipFamily },
     });
@@ -104,7 +104,7 @@ export const flash = async (
   }
 
   fireStateEvent({
-    state: State.PREPARING,
+    state: FlashStateType.PREPARING,
     message: "Preparing installation...",
     details: { done: false },
   });
@@ -131,11 +131,14 @@ export const flash = async (
       const data = await prom;
       files.push(data);
       totalSize += data.byteLength;
-    } catch (err) {
+    } catch (err: any) {
       fireStateEvent({
-        state: State.ERROR,
-        message: err,
-        details: { error: FlashError.FAILED_FIRMWARE_DOWNLOAD, details: err },
+        state: FlashStateType.ERROR,
+        message: err.message,
+        details: {
+          error: FlashError.FAILED_FIRMWARE_DOWNLOAD,
+          details: err.message,
+        },
       });
       await esploader.disconnect();
       return;
@@ -143,20 +146,20 @@ export const flash = async (
   }
 
   fireStateEvent({
-    state: State.PREPARING,
+    state: FlashStateType.PREPARING,
     message: "Installation prepared",
     details: { done: true },
   });
 
   if (eraseFirst) {
     fireStateEvent({
-      state: State.ERASING,
+      state: FlashStateType.ERASING,
       message: "Erasing device...",
       details: { done: false },
     });
     await espStub.eraseFlash();
     fireStateEvent({
-      state: State.ERASING,
+      state: FlashStateType.ERASING,
       message: "Device erased",
       details: { done: true },
     });
@@ -165,7 +168,7 @@ export const flash = async (
   let lastPct = 0;
 
   fireStateEvent({
-    state: State.WRITING,
+    state: FlashStateType.WRITING,
     message: `Writing progress: ${lastPct}%`,
     details: {
       bytesTotal: totalSize,
@@ -190,7 +193,7 @@ export const flash = async (
           }
           lastPct = newPct;
           fireStateEvent({
-            state: State.WRITING,
+            state: FlashStateType.WRITING,
             message: `Writing progress: ${newPct}%`,
             details: {
               bytesTotal: totalSize,
@@ -202,10 +205,10 @@ export const flash = async (
         part.offset,
         true
       );
-    } catch (err) {
+    } catch (err: any) {
       fireStateEvent({
-        state: State.ERROR,
-        message: err,
+        state: FlashStateType.ERROR,
+        message: err.message,
         details: { error: FlashError.WRITE_FAILED, details: err },
       });
       await esploader.disconnect();
@@ -215,7 +218,7 @@ export const flash = async (
   }
 
   fireStateEvent({
-    state: State.WRITING,
+    state: FlashStateType.WRITING,
     message: "Writing complete",
     details: {
       bytesTotal: totalSize,
@@ -225,11 +228,13 @@ export const flash = async (
   });
 
   await sleep(100);
-  await esploader.hardReset();
+  console.log("DISCONNECT");
   await esploader.disconnect();
+  console.log("HARD RESET");
+  await esploader.hardReset();
 
   fireStateEvent({
-    state: State.FINISHED,
+    state: FlashStateType.FINISHED,
     message: "All done!",
   });
 };

+ 3 - 3
src/install-button.ts

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

BIN
static/firmware_build/esp8266.bin


BIN
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": [
     {
       "chipFamily": "ESP32",
-      "improv": true,
       "parts": [
         { "path": "bootloader.bin", "offset": 4096 },
         { "path": "partitions.bin", "offset": 32768 },

BIN
static/firmware_build/partitions.bin