From 74187d9f44fb080e93c0fdc4258b2fe32ac80e07 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen
To try it out and install - the ESPHome firmware, connect an ESP to - your computer and hit the button: + ESPHome on an ESP, connect it to your + computer and hit the button:
ESP8266
,
- ESP32
, ESP32-C3
and ESP32-S2
. The
+ ESP32
, ESP32C3
and ESP32S2
. The
correct build will be automatically selected based on the type of the
ESP device we detect via the serial port.
{ "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. +Wi-Fi provisioning
+ ESP Web Tools has support for the + Improv Wi-Fi serial standard. This is an open standard to allow configuring Wi-Fi via the serial + port. +
++ 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. +
+TODO EXAMPLE VIDEO
+Customizing the look and feel
diff --git a/package-lock.json b/package-lock.json index 0a45a63..847365e 100644 --- a/package-lock.json +++ b/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", diff --git a/package.json b/package.json index e8bda8a..35b3eb8 100644 --- a/package.json +++ b/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" } diff --git a/script/develop b/script/develop index 665528c..2cba10b 100755 --- a/script/develop +++ b/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 diff --git a/src/components/ewt-button.ts b/src/components/ewt-button.ts new file mode 100644 index 0000000..f2600b0 --- /dev/null +++ b/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); diff --git a/src/components/ewt-circular-progress.ts b/src/components/ewt-circular-progress.ts new file mode 100644 index 0000000..13c7d7b --- /dev/null +++ b/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); diff --git a/src/components/ewt-console.ts b/src/components/ewt-console.ts new file mode 100644 index 0000000..669c660 --- /dev/null +++ b/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
; + + public connectedCallback() { + if (this._console) { + return; + } + const shadowRoot = this.attachShadow({ mode: "open" }); + + shadowRoot.innerHTML = ` + + + + `; + + 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; + } +} diff --git a/src/components/ewt-dialog.ts b/src/components/ewt-dialog.ts new file mode 100644 index 0000000..74c3174 --- /dev/null +++ b/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); diff --git a/src/components/ewt-icon-button.ts b/src/components/ewt-icon-button.ts new file mode 100644 index 0000000..b50be76 --- /dev/null +++ b/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); diff --git a/src/components/ewt-textfield.ts b/src/components/ewt-textfield.ts new file mode 100644 index 0000000..dae1d73 --- /dev/null +++ b/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); diff --git a/src/connect.ts b/src/connect.ts new file mode 100644 index 0000000..b61cbfe --- /dev/null +++ b/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); +}; diff --git a/src/const.ts b/src/const.ts index 0ec91fe..7744e14 100644 --- a/src/const.ts +++ b/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", diff --git a/src/flash-log.ts b/src/flash-log.ts deleted file mode 100644 index 6e5985a..0000000 --- a/src/flash-log.ts +++ /dev/null @@ -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` - ${row.message} -` - )}`; - } - - 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`` - ); - } - } - - /** - * 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; - } -} diff --git a/src/flash-progress.ts b/src/flash-progress.ts deleted file mode 100644 index 4a151b5..0000000 --- a/src/flash-progress.ts +++ /dev/null @@ -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`- ${this._state.message} -
-- ${this._state.manifest - ? html`${this._state.manifest.name}: ${this._state.chipFamily}` - : html` `} -
-`; - } - - 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; - } -} diff --git a/src/flash.ts b/src/flash.ts index 9d12fab..720d4f0 100644 --- a/src/flash.ts +++ b/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 ; - 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 => 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!", }); }; diff --git a/src/install-button.ts b/src/install-button.ts index ef7ca21..1abc54c 100644 --- a/src/install-button.ts +++ b/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"; diff --git a/src/install-dialog.ts b/src/install-dialog.ts new file mode 100644 index 0000000..f29eccb --- /dev/null +++ b/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` + ++`; + +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` +${icon}+ ${label} ++ ${heading && allowClosing + ? html` + + `; + } + + _renderProgress(label: string | TemplateResult, progress?: number) { + return html` ++ + + ` + : ""} + ${content!} +++ `; + } + _renderMessage(icon: string, label: string, showClose: boolean) { + return html` + ${messageTemplate(icon, label)} + ${showClose && + html` +++ ${label} ++ ${progress !== undefined + ? html` ${progress}%` + : ""} ++ `} + `; + } + + _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` + + ${this._info!.firmware} ${this._info!.version} ++ + `; + + 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` + { + this._state = "DASHBOARD"; + }} + > ++ + { + this._state = "DASHBOARD"; + this._installState = undefined; + }} + > + ` + : html` +{ + this._state = "DASHBOARD"; + }} + > + ` + } + `; + } 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` ++ Enter the credentials of the Wi-Fi network that you want your device + to connect to. ++ ${error ? html`${error}
` : ""} ++ + + { + this._installState = undefined; + this._state = "DASHBOARD"; + }} + > + `; + } + 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} ${this._info!.version}.
` + : ""} + Do you want to ${action} + ${this._manifest!.name} ${this._manifest!.version}? + ${this._installErase + ? "All existing data will be erased from your device." + : ""} ++ ${this._client + ? html` + { + this._state = "DASHBOARD"; + }} + > + ` + : html` +{ + // In case it was null + this._client = undefined; + this._state = "LOGS"; + }} + > + `} + `; + 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
`} +
+ This will take + ${this._installState.chipFamily === "ESP8266" + ? "a minute" + : "2 minutes"}.
+ 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!")} +{ + this._state = this._installErase ? "PROVISION" : "DASHBOARD"; + }} + > + `; + } else if (this._installState.state === FlashStateType.ERROR) { + content = html` + ${messageTemplate(ERROR_ICON, this._installState.message)} +{ + this._initialize(); + this._state = "DASHBOARD"; + this._installState = undefined; + }} + > + `; + } + return [heading, content!, hideActions, allowClosing]; + } + + _renderLogs(): [string | undefined, TemplateResult, boolean] { + let heading: string | undefined = `Logs`; + let content: TemplateResult; + let hideActions = false; + + content = html` ++ { + await this.shadowRoot!.querySelector("ewt-console")!.disconnect(); + this._state = "DASHBOARD"; + this._initialize(); + }} + > +{ + await this.shadowRoot!.querySelector("ewt-console")!.reset(); + }} + > + `; + + 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=> 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; + } +} diff --git a/src/start-flash.ts b/src/start-flash.ts deleted file mode 100644 index 309a361..0000000 --- a/src/start-flash.ts +++ /dev/null @@ -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 = ( - 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 ( - 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 ( - 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); -}; diff --git a/src/util.ts b/src/util.ts deleted file mode 100644 index c6cdebd..0000000 --- a/src/util.ts +++ /dev/null @@ -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 => { - 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 = ( - 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); -}; diff --git a/src/util/chip-family-name.ts b/src/util/chip-family-name.ts new file mode 100644 index 0000000..53131b0 --- /dev/null +++ b/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 => { + 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"; + } +}; diff --git a/src/util/console-color.ts b/src/util/console-color.ts new file mode 100644 index 0000000..5f02443 --- /dev/null +++ b/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; + } + } +} diff --git a/src/util/fire-event.ts b/src/util/fire-event.ts new file mode 100644 index 0000000..184a38b --- /dev/null +++ b/src/util/fire-event.ts @@ -0,0 +1,20 @@ +export const fireEvent = ( + 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); +}; diff --git a/src/util/line-break-transformer.ts b/src/util/line-break-transformer.ts new file mode 100644 index 0000000..71a9323 --- /dev/null +++ b/src/util/line-break-transformer.ts @@ -0,0 +1,20 @@ +export class LineBreakTransformer implements Transformer { + private chunks = ""; + + transform( + chunk: string, + controller: TransformStreamDefaultController + ) { + // 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 ) { + // When the stream is closed, flush any remaining chunks out. + controller.enqueue(this.chunks); + } +} diff --git a/src/util/sleep.ts b/src/util/sleep.ts new file mode 100644 index 0000000..0d01adb --- /dev/null +++ b/src/util/sleep.ts @@ -0,0 +1,2 @@ +export const sleep = (time: number) => + new Promise((resolve) => setTimeout(resolve, time)); diff --git a/static/firmware_build/esp8266.bin b/static/firmware_build/esp8266.bin index 5ee57f32aa73ad497f3ad69273ccefcbe369332d..7040b4530b49a7d82fb1162150d9705625687695 100644 GIT binary patch delta 136805 zcma%k3tSY}+4s!s&H_3M>~c|I(VgKoYglC$8W#h}?#dzrNL}IuEw(O62 H=3JlioagdCNB`l;{-cxk--vKqlT_5*kG-DM!z%ReP?6TiNWJxH9ZT(dd5(B= zkhhv7DT`6}QND5UzTvU-+{?rF >6V z`eH2IQ;_QY+45NTfMnzXb5lc2Bi~XKg>Qw%e0=NpcS7*n8l=kUVJ0rhY}X5onVzEw zp{8K5*|o#>=+`A%edrgyZFAA-AL+}DA*P#>^QLr+xBGUsFkDHP$?+z~X@%k$p(g&C zbXsHJjGTM4+Lgab9_yeWP+=yEVC4J?s&bvFTXNozxDxxGT*m=!TuZJ0mv25B{U)FN z>-cX@;~PIhJ;ERz>9%&$k*L_;exi*DJ$AOIVc GW%Da3i>-T|QQ`PI)xre+tQQ&6yOM#n?@8eV zXP2}hyf$UzQ&L>>L8IDDe?f{<2^e3osx;ifteIN*Kr!2wes$g=zh6c5GQyso)R&mr zGgAGq$c+q&u;=RKJyPmRGdz3r7Dn(@rg{_e6ck0bkkSnMxP)3Y#be@HOhkg_-~D&3rJE>hMDrgf6@s#Liiv$Xqy BPGsZQ(BgVus{Ls5mGoUiMK?^$+j_#O)N)nGN;hcwKGJ2)4;iWa8-u G7t z&822vN;O@Pga(fNeX8YAQITK0l_$f_=bOxjuiB4X6_urm{OIY)CoV{-Z?NV>I`vT4 zaek{Q;7L_6)K;ET9JhIXk!oSGG^VpsJ5$Izo+H2Lb|!~5Mg;zqYNQ=msfBKy%}U+M zdn~EPd50x6|IwTX6usL>7dRK&0?()BJ4 7I<#(97*pF*YZTp6zb9aX{N(#fqmWtHfI?(fcPc z3cqk8Lc*V(9lmGVSqjt8nRcYz*`eSRVXKb0S{V8!nQH!uWZ?J%k|E8IZ74F78LA9* zhI&w= ?~qQo|4qiRcNjAs7u?Rms?5=$t(RfHv+FPa6+>#ken)3)oC4Ug5aZs zDwU9?2y}uojiUi%4Sp85ML3U?GWM3Dd?>f5lXSM#-RPRo!tmcnJsZxhMISk)efz+w z4y8dS6oroVRVdP JORFu-F2wZvL@|$#&`?S2UTEh+;+tt3kh3VNqw-3t;SLQ2;U=AJ3 zzOLKL-Kg$f$3BiV>v(|hls^nR`@mMx_Hx3C@bz5ar(9Dw=!ddYwKeO=nh+Wyu_(+9 zkw+M=E}KCZ-;(%2A74_ip@hxR<9IIlT6VQ}`JOOAS46bl2(a-5azG>FU1yXdGda zzr%A4LSF$xW#hT*JVhPFDt&v!XKxV}s%`K!x?)=xfeB??liFM{U9RYUSJXnSK5&dZ zbMSWSO{<5U7i3ms_hqs3In^3BMVYx7AeE26MK__h*cOUH^QtMcK*?P7fArq*5PI*q zpY(p&u(KV#FM2T8d-lhi>OsxXpOMv%GtTYsR@j#kn$9bGT4Y(J4Jz=hL 49RIKty(CLR~q^jTf+dga(oQg(TY8 zJyKpM2HaUA2|lLtZoS<%uX0`BPA>0;B XGRhpYa9kWseBfGGAu=ZOi^zzZOr%&8wq~858lj9bq8rfNI(niG+VbMW)UH|Bo zv-U-ATb$@tMN_t`U_V@xXF%fA`Y!z`t zN# -NRu8U&R&r)uY}mm4YZ?2 zV)&3?M}yrg`j@;nq4@MK^_CsST h%Q5E?9{@F%1gY6T*L`55xlMuZupQ=v zd)#=c>C1%O_A<@=$Viu#>rRr)ELA&!!scoA#j#f<&=+>EE7JMiTC7YpRcI1yN|BE5 z@N`P&cJ?;)NuwhlY>+Iyf*~mamfMo=sO<49cAF8|BauR)-mFJP>{Y2UbKWub8sx29 z<&BNr ll@=TO@&v`_e(h<3UNG*w6B(F%M7ZDYSTq1d9BJwTC zLn3`74~bkRc}S!m5qHxJ#IBGe61hq;84 bVyP`T6W`^mxSps9hCBjlx#X6 z<>|?DzmzwXJYSdcbmaM0DKCyZUz74CljlAuFP1$2BIQL>rdOpVE%}?LA!&b>@*>Fd z6)8_mo_~_^CX(kLr97}C%Ji}XZj0w$>5)8^M8NVOUQ?#sQXY*bW%5Zzg}M)l3djf~ z;%LRf2&!2~IL1SCSw9qJ5~?U{I-VgSN3lcF@wC98v^CA+P7!r=CD{9-$lk{obkK;V z4SJtxo$S5|w&Ym+J*h}8FwCSs>|enwRn|2_>{JzVv}?#uQA?Rn)4!8P n>R)Z0W7dyM$ z{_`E}7gd)fj;Rf$>}l$fwD3jImxVOi!c<<#QCb~ArNq-B#qPe@bWIlSPHVd9k!3}O z!!AUInLtJrN$S$J{4P#0H%96CPRc=3jU$>UMZGr09P>F*OSndlZNTKVw)toM<7cfO z`D4{8OPW5ze<~xUSj{Qu3xnvcsQa|7T_?{WW&2jjQDd-nNJEV09SK4lcz5T0dbU^k zw5^!o6xL!t-64O)Io_0dxSN;Sbv EZr#Q80Fd825(!u-W_=1|H=^q z&yVAXY~2qz;{P&T-;$R6aJovdd`CA&rl%q7a)(0L Gwcm|$DbsF zEz4QpTD(fHU%l_3m|eQTQWU=cgIiFyM3|T1M<(`V2rpTh0)DZ{ja;$ikjV =Mum;Ksr3@HxJ z313Vv=htc+|CB^sCBH`VNcnPI>s)^+UH67{hCfG5FCm0g3f(n_1v6XuWQ{R2hb<1@ z68!9xv_@0O=j&Eh_#es53Ab0#*vUQbqOdiZ&Gav;*Xr J`cl&lS!UK>7dKDeYy7?TllmQpzn7kaS)4bmj6)((Qa7wshIU`<7^=imteK;4f zu{kr Ck!%NSbBKC*yQt<`Ly zJ>BT*W;$Nl`oyW3XY$<8Y}05cb=bAb6C)REa>5~T7c&L4!!700rJCyI%rk9}WXosJ zCD_|gSjcE$>{loX1zkEdvor4_sX}?1d)9_iWZFIe)4}((VN~hVMVaipTz?abf=DUW zyc%Q3l7*Zbjq-efayVtMq>oh%(kN}*SS71kgkkR=xZE#9HoCNJE=`v!l5s_Ku)R$B zysM31p)>D?DWxGYhc4K@c*8N v8{}V$>0=tl0Z8Z; zXa07Bn}#53z=}48=g`Fw?IsX4_5`SZ+rhG;j9eS9Y2>wCeB_PG+J<>VvA+?tGRoW6 zkZfZVnw1Vs3jgTMappzrevjOgt(vCF;IrnobrSnt@WbP2?tnT4d!0I^FwLP(;g^mt z!n9 ?jz)ax@!QaUa@;5r5!rw@b zDg|Z^RmxU(T7XUAt>ddj%d `vzS?`Jw4G@E?CFwd=Ie$w~znWAo@yrBKo^GWikU_qJGW9jV26(tcaQeq z-{W3%jQil&xb#zF>7Mt;rJoo}XPd^QA0JEi92u7$7)w{Mhm&FAjtOPkh)mUx?5(*p zCFCO#|5vg%^5H2Vr%3!CBtAKm?I1t?9(*Ck-<4lv2jm}bCyy8DF?ht5$-afuQNgDe zqD`j-w#TJ(&_L&zpW>|-(?i)MDcC7KO7@<_56?pUU|W;DoFOwVP=X(h=42z+B#m$e znJe^7_bxG{OpUxlZ%sfjBHWEojIagarwEVU;eE;QXegUU%0_phr{{$_Z1@rQTe7!& z`jSxgF7oD$WbcvbnQq4{@@D`4s7R(-A}fZOI+&wOw-!na;kxW!lN+0r=jyMBYFP80 zmis +Zfc4dBIfsmh3V9uXYV7G1dZn@)&M913X4yII>&tbP*Ch)C- zuddn>k;U-$4j3XJ6suRw^=?hOL%TgWUsFmK<}P~fg2 z*uCELJ*= z10@eog}DXVT8i@YLx2s=pm%~Xan=C{>XFcUJrOBQO6-QR8rl%a?ninqRAqQeXwOs0 zoHh?a21A&+JYn9!89MsUUd=D`cXb*y !LDH8L|v&WXou;vYwjJcn^y7| zRab5dr`HbnWlKW;he`|*TbAryJu{Q`j4!3^|6R(0@0W6avR9cd ADCq9)WS3KWGIS^Z5kB{W*lnX3|~Rms{6x`O>kwpw&bl6x9O)x7_r_bt v_P%Ns<88^72y9 JBd^DO {>B@mS;0S=btdu+|)S%_evLRNqb)XdEAS|DKt_uy1(#GIj0+3l_xu<<(bT zMVJohG+zt(lo6SMRr*{%e^avBd429iBC7Um7*g<8rMl<(GBv%-+>9tfg;BkVSqGK9 z3}%v3pNg8$Ib&O=pz*~1aIMrcre6 veZ2HNrmu~c=jcOPo^a2MN(&r22VV5-OMZPJS(^uBi%!{YuLNI|C*8X)APi) z5&=3Mm8(XiDoRhw9BO2%4YD``cRIVf2>Kt9fRp0eTDWsNuZY?3N!?5$a!x`rzw aN@Q7=sP{ znMRA*mZDi&O`RF5wA@=`n&fsYA&V8H#PIQre4MsnCFHf0|GATC-PyrTIwqi#^Uz5l zjX|rvNaEAKBm6hmyCB;G8xLBz^o@F!xMVzM#&va6d+}<2bk}6oJuFTw<^L@hrnWl# zb 5}5_py-)urzS6T6dDifoO~0%b>9te!`Tp3Gtc8ZDZ!YOA46l^! zgan4*&(^c?!v+l(ab2rqKTaBAgc%eT32dy6$A_0Pb^9zdj2yGcS4$c54BGFgF{(X3 zAI8!Mcf@$rcivgT8iui1<)}U4Fqxgxn$f)&s|e~A >=#pd6-2H*Ga%{5Mr((}a zc238!koA2b0obpRSI=#NjzMBcoR|GV%HM~=_i(J^l-!&VrVcH``++6IyJeO-=3^8R z8f 7B$$X}aTv_uqHU_P&yp;O@dufzOh$@*I=$vq?MyErhe6 zAyV!w+boG?eRj{25iJQyp3q@A;vUlF>DOS&6 z2@=k!`d*cqy3u_4y>-KRmn1t+!=Oh+TdCq*`rJrkxYg)|A?X2p%*8BsLPf^v{BvBT zv$oya@Y|><#SFTeA03=fjtyG{vxvNrdS9N+rw9HfcU&)b{0o@)ZxkKPhA9&Nu$Srf zc+L!2Hi(MuDE%crK@fPqo1GZzxpP?DkPl;9spfnJr()|Rkq-F1Gw;fo`iXQS`)FEi zl>Bv~ %}_>&Wvup5a!#^$r9Z!XgCn ze#OX>#1Y(b;>eHN@lD^)bUS@C--{#fD{Xq~`0(yXdMln^Aq >vR|;l}bOQ_UEVAV9CUPZjyE>AN3hGGmK_gfNDp38H;@`e@ z`z5=TyliZ`F7Y?7+)njuA*pS7{SyDoXay*LGfC}gx+w9R$5I2eB(>k2cTVD;9830i zkPJg@?OBO;k0c8|%AgT!U=E&5@>`ASQ;Fi&jD5v!l+(4UlM=sLDu%xfYDJBl7Nh!5 z;y=4W(qKu`V4&P`daUXg%2+;H2N}`z@{1_dVToTlI*iX^7!AC^gz|oc*EzIZh#nov z1;g-9@xK}h_Menaa+0l*T|1I@(nwxU*@!F 9!vmecHJ?`7+xg|OzqES$bR*{VYrgy&uEM+4wgx1=1d-D&sLb}*bB{AODi26}>` zlCDme0Q^l3XZJnD**(8OGJAKD^D Bc)N&?k6j*D z*nFz~0=Jv~ d-rOEMd&EXKe=!W0=c3gQ9BjP14%AcV#=$Pn!Blg6XP zph&uZM+?@MC&5_lRZ@{awb*U};8q~mZUQ<{z}N<)EVI32f(=k2dqH_tb=JnL9a+0= zZ(Yf1%xcT(%IXJ(!t5^Bm@Z2N2MutyU>_S%I%R|yWT}8Vjx^^s^bH&;{nyFXXaor! zo^A}_I|#o+SdEa5MsRcV){6)-= y)C#6 Oa z-^i?Rf?(4Kx(SYkG{K2abxQ0J8D^jySgKxNS0$`qa@3rV{h%`t^j%S;`254%9F$() z&pwvm^uwAf=Q&xa3Oo{qg-{Lg2_7ioAu|0 3VJO+0W7 zJnXy=LH1^=;^+(gXpdoC(5fCJ@xd8W-29gVb heq$15>n`W1ASNZcZh$m{|^ewNRM5qpz>u z-`8)0rco$axO|8}O@K}$QnCFYVc2sQYXU!ubC+tcZ}QOX&4-n9y`3Yn_PNCWP4%b* zJ!B&UVJv{J0K^hRu(U9`7E4k4E>TfTR|+M7STrR0Rg`6W>_@5~yhevX1qP>bl}RPp zedSz zF)R)sFSKD?Y+tZ2F{rGM-1?Bey8d>!GO zg5W5OR4ni|F3wRDXKBb%6M^Y`Pknd&>AEJGye;ft`v%YLyb2S}ZUy}ane+TQOZ}|p zQY`CW+Xb9*Q!(KkVEOj%)Y*$}>4Pj32rj_&NlOXG$2#0Er;#i+jPp4sb%WeH+9L z;9 X0SgR8g+FMPghB(sJ3ia zW6tMGO-41=spy*i7+lGHGiv&>;ToIFnuom0@_5?r-IzChBD*!Nm?2-3mU!RJ;}zYs z;QcHwIhJAss|{#ha{ gmN;hX zy)u^W`OCQU-m!G{)p6 `I z;{@i|IZR3ud`i3T1>)U~4MW4cD{Y3kFDH)Q4LmO;et$Oz-!gp9f1#XTA#6i<8R2gT zA0o6PVDj6YZHkEkEu)c17$$Gi&-5TBoBbcHu^)sh23A{}sEiO<}r#DEnWrgM#V2 z1UBV(B=%x8kPdjiJoZc*@QZl8Ja&jVqcwenxTDg0BVX?bZV-)vA`IYDwiP%l$96RF zwu2mCSuFSwX9Dq5$R2t
dI z?Q`tL$k34p{Hx2f_Hq`=WPOPi7d~(+P!QJM2NNNZeH8<{n*(z_9wwOYNw!8JJo#j@ zbsa(x!sqB+)uv>t9$|1JV1x*7AZ$ZejZld&4 r06T Q3IPAm%~^TXuwy|K!$^sKO?oI#Gzepk)NMNo8Bbt`!u?(C0gK> zz#ulAl-$6)kPe-W2Iz%!2$9!G_EXXD?L`D$ojB^@9s?6cg2rMH=3{j85z-N+;CGP8 zg3?Ua$ymdx$bJ=rG3GpaAP+y813)|WZU`m~#P8_2kqAM@9XK3AmL}~uD`#b={vc~K z=;pgbyl;p(abno{B?KzYT$JQF9$IO0o(=xW^!z)t(&YR+_$xPXEcAp$I&KryST3G> zSW(UXSxF{`W #N4_s=PK>G*3*Ub5_H@Eni9DhJ6EO4%Y)yk}pG z>EDvO>%%It2%=5DN5O1tENPc1KoCu+Y&&UIAUZboAOVG#8l bLCLxLMG!OhP69@lO~Bl{4tK_hFz1nXBtdTVCki0ps5pUC`TaoA-7ORdw^PNf7H zLoE2_2htM_Hgm!R?2a?TiR7=dckxQk%*47zE-Fi#C761o9y4}Ti__r55`j3wzB7e$ zGq5l^35-Ns0c~M@!HTT)22COFc?(k=&Av57ToJH@-`LsxOddm-XHepb!b^dS@ar%2 zi^cJRp;+Lo&AY|H9L{rDAr3ljC?GfszRY%8yEBlA7M;>Eo=?LCUo;xi1~*emBGiUp zdz#RmEH^{iW2Cca$Lmu>n>|fqaSBx$Ax+Dv%~e{`lmfcl*>3Z^CN~Iw(=@Q %zUCWB7T z)EjJslq=nF5O)D5!|oHwp2Fu2cZ@jLUEX7}RA)I)MX@44ABHH--p&W&f=Wg`Q8G;Y ze-j-eJOINh9oS0R=GZeO^fJMPTZJOq4~|NJ6pryn&18=cd=gF$9;}HWD{g`t`+dYX z3P47o@g$L8%0}>cHDqlNS_-BID!-RL83ZkhsAER)z*dxpeVo^lqgD78o+ dqjl0LBE~YYpMM+t;qbZB&2md8fn$LBCVAO-H3y; =eeFK9BED0ifSh*qt{O*OFU zt_*Uv0mk kSkMMciUz?G#GXmR*T2zgfXWryin h!6fZ1B zZ8!S|5?FFK4d7b2Uq1s?|{cFYAFHG5HjodA$cLmH?6P>)?y;A`Q;ZzY2o zWAi*28xs_r?GgrUdZIqfW>H%O4Sk8cK!$DP$@joa9OQ7?px4#3K@#Rr>b@W h5E-eCEQlH=Ej#muRLbg_DRZm%LWGXh%qA+}cm8HIxLB^nMx4Z+}lE(|fmTTOh` z?x5?otr$435*I};Bg0inFZr2KJ?}@bOHlR);6s+6XfHHNA~&zY2R?aH=)2?_;0VTY zR8Aam@|80AT2S`9Tz{Ae-ppBVgaMrBxhz5NrRE+|&dcy+X-D1g@2;Ko66MxX%ksaV z2sxD;Q4q=aOZ_~vt<)YI7Z|^Q1zHrv cm)i6d > z*FfO$5mM*#nvD!q3_)+Vk+4qFGX@s3qmy%7 N1Q9hhHEX?& IjL4b?MS9d0?Lswtw?|3`CdXv0uMS#7knb~@Q$O# z2zsl`Jf?~?ysifXC`jSoJG>!t<}i~R;E~sbQIe1ozYo=C-?;sDU=&C^3Lz#dX4a0> z+7RmE3cw)D90k9fIhV2@p- Ws k7-0(kYVw5~h98#nNs-Ov`HJS%WMi<34*?J!Ma-1gU-kz9eL(X8 z(>HJ#(6I1JP!|cYmK+)CP;`-_ql;o;jKMJsa}h-tN-a-K%)QrG&bR?CS;~M+sp;#h zL#TM+mkVI2m}~9>Lwz`A0?zT+D4~dgGpS-~e!rRs>*fy?vvZ)57D_i4++|$6A#e$( zKzl> gytO`UxeNTYVt; |S&Ud|?H_b*qBA;~3Tzi*{fYQ7;JkQ--?MnyJ%4sB5bX zb zamsj%s~Q+rBE*_e%=a*^;VeHTh;jWyf_W9vG0+MpZxM(q+b&~RVTM-+W( =}Qd7lGE-SU~aQ6vLJeHldh2H_j++X27qP#9=Snh3ie z0mAkwXK6E1cSbbf+=5UHbT?!nUEUPR3|6Y3aMp-`369VP-Na0oTe~If nvt-)Y#$@2Q9jII~?W0AOYn9ab2ou*WJ9f zm>WnVXKZE*#Mr2$7WQ7R{o;S1QE#hBT`rECgXY3JlpxO@rbpW& Q*K+Gd!f7>{ zM11vbC~MydB8?+CjaNmcumdM^K&R6$K}l)6Wzj(nbp~D$01^gs4*XdFEY-G@;Z%^? zpsCXVuW)_mI~J}O?90f3`WzRrnZUfO05RfU30m4n&NpyW jX zfsl2i_J{VbsYH7GG-QMs$pRWSyN3w>@vgE>dO=@GM<#nJ1(?P16M(NV7C<`~b(Xl0 z?Y6VTd!Q%U&B|ZOqU4AwegqtzSbqjq`5XDL109N9W=PqYt*I~LcU*b@AmbPo(i4e- z;cy9N9)j8I>|^0sGny1sY#kXCOa%LxP}ot}e0}G|&;!S&p59Kjk1?`bhJK+gnDrW6 z?g?Z?T3c6R)fdx1NU&Fg<2*P+;1hvkkDKf`GYSyTV$EV`^b&OMC*YJQCgy?9i2mOM z%XDPm@sVw6)D-(CR6q_czdlmYpmc1Py gRLI*K5*!x=H_Q(91|7J| z1;O;si^qA-hY-q0W0)+QQOI&(iZ7vF&tt+?-tibXP~cGk<|zE(&^EJA!G1&(@4!C> z#~FjsI}D)p%bH`mh{TjW>?bZE!cvV9ga>sfJm;jF{RVEpomR|1$54AF;CqAaheL&e z$l-vV4VEY@_%!nEz^c@!$htX#ZvrH=;F7J4q-@JC1y^L?8LUWvG{zkr1ZC~PqBP6M zpklS-+^rle#Vp#>4#Ygp3&v;M?8pKK5%0_AaH>XE8ijK;d450p!sSOHdq#WY>B8Iq z!_vMzy>)e)eLnV&n^H+bbiUStQMJ32Stg)2;G#O@xO6L@=3+{6RVBG0CApz(xnVg< zQ6sxi%E46*)5{jrVBbjf!d#X^m*h?Wcm<2^<6A)~O%^kBs1wF(bid8*3PY9VBq~1| z^$}i1R&Uq=R0!)Usqma!At#_p;s0GFOd6=u(}Qn4AB?Y)qOjJmp12HR!PhtHVcf)# zOn0KAPclWy*Ea}uU%%rIA^dM{ii~HUV%%D#l5V${OqfL}w-z%k)0_haL7MY=&fK!l zP$B22u+-;- 2;prI z_UdxIwfhq8MEIHBn*KONWy-Dh!Ee;2pl}Wvt|K^*$Zl6*WK;{N?{e6bgy~J?R7DP> z`Uw;si5w{Y8D;u(*a=gukHNJVp6wx6z#ydBr9`F5RzgE?XJ5po%3ry_Vec@QxLl!O z3O$RYaSHMyfzBNJ!PDtuKay#q{3DkvC<8s6{4mmmcL&xg05rNguwDVnFM#te2^NiA zqs-c8rYG3TV$DS}gy!_)CQm*50iinge4#8(bebC`8!igX*>og1W+u?a6EkiKMNW|n z)=dRY{ir<^kZ-vR&&QJUZK-nVni;0O!?^yz_p0N`5ZDjw4QQ-2uutF`+gw0w39wzZ zYqOYRf}w%`t7Lm!GF;|g15KwJ&YByn;4lRtrDspY5I {TPPnTKNg bW?APY=SuG1o~IZY*&6|;+-o|g$ Yel zKks~0?|CfbMp<)j*9UKN+RnC3t(~jogOC*o>aGtCaZIOilfH9xZ30zc4C(q{Kk>YQ z<|4a3_$#L#{Xm8DRTaf-*9QkwH>HZDh`b6+$;3-3D?UwGf0qyulJd-t>=B{d)UBDO zSv!pt=IIz1MS;!nFKARo`oHgS{1|2$7*|e6y$}sOF<{J?Qo!dZZmPHDK&*RPqbsh< zHMxahT=8wLDgCZV^qKRZ9I(;4>_q(xf9fGt#Pt+Cb)fjbmdPDVeKqF%>B%{;!-BKa z3AH$7Ome8;R_ErYn3=r0sk6@xh~XZ)BDO2s65iFEWDEzumwjb2yaC~O0O1SaLXi`} zx<69nsO9yu`~qs_`3pJfvE}n1NrO#VUOuj|A3Xiju^-9wPsk50mz;z*(2K|qV>sAX zVC#QgFpj6mg26(;f+-Q5y7|EcV DWPk1vv`is0%?H zZdQ()1Jfm;VlF!HWRQvv5-R@HtryZG7~(h0hG;`kH|Id#z!pyYM#S<&|Mv}4GpZte z4hE%vD%l#1@HOHTo) jYI>rf${SgdIpG5O)rx-Xf7*Lr$$k2R zM|amMiTu-dkDfirH#Ubho5iWMG}X(Usz1k-c;73Dn_JSWuv87K8dA=-lf8Umu3br0 zJg1nLkyV9tm@d>n*~3 Rt(( z5XP|FM`8FOTrSsQQ{ML7@@!awfMkKsd3o9Ls)BWk>z3Cq-?#j|<;}~_EWfyXu)N3` zpTr*GLrsRVQ7fVzW -`)L2g0eV=e< zb}9te(aMJ~kS+;E!Tj>l>P35_OH1Nc-|Kkf{t}j2TV9eb;E^`4Zd@(m>CKUPX?Q zNL7p7n&pt@i1aw5415W-qLMzhn`ue_KGwQW8v@R}rb=9w`lp8%wqRc%fTn5}1I}wH z%Wo(>QF#_R!N;I)#|>!>AAG+w#POvBqkj_z8=|?$YN$% $^8^i1BLp)qo^ z?{@sit6%T{^Bdkf%@ptTEtn89Xt@ZVZ)4`7C@yfvWbvZV7-_iZ{nvsRH>M$|F%$}6 zCeJ>!(XkJ6Hach+4J@A^ki3OCZSJ<`K}jKA!$GO7C3FNy&erlkk_TSlL6?qK S zo9P4j=3~k&!D&Z*4YvrW3s+{j$%-t?B?~feO;x$x2~RJuTV?avYbw%;F>tdaNz*Vj zrDMkg7Wm1;^lX@SKaT?fH>A3k`+lS;W)@{c%M3cIn4AzO9z7ut-kPMbyXe3M9(Y%n zl}dHpT<1e>|FT3Jso7uGMvl~&9_d*#Eh}QJHftL3fUT Ax2guaR+0&7FJ3R^Q4?dV)~1t^{UTgiC pY!<|8J2T$|LBPT_t=pDGSvCnQcT05=t*@a _u7240l}M3)2ekW-stto3~!N{?a!v*Cuq^$h{Z0%|JnV zX$OL$1*co<_ZSt{Onu-S`7p LNJ%ZeL9%UBbO0)T>?HKm@V)L4?poplRg<~QWJUUcum5s9Kl?UlRO0cU@p%` z!CV2L_fh3vB;30pznRRok%r^khTQOg+!FTBa6;!iLVL(b2gj?DrARDG^)4A1zU}hx z<#(IlAe4vCvz|9?#qhD~`7*||g^XQr=p4 XIY5m+_Yc!?R;hbadgfC5^WAUM#`AvrWuF=KG)EvxnV z$KesJ$ b^SA(S%vel|;crN*K^&Tl#4 z`C}A0V#fY4szj&6&2z_1#k8jw4yZtF6l^E8QLki+MI93i8w&r1^X CF{vDQ} zg~82_Oz@VRM`7;`GG6{JyaSkzf^!UtkYgJB AMgt|oL9yu?;P zkAxA2mkc8aLI<$Mc=0 {{*H( zBZC;X0EZ7ot{E|1lWwOwCXS_7Li7qUmWCm?LxJO7cWni^*YJk6ZU?7zh{L#~AdGTD z*`&EQ1fOSa)D-SH!S~eMD2?E&P1!V2@NG699<=+U^d#>%A%Q >So3dPi)OU+|lKn>b~t376d%Z!;~5|5R?UEMf-ImXR`W07W7Ap0p- 0u-?GQ zS|i}yf$RF9#d5SV>((F)eR#?6lB-V>X|)qx&=Xyfb3lUS(e5iD2zvrXXDe0ETh7Y& zK+@H^P$&TEvP@3}QXMh8!0GA_R_=Q%WfJs=)L2b5m*%`ku+VOG*^y-r`yCIe#sSYU z{{+JQL-;oKOxVQCK6wAQXd?}Q81_Xg4I>XI52|z-PPuKHBs-UWGf6C4sHIKsNX}kj zZAp8M{i#YdeFUAYG>@ROH(YTOcy^>;hG!qd k56KK&6UDqaMrGkgI9_o#`)z=@MR`3P$TF zj1!r2+N8j}@>7>Lfm%O{J3ai(mo2Dt9pfhYAR#>CK#$egJj3srPq2&1oT9dgjLI ztM#U0$@#IYKnnI Hn8r-ADQ`(kWOzDEeNK@V!oDNYHOd zr`)@?q7qNGN)>}?nW=(MjDB4i?H78*-Yxfr^W?}+CFB{o^_is5_jm4=8&3u9l3Sl3 zIX@e1{qrl-?5zLTc&s9^F>F;9U3bupd&F`}63Iyn3?u*Gb& ZbO}Dvte3`+-dyW>l};o#`S24x>8~GDFJpjG{ZWxbdu>DC~~&hu#eNG!}Qd; z`s_Eu^$EDkR`4mv&40TZS%azxj!Jokm2%RNK&r}|HUT&G9b#e=RN&o#E7HOi%6SS< zaa=*O>)(N7Idu@1#2MT?r)A&9UWt7yCjv;IR;ic<7Kn3PW=r|H$Fb4TvN4c<{A WRTAjL5|UdnPM5@Mq}qqGJP;SORGdJ?(?#E z9^`jpnaK^r*wfg1C;jkz$u@Aol?d|@q7iOvNw#((yoaz7p$H)z0heXkb$Ffuk=_q> zyg3RGc>KPK-+Q|#>zTupwZ0rT5D&Hxq(kS9z3JrkydGQFssYnBjUOQ6hRC)yWp@=u z$(Z_*$x^0kG$e=$_(5-vZ%$X9(5MEX(3>pWNwHQK2nP0ysvc1A` z@ELr;P3}6W+_5)p%hWZ8${Ql%evbl*LzgVv;tt~;;PSx#h8SUQTAXm=(c#TZy_<~y z)52L27<=FWviqJ4Oi-z`;6|3JlD%o7B5px`^%fQ_1p5NW^F0VScHd^M#1X1OA|@2% z)4|GMXl{Ps9jO{+Ke2+@9CD_A$6nESK#Kc4CoIRYcrEsj->|Go*ek{fmCRCAZL;?_ zOJdxyX%8)WK~v(pzUaA$CBCvnUYrMYEYc=8eUjaUgA26al25bPea|~66~}h@YHE*C z=)T K&Z~OpBi|y}bsz<{7i| zX|f}sBYRl)Ep};m7jO`VV9Sdvf|ourq yRGfj19u1RTvF<@P(PrP^+ZOvvmcfuU+7O?*aQzrJG5#qyI%F{Na$ zXkjq4iObz=P6URw8l&AIIJ5A*Bkde9|r^`c}rVQ+oFs^aGuMBHG zDA% f?mC3^;iewf9ja__d%U znR1u!k>>r_Kld%hz8AZd3RF~*NU*wKQcJ<>MaRV{xR&b>xzO)7!DeQKuotGtjQ|S^ zBlr4kq4Ks;qwV3HcS=pUWQIVXg213_C^Yhkn#wsahm#(GnB~t`Fr{Jc`W?{m)9ibb;&Stq&oJga-MCFK_0XeoZ^}Wh zY+S__6|E=e6VoJ#o<<^QK-?3!4=Mz>lqn=Zt~@^s7HK8zo RHk#=h 8HpmKJhEZ&y^)Ux*VOa=(0sFh7KuL z<=868mk-Ygmg-i>!;@828Zt{87PuKJ4^C-VaQZ}{PP_102*|kYBoUuXO~dxRoYhf7 zpv#x_w!;Kt3`F%_T$mnsTrcbmEpLdNo&qTo(P~6f1FuLpxP*U2<>+s%5nBWOGt$DQ zze<6oDJ$HJ^U&}J^Vt>eX=H2LB+9veWV{MI4Xo;)5^(C42hiTU4iJC(i_kN^e+R1{ z>^aENv3p_*fT{^ObYD50M{2UvLBO%pryiQbF{YVAI0uNG((bF^%$~oq7<|X(aXSM= zRVsxSX8x3paR+ksw>1och8{T@0)RQ$WfF0KVWB_=-koChRb~O216E-AP6FPjTA*#$ zq?O|1f{W5^S~+j^%1YvfU%B#e&$n^%GRCnH#~JKkW5-5 UUNI^J-PFSs*UFlGDV zKNifMzg=+PlOGfebtFew_Yk?zKV1deSFV)b*8E^rTR~SX*t|6f{#X(H6nvC+1o?`g zEY*g&O$* M;IlSmvoocz#t_>Hp_WNl=lrgA{sQZEy$mRd>e=XllD%kt~Re@S$zFz^|Cu48d zXv0hk-=+e87+1-pjXrAGQ=4jPRySMbJ+-OsbYSOL9kps|`(^31kQcq%UU7TtXYV*N z_*%%eSG;vEimJElAFpL$@T4jpXs(DpW>LL0=ZL^$8(7Y6Aj03Ml@i0)Cs+I@%JDn7 zVitq9>NDw=KR@$CMQ#koq~xV~emhpFii|E7Qt?1fMf5ind0*zUT$FXzCb!w1Z5+j; zkU~ZD24ddAjZ|O 4seawYxdKC++DbH`Fa9$vC z7ha(P&yG}{!^uYr1O=TUFgW%sMA7USQE%6ax*XaKRV#sA#IM^lYey)s+GgL%JQcNW z5Bc>pYE(fWgS |dw{S>u^Klj4S{4XrZ<|{^J7SHx2TKKFlvc4rJTFj5BVZSZlJ#UKnywR<6 zTuKx*;N?VL3hxwZgvY|gX`Sg;9SeDp?rip-7Yy|Ij@b8Zh+iyhyx!C~?oP}x{ms8Y zZ?*Kv|BnE bb%0glAt#RQ&m_E_Qmnu6HE#bAms~T}%2@UZSt`nT! zXG-Y`M%BT*x2}n$wsuT!7Thg^B9rQc51(E%<3t-yR_hXkh;08EeZ5_rhI_ ucbmsMh~?W*J_V z*=3iP1z~}m5oEz#Wmip9P zyJZFBkP4)(meQ3{veHsRe8m^`|2?yy*zf-SemuMEnKNh3Idjf)p7XqXAIQ6MoQQ3p z1-LT2+#S~N3`r-Hph{2VI2x-`3(Qt>w16>ql?n)6HI`yX#K@|D%xEc|#)BrPggPSx z9kM?aATHA! Y9ez5qVq8sTas8JTss!Oq%I^Z73ri1<& L|7jtLqR@ul%WgSY6eq4s|x}-s3cP`gY&sJD~+} z=GUumY7lp)x;PboziQgaDHDJ1sL0X6O`6|JPi+{7=oyxf?(R9T3R~*()8>)BsrBQm znKjp2h+z)wkgPX^d(=U-gRsb`z{s4&MtOB9SC?HUX=$g6_?51sRTmbPub%eZiZHAv zykfV?rgrS5XW8%No-ha(11o{jT5d0*rgh$$_k%6uR%2*hxD14#C>*JwGbP^aehS{% z71K^a=`e$=J)6qk9wKi|{WT8&Pby(+YS5tT+FNcMEF?nEAkas+I{ 23)>x> ;`%K% zawq6E7pyL^rSiUf`Tf*9T_-I7BT38rJJ5juK*8mAl %%0lbw$|*J^hndQZqb&{Lo=sSj4VLHw z&yiKRbX B`tZ9fPpfJ%GzG(_9V->12U~Z?%=kEVw zu#HO%@jDq{pl(+bb3{qce^o#nSd|Ga+z~AuyT=^cc?fG06I@uQ4l?JpDwT9tpFBl; zD)pLpTU?HrO$>4ev9w@oK-X>H1UeNv!g&la#$av;Z>QT9!Z;8IW#&I&Ui)oe{gn*> zTIS| g8mv5W0@s(4|7IAE(Q*?J@{~Dq;Zhg{4Na9{Y2Ch!RLR1 zvKfsdj()lEHt<9=x6U@mvTU?;Yr!UcoksA{StVA1mCFk%BL zA29G9C>;+NS|G@Ru@A9N ($wfLif__}JD9}R=%g=#(C!P`)# za4Vp_fgnDUYKA~y!%++)gq>u0jWfvqF>sdNJ&(fHu7eWS8y35(ogRBnptL72zh(om zS>)vw2rU8n{22@=$gByjomHq FlQ `KB=+gdE#yaT>eS#QB;M?=u)3=j^Wr zN D*N5)|;|C`wpRT*ohGu2AHmJaB`%EFSrJKIoNz8xWaMfuxv(57UDcX1i#}b z<9Z72zQUN)PWmW(Qv#elaP0F;Oe||=dj}N+j)}^F^AzU0rQsTO3{h_X^%S^-(0WG> zC6Ct(NJ0kGt(dW(U7d7pU4$JJR2Z3xhcn* !fQC)1wF^bi76^C>c>oY456QBg09>S`z zo~YA5q6+dm8DSv02Yw)u8+BsfM>?2C>KOUOw`D5fB7Px1!tGtfw!vtZ3g3YKqw}r2 z21>JxRLFTUQYH7i1ds4!YI(9NIb@c}q1CinT@@jYVvVDnl~< EN6*m;IqDq-eX z0myNxshd1aYYVsY eBZMkXesf>BBa?!#pL^$eyt^8LU?B^Z26rk z-bulc2pa>TnQ;3M{{tz(=QF!vd22#f+iaz6GwVX_$v+a8`+HI?rB+7C%=mB;SMr04 zE@(JIt|X@Vn-wScq*PaP@9sPgY;Ix&Qz|*Q^aDS~3J;_rldP^MQ?UiAVGZZNZ`g$1 zqg;UBES y_aBm@r^FQ?#hnq4lrgs@}eVmrCW6qAecMRL*6 z6fRG@RQds~LKUoN<-cKWRIXo0fwv1Q+)4ArpF<<_H@tfS{}L+~T`GMC<*{k`LR>yS zGbO}Bvd^aER9I> uSxq8V}f@WTswU8Y_id{1;9+fL%*{BB_G7=+KSYGbQ!6Z zr_4Ea*L&aK>@j6Stn0lmVbeNgL!|4ycgY`cj6b7LlzNn1^bFoVdEePcCL|S7_tQ|V zA?-%mhV%&%HqT(#ykpBcX{MSs7XheL?{XT<`45;OTXn`boUt(a$YF8u`-m9BMn03R<0W0S!Aim`()wRj(mi-!WRz;vy<6IX8_SB0NbaCX>5Wx8QV z1lEJO;ZpSipm>l%vssrg8ih>EPboYVy^u116QW|OC_V5Kso2t8z%t;exI8w~?pjxc z8tozO_d82l1J&oq?RVk!Y-sP8duTF3N9{J90NZNMw!kBF9y<>T(z{V&1WJf=>C!K; z!;!o0#XGklPo4|YqhOn4D{0dwN}K+Q%fm^VWQ3N~3in =E*~q`ur(>nk(J) zis;?036x%e-x-Vtj)Hh7=B;LMFt8xLcEo&Ld95D8z6a+QwX?GkTlu9v7@6=j>8^DP zp;$~Nw*@FnIUZAUfUbbjbt5h*>u5ZD-lJWziV8i_~0R)2xNs+T51i*oc5!gXJ%{ zr;KM@@P%}%*%%U@2*8S3BBbdz9Qybv>4o`O* NVse|S6;DA?SXRMC~ ze3K%#hS_dfs? ScKSt7LF8clFBdBB0xL{H-! zZc2 YaVsIN=2dLgw2&N^&2D9s>naG;Y0_) zHhb;nf=2GepYvS`aJdrl4tB}X&(iXRX=>ro6zScCL$j9soL$g}BHd@|j9(IKVVsVs z{p7s^R2+ev2&_0UD*JnibYY>x1E!({ggK~Co-@l$efk56wzieK+g!)eA)i-OvgaFq zbQoZF;3%bY{+US~ @Z(Pn4oKu|$doJ2cd3xf?wm;n>b->9Ad zatP>mB^1#NqSZn#_ufI|VLKG$z|81yo?Uycs(@!V&g@dJfPBpdZ|;q}HEpgViTa7$ zjrE5)?r5{(792mrR31p^iKqa%)GMSeEDPd=3uWK80ejmOV#%^TN8UpKW5_NU>UV|# zq1j ^SSFkAm|Z zT0OtN)dQUAfFbn(5Q^ v2-g9_WxUW_iY-Hj$U0oxl8DNw@uX`G zYdX}4_Gh~@wm{i$ufF11A642GSP_Y#-dOKP)?LVoE8QL-^LSXY!+vM*t)=1|Gb}HB zZb@|MR{_`hbooQVH^~PGsDcV-8Q1!(dD?jJ9Kr|c0U~= I`c$5LC`GD%V5A3xRS{kgV|C*eHJqkG !ujwyP6ior6|(UOpPEB|$Za$9lmw#9Zn)W%P-t>qSeon6(E?S0_|?sU(g zUm6b`ahgxoafY6pDDz1@jMf`Z8h(RUUUpGxVMx%xot|nC69NcE4ZQzsV&EPHGo0j< z# ?%r3WuIH-1puxg-|+MqBSPQ{bSo1?SBo|5?? zAqT#1Tyd<>nZR2e_3L2(>8q-S-zZEoxHpbpHBEE*vWd99X$}>pEbFE@B1Ix~B1e8c z1x#hreBt>NLagVAX9LLf2w`aXaI~hG%kp!XU7Y15gSNY9ReHV!DcDLh@9@Ky3COX| zI-}X01+L$PCZgf;QZ?oJ51D~A8KAS}6!-Igm_@LS%{TDjF90+pIYi uJ%G7+`hxj3{(A?smq~Q<4oa-m)`G+&8=cLaczPWT=UjtNIkeIH3 zuoygTTP(st+O*Klkz7Fx^t2ssA!kTc&N7=$cp`9g8JzpD$@aLzg;)A|b{(JT>VvNU zLhzt%!jWXDaw%f!lA${jAnM*zi9Z44w) 4GDwaIM&inQt#3fT6kC*^2pRo@qfVi#1guz;9KAx;Q@Ge#dduYdp_A BqXHi;vEaWoDAc1RsndC@s&B9`)RjE#K9*(Sa^U2{#2+ zmOlPk6xLLGY>5sQb%nQ*W@1oUP6zM@@zY|8CAQWPDc{}K?P%
5 ZKm*g9Wn@tbTQ-H3i>2wyGU=ZVOAjxb zLAM{4_ADFe`4(q&BK}+hetkL8e5BcHfH8^h0;JJMu}BxNb+7jM2;FfH!-mb%TWNQO z-<{s&PU~?GoypVGTL&7iZkd;lXN;l?PlIo<@9TA~dkC9#%X6y3dwRha;n2nxZzYt) z$9P=pmWj}7SZxcfR^Dyu40~`jOV+S!-DALB3>8%uYH}(<0j`1`O*9kH^^QKrH`zFB z^F9hv%%m%88&=jAhLO<<&x>N`)VWhvyHlvUzDHx~xYX5LO6$scmv`- {+ zeJlP0iJZ-^G w6)NZ(T`qU> zl+|p@n_z`calfSc3*UzGGNbndT;IgDs8#iAY$3RZi3;Eb*EZmUCOftr&sg&d4u{c! z!WYrKZjbNKc|bjf(ki|^T)01(H0ZU{_U9Fx$0CFZA86JIb5E1LWu%JNF)kfdrlYHL z>V>)x9B6f+_B9t_Sb)jMFbQQkm1p C7 zVy|+iE_A1e|I=5+r7q-B>{Y#`4+Sy}y%g(kt@|hQF)C;66}_eZLwwuU{Y(B38CrXJ zZ|z(PhLI%iz1h`v@dFwpfgFtixav+J!?U2z!KHZ6-AroyoYLxmeOfP^S;!5WyFF%c z6O9#$Vb4gRSHKwx15T?By6mpr(mP;6=vw!&@E%xk(ceda7=Tk~>%&W7=OCJNQXcOt z=bL6xZym!N;G1SYmB)OV%xl@BHM|oAv^f>L^KM?N wc0j%SS#lvUal>y%(`=G~nyEPiAixRvz- %l-hRy`&V>L&7L$U^~HCmJJd1^r53dNirFVv_7Ju?4Rwc2hDa& z_Bu@V7Y*hY``a?Q{0 AU!RT z|EP=@xIihDO(4X4%HFUXb$x=gy+3u}8$07%ntGPh(a(o>odQvTNhNkvVq;$cd)E-T zv9A!vC=|ZGFEoZ6$K)!SY?B`-;h@QHH~B5M^c7;eRSMkScW}LZ WyE#RNq(|!l^2c9h!}^=h}&c82ijY zFvpn(F9S*$ru-~l5i2g1#y*ka$$|tWIwOYI7CFA^W7D$J9uwX90~%PuGr(NX$vT`6 z9^q63z 0(j)`EwsO+{Ld3pNWWO_xqN|-jWonA3i#cm^T)6`b- zmRg~O6`silq{%kOKtCo~e3RWHeh~xXNWN(d^F<6%ERGXDkI6K!rmtTK5sOg^+6DZP z)%mQ}-2NEflqY@`vp+gQkzqleCP60a6hDnIu2=DH;v30E%g?3qoZ864M`&!cmZin7 za`UKZ&ZkPnBl!eCh?sZutDn(1PsAqiUomYw@cv-f!h*#`<5rQzL1^kz36JIHKzOQ` zH^*3 9Vr*dij;tKVKw{@kUl}$h~z_BiByABj5Hd_gv26s zKZmkNTajKz%0a6680!}J6VUsc9)mka;aGr`^}*21G-oVqoJXmg=RJY^QNA3tPp#UH z!c#sm=eOhAkF+1@Aktx^Uyy!B>PE7o%vpS2L<%5<;CBR)4k-pH0Vx@YN6J95B5_|s zQv6>(JT2tPpFU)g_ot6-B=1ijUy;1)KM)(yoV|!^3Y@!z#ix0c&jLt4?roG@i-zO@ zT25R_vm z_8*diKfd=nsId@Fl&E;$jy;F^GNc3q6$u)e|5(+OBaVew18z1X?1!Vt&No#s=02Vp z+r`Bb_q|q#LNh7Xx}R7cg&)z0qoJ*@5PmXYK#8$1fi_3&lU4R+0$S=>VSt+?lo|}s zD`u`&+qd Ya swrb@hUqYAzg%57Cd)@mO`%)F>3)+pU=kPM6Eo-;iOzd?nV_Ut|99N z9mNq}8H8GCv}~Ug>0s$|w`dWtV!VS__p>|wlr{(RXyaGX_@`5}F9)_ZljE#uId%7U z(%nxx3Wc915;0JaYDG8IHoE~)mnuh0Z1z!MRg9c82`vNZa{1=#IWtxK9eO~B{>`LI zP-TBjc7_tE^XV*)xy!k#%lRCR+C|IS)ebJrrp0+%cs9`HV0EDd1l~eC4~;mfi*GVh z*`zMKm|J7=0Ap{(nb|Dgh1g3>FoUS1^5rUIh<}Lz3)Zli!1~q7Gh$p#<|GHd%?)98 zJej46{*m`MYq8r3x5PmB2KgO6+Z-(_{9}3EJ(PcM8Sh+)wkD8<<59xDC!W9#V5Z_o zhVh%T*n6j=C8imm(ln_Sg8ygW$YYE?16FulfK&BP*j^mxRb PFIudUQgVioYo4^rRDX@H|bRq6UdEROC4QJ}hf6RCo*6 zA)B45W+#9&A%3OZF~P{*y{F_Qfi@Rhi^r1wunvz8dm?p^Off$-y`h9}Dut&pMD$I6 zVenlFz7$Y%j`tdS-g}$931R;*vE1Q3PO8gEH#^mGK}^mv5;6< ZEul zjc9%l?6_hwokDBg+-RElI**M8*jIeMvFx_ k^N4wl zCwK nocTEZf}wMBc->hs0GS2tY07uOgm6qerpAM0GLWe^2d|#Yj=w9j z<55gK8DgB^)SLYaX(jA^AmPeDpPRX1elER>^P~|Gmnsmze|!+;3Cs?wj@U$1kdDDV zB3d{%DacZ!$K*Im&jzwBi?Z_<;Y?soGiT>N0ES{ZuLnEf#Kw{HKtFS#2e%Pi{`){E zK#i0Y3ctJ1hy|2Dss>mf10z+Sbgy>u2DQTwVxGRpY#|l94+hNX=IrT prlcN2bz1=$wK%{E>IK( zc)0yX==VYuu*U_$Wx9wXGRS=UhE|VLI1N)#`3v130?O?pWE=V;F@UwC2EhiUu0@N# z>Cb0;BtMv`BJ1jq{p$cG9Z@%&q;m{JV-=HQFMap_9;-K$v0C6!II-YhDY1=}%0B3? zOgyesChACKYya=cRtGElZ*}2K*?s-h3BS<}jso~X1gUR1sn7P`-{hnJy+Y~E=aQp# z{EU}J#P(vd35QT=-UIQ{`sXs}#7?R8IYgzKWRldYGx-DzjLP^Z6Fk5If_+ZXh*{2i zsU?OwH~pfWdj4nW#?^`R&HJRetF!bkgKwG*gJ?etqU*RHzn6Tg5#DCHsoa@E;oLm- zRRY9PHp?kkk<}gWg`b+mPQTOFVVY&V9(TW=tx-F8EAZWVeD4G34Xh1#oKE^8;!Fc) zXGN%r|0Z0C!W>=ZHZV7u$~c3Iiw05~9(C)hhaK~5#(fdH!yug5dL=YNnkL!jp|gwI zh_0oCo ~Me{Ks4ZJN&`RAWCdpg>45O~6~4(TgW}k1ylq5AOpGnc zg@f$(0UjC6vHUz6&Sel CFU)u`EY}AQL!ad0Vu()684r zrk&i)oB5hJ8yh^a6*;H8`nA6%j=JbaO($q(E>*vp8gs5uWl{ln8x+YJ?YG95CpPZi z67szt#_oi){X@F>g_JqeFXSHH8rxM9{X!X2v@xe4dvqgk gajd1mJQN3RxYuoIRFm4EE3Anvq~Q13*K(2sKXvOd z0vJC4!1OEleM^ ;|s~t|8&22@PhO|UPq-s5m^KCLsCx-DFGLPjD~#{U468CFzL^V zGCT>U+=@r{f0FvSERYtWu;!|dMJ)Jwy{VmuLhZYH4_OV&!6YeS?G$?EUa4%YgO)#$ zR; fGL9Ti^~359}a@^Xww{)e}okis8EdY*frnr&1u9;={)wJFa)$z zh_g!RVbG=g!20d0rTbpiYUGP5m<~gT&Pa7HkE7+~lHrx~Ncr1937rS&vhXc%)h5M= z&HYmAo?WJ~BThhO-y}_Hu+s8(QqwCOwNHAsp_Uf@PVUu+js5pBQvNIXF_5MN1>RS0 z-9m1~^BP~_$XGtxaHr&YwHO)CzG|bp&q`msYNmxX(xF#J)8C(!jIZ5QBs74H%hLkS zjm+#P0*irmV+$)`<(C3*kx|{sTf29ejc=$+fE#YpEun-L16N=rQ$mn^*OJmvq_nU? zYI?1LW>!nn)@R2(8)%uPTFAvdo-vCEyKQDv_q4QXeIhO7O0_;CEiabV`o_`Xcqy>n zLFG!J>;4`oT46afv+B3dcLRi_k-X~w$ox0dH!@DB`QH_yOY4(pIZmmMnW@w#aH!8t z%RDL^DY-WkL^7d&uCzqbzg|9Rb>!@!kIi&M{xy9r9}CovDDiTi(WOCDhNy8-nJi?E zx4t=i`X9_~Cl4Lwy6^YpP$j_9hVs%`2E+F&(z~x`(Sbf`|LZA9@|wWgT(zNbXCpwR zXSezCLTyncY^83VR+jW{#L+^JH}y%{DCNBoOUq-W*WMgKiBjDg1=JDg;nzpdOb;r{ zR5NFk+GwU$I{*4@w48*3PS4wz!B)`0mC8(REWI4Czjq1Vucm!a*g1kw6Np$KRH!aA z9NpMGqS4HAG7BLF5J%`rH7vt*ZI#U7zzPZ?)DpA&X5g^u9NtHsPVPzs%^ayZbj5HH zU`RHd@IFNB;(IE@2G6O3mIB0Rre}Rbb;HHNX$7;>NX=8-_m(nYfg*QxDR}}ssnoX6 zypXm#fe=TYfH{OH()OcgiBjcYBIgf) *-WJ2SN3orWH z;c5|)vyo`8L3KZ;3)=$r>PzIL>&V^4Ov5=7q!iOy92NMJO|gp8f8*T$jWqu4@t#HC z?5dHbBXQxoK);C1O-~}X?HduF7XrfRK72Cxgqz&)KJHArc)TyjIT5MT+qmy_Bp*@( zspB#X_MMjk1en@IIb1l7t8q*yQ1Jy6UqJH(YWOB2N`j*Ru@Y!ULV+4~=QtnnlPP{O zPWwt&1Fn0w^zPAgkJy?BAEv7@Va07q5Z0>>P%86kbM|8ZA~ACbL}oZ4)KZ{RdLWh zM?2Q5vJ1>pqq(OB=ks{H{`vI$Dqg*yjj*K%)`(f&3@bL@J?-@Ddmdxqax13+4&3pc z$~+YD%)vBgns6u}2s{JodDUfD674%c LexF|4S zJe65SrB_kI7E&3&)yg!$UR2M_0@DIU%hi-6k4+kEAosB%>S*kDidQN_zyHt5P?s>{ zYF*5z>*`WEDm*%3VCu-Pn3hSDPDSAY;y__v7bYZ0jqj! $|`)GL&JORsIJ&<;BMnr8Dmh(}}ZSsAAC` za2<8X z6W&M-d>ID-JaIoUSG3lVS0^k>WYyN?2!8YdsfVj+m)?DUD0NcW_5Kt!v*oljWK)h> zc<;0{W7ES?avs0~p(ZC^@{fAyhfSlX@1^L?qp1B-;pXXjKc?O_EA&GLr8hTMQSDO1 z7C#RCQs
aOK_7C>Z%)8RdA7;?+CraBsgk!*ZDdQuKdP 6UK!{I-;|2U_wO9k^`oVw1zXYxW|GCaaO~t+ZrzUy&+XT{% zwh~Htg&4{x&qoG?56Bw?pL7aua~4t opEhDV+ko&7sY0!H_dO& z7)`glugZymPON2 ua(G2;_5tHzlEhHr(8Z`GtD;p&iAS0Ja zOe}UWSB4-=Aj`Pc!!c6Kzp!TujQ)3M8R0^Xw#oDCPGGBMB8-PHtuNOlTpW^Hz^XfG zYHcf-*Y5@nKe<0sTS4=c8m^?({&pbq^UQYctw4u56S~r4Hc-wfeZ|f4xgpMVB$Psk z_|ZAe7jU#&J815;K;{XoO~k?Q?Uv6Bsie!?u}t^HGAw z1d^oih8~$wV(^Q3U7rIub$)Uqhge;A*}2+acXQEQ@plheg@+ZOWmE-u8XDs4YvmRP z_ACzAmjp7Isn6S(W9hTB!b;S`J)qny=JZ*#&MEuZ4Ptul6?&%2P+TYfG9-_7xMStr zXB}KDH!rZ~KBeq9tvm*$?GyGhv-Ra#fU*TUf+va(C3^5Y(jP|Nd6x7iFa$+s2IM>R zCYOA8h^Zhi3NPaZ InthU zL#(vWF(fYvGCT+asaU1vNdf!LooLF$z_>?HOCEVp9+?ciCFD7<*BJ(#wfHmC)vVAn z1jlIk)Q}eF=1`@hX|y~(v4zIpqa8kj%7v#4Pb9cFZq*LDrAS&}i6=+_+%G$V6hUmi zGFYy}y>T{}kD$?m^X;<_j3cn1_B#&XdeE& C$jXxbb4ylKB+RIEkz|9@MUt2}g3*v;eG2y3hMZiI8)vZC+M>m(M5*d?%VZY| ziN&;B8A{fs1Qfo5{APx-t(>DR9pdx16caUeI3i5iW HQU}mNE z&xgzy0oWPg!y$vXkt4rBji$h#5c;?(Z*Z^CWH4um(3}KGcJTL*2?M`#0hp;jCS`q* ztPvTo u~3wyR21!gE?^07_JHuU@ME z!gP~90~W0VO;FCN*93hBFn~C(VsjwUV!*F0*x^3r?6nh_bs22t=kf;F#KS&@*D^E9 z5M}MG)bmC5Fh)o$a^-2K@#y___Sc3s=x#9+FR-}KY86J3VR&_jRP?1oEvy?NJ@RFe z8hC!v%U{C!yJ1Ktg29OM^&KqKQL1KMWo!UX&uDQIvu22t-xw!Rjj3k&gn6Cv=$Ug-Ac<$Aau7x*y9|D~6a!Ck zD#rx2m4pjrp=kt8UzD`8$?RT^#%=%o|JS(EtBn(u4sM)0&G z4iM)7kf2TaK_YCh7!Vu{R4bgy<>Ui+A1KGX_os6GGa%o*i0~g5lPt9T(*v;6HjP8+ z?VKmL+Eo2Uy6!B@77PP)aAN8N;-@tq^pZ*^=4m)JI#)TG=A-RDMD{D8SSX Nm^*>e>Ha!S_EDhhCt}YJ^;#)-l{NpOQ zThrSorS7#@3X((3D&u1zrmtM &g zJ-07iH#!+A7~{fAu##oYa?-Yax6n&Fyw5G5vZTr1rc#;GUEf;r@9e;Mlv77_w^dCK zG|v3jmC&AmN=!+{I^%0X`I@lI`ixiL*9%#6IVhElnI@FQX_%jr+i4I=#w4|Vn@#;y zy8P|b=o005RabtEjv#U7C|ieg>vt0 W=f z4h;h1$dwuf7O49DnRN6JX~O>Lw04N}^#1b1y~&oE4z_-w3}|}=00V$OAY1YUlN8tw z3!T0MDdj+_TKt-m@(;w)HQmz814A?V5ITkVC19!8QHfYG6 3CetRI^gT_qkA|lZn|plx$KBq>HXdkjah&Y1QdrQA}sTAXff6$$m6T*D)wd^dl=X ziurC(9 KPScX45AWU#VTe# zO^{ZcHLIB~5~O#|j@L5D=!Iw^UD=W#b)QWdD< ol7juq3cI&A5|C*Ic;c|q7bi(G?qc3POS)0)fX4#t^nyN zVe{DbkeR2i*woiqW?`HaY$Hd6eIsUkLzfV$leV3^(I~8p>fc(GcrMTh+GV3ovizPZ zO+KGUy)WHyo(~rtaNQKwCP*vKr$=<3y#h^y{Kr}8qw}fxf*84wRzb?CU1ArPM}pm0 zY|o+QBhu+ hJ!E6|O->!s1XMWQdUkl6Kg35$PBP(eFd5&8c2``Xut6+vAWW4;V#@>cgQ5s@I z8GDj*_u^0S;m9Nx3gYUnfWNI8uUjj4%qY$LtN|WA8bC+d8%ex78dbGAAx3^a62E@Z z;Fc~8Hqm0SWeytk3ChX=jnXKgOliA(NK@a5hsbwC$-l$5j$d(`%Kjcv3kp|ALG)6* zIkLV7SOuh1Q6w8<-Yf0i?O8;2yiSer6*8On;>TNx@qi8F0lobXC{}tcf3Gp#iKtj< zLhIR&s(=E+{)Jc%psvRv?Q2Q-a~d?76Q%~+vtI)> 7p>?Jkq)fn@Xo-bEMj$OoT4e)J?aBO5TaN-_K zC`Vq?Ll`yrBxx{0iaH}3HrpRWh2jZRh^I37!S329U L>GoG1Gdr)T|uehXcv3O!4y zcuqfTHQn+Gtm*lZwz^IVj6;zg_YZ?T`pcyBFiFpUb-I2%x+GGOE6$+~ W>JXr*zdu^Edmo+-;7tAWrIlzW-QzV z>{aA~td4|vH!`7zklQqTC#48x5R-BOwot};wMn)8P1-}6BtB1?E|5=)yhdYVbGL|@ zk#9!Rh5G|aYb^Z@G K zmA=ct7srrgqnB5buJW|rSaln3bJe>j6VY$;Z+t){DSym!&2*{Q^@Y0#k2-I1%bnC* zN4_%8#t fxdVP$maP%@FN{tSt3k-7uM3F}FvB`kAAH~3DmU32x} zI( t=>IqD%K-j(5=`8uUOI~>)z0&0Pqh=V3WX?v=;*PdRP^` zVm@3cs=Ku9yxSmDodrx|#zZ<4w{Qmeq=JGturgaNt3rFB+Ook4PYbj`x#zl8319v1 zRvAiIv3pRngeIj~df}vwoNfA>#e5lTEOT5J3a#(J2-yftQbvo7;@#0f3jy00#Lgw7 zuG5oQfkt-eE; &uaoMc z^_NFjY4J+D!*^hOUOVj&)*=SO wFvxvPOnQN|=b%>VIRnp$_nrhfWI@If=sTm$|-?cGg_X=-Z8|^4PjQNx~ zAovHB*}!c0a~Ug{OuMg@vF?=-|8-y@qaMs9Qbukc{Az8&s~Y~V!8f79Gzmrg=Bw`^ zN^f+4h|Ak_zg>L|JIQMR!56;@X5x(qFKLoa9y+uLI{^R!-7&&w$N^(ACVtow=!4sE z9lfHIQ0uv?>Cl_J9yJx8+l*JLO#W@x+KW}S=309(_xt-THw`MYL0lc2W#X6Da; MnBl=%vowx%&ym*HqJ*E7T7^hbYow-n*`D76!;I#GKUj!MXcrIU zeOAoSAUp#&m<;rTgaBF*4MvYW_yTxrIbD;d!w=y$jeXi>aA>nMI1YndI~ZWnl-@Jp zaK?aZYl&t!s$kIYJnhjk!gcYejxfOr@S30|El_KqwPz8oPqFJ%{SO)sO(7Jt(C-n; zr_K-z%Z!LqB*p}lv={4>oK?=8K y(W*gmKBv| zNcQ0~e*JJXIi$>TGEZsBokgX2Fx 3;$Wla@l05_Rb`1R|+nXhD-mo%Xi>`xg0Kyi>(gXca0d3FL?Mgh?P_AgJyx7tt8 z7~a+q7`${1v{?7^H<|dGl6h{p9!%)5CSgY(M)dax@X)`3ZBG!$XIj#6`B((b72lu9 z_<|1I>m3 zh|uHTeK2Fk$<2vLO&0SX5y}Wek`dUAx5MntB+U?ak=G;aiU>ai9Js>=_8e^*02zH3 zui$;8&Cht(M^fo(hSPX=Mp9{6;x7?klu4UdQKhjXFg7dvtMA}KpyPqB{AVA~NMSaf zyO+7$o4``Z9>J|l8{u*UnKsPj2mtFvk!j<59aARD)_Z@?P07ca#Mdx$geKCqCiEW4 zryoe0;n9y@@|Q|$$!8k#Wke`m2zi8Xi>8tWogu2>>foVExIl_;#dzfI6-zZqE;JM4 z_6E3j3?c>}qxD%t%f5ih+qjg94gCOJoW9cg3rpP-BW^g8 R)o~g{6i(!bUDrvtxO^@ffa$KCPCwURZAs$^njfVikGmd6aNT1kY)Kc z6XOSfTdcfCJ4jrm$WeiR$7i- zQ;*@-rw z-fQrVDplG=bN;sT=KRaONci?4jlN*cZ#>QA*Wf$%k~#l%e7}wFgZQq-cPz@TP2}>6 z@omBH<@iPqiQhYV$96pDHAGSIf;a?|M|c^pXEcH7W!{P4eIA5m^%py_TtP+J3govU zAe=#LT=HE3V)7=wtMU6yP)*1LDkW1)j`#k8Ob N(HiAD$`D se@x;<6(2~d;p+pFgjQRAuB*QqvSsbqp@6RAQMeWJPQJY0?+gp5RQ@pbxHv_ ziWFEGEU lOrZLolr6sQXpSk_-aJVOdR8Y~bQpIJ %E*SECAj>Mw z!uM9QwN|Xgt9u)NFyxOlxN;Af5#p_6hA`x_LY|4w=>qvIV94ia@h|vH^DuX#XskF# zp$UWI HJxh2*A(60t6R=mv<0A?o z!~y9j>id&L;Q>(gyPop8VyTrhGvC`5OQpxlv3-?v(X#qzXmsqm2>?ME+;_d+@HpxN zTE4~mMI1FDb%OFdRDbn(%q1*TISJ1blOntcMk+Z%tiedyLc}xPiAHK@eAt<+NoYh? z1c4l6?!~QG!}mpyq47Rtq!Pz`bOk#Dc4DG`9vN{}$Hcu)@`=_;-=A4a21yk}F7|$J zq)g+7N7yeAksvcXf^fP6sK6}geq@A&b!naDRe3B*G%|3kAxwclwmD6NH#?phl{_J$ zU)~;klFJw3s?ziDNmetlthY9v$~4Mrqj{_8HyFB`Y~)k}(^SzL?fo(y!VkQUpW>-( z99XscH|uz7vV2Z!&TGUU*lY1g;3i7U@&)gx1Zs4YWW1)Nkn4?5QgHMtqxZ!GsxYd4 z&^19cdQT=$DeUoZjN9+{5bKOYaiNuu;fFvzm|CMZe+b3tmm71O?X-{@(Q1VtJte}s zU v);8ssC {O zuT`!U+WRYS3>)n$`4M`M? z3iDO0J+QI5>H8ni($Equ%P`H6-jF0}xMvYspa_MTEmv$*t6iqZ?1Gg;1QxbVyc~`> z`E@v&@^!dnW7gkxe+M*F!YeWYMu2>Oj3zAJQ-XTI--ijGDvji8qTry4{-T8ZRrm@b z$lm{D`E7UJ`uQ!EgWXpeFY=X9Y)Ixo@4u3$P!Ay=ghh-%_vOa$5^U@klYb~ (?<7o=V!2KmuQLy>GqW07t|nuYWr(n_S~kX}G~6X{c=y-3Ga^$ne*dJfWY zq)|vyk!B&?jkE;mNu>SHeEhwM`i-he2;=j^lezqI6PLdj^D`TN@5bMA@b_B$4ITts z6=4t027<8ithRSSCfi|Rqp>w-8L{Ve2Rf|Sc>f? A-GL;20EfLIc{a2%;6sKc@_()R*S%@!sA-tak;ifuK>XZ+ltf6 z1sbXY&Pa@%n62Thw$@4FXW?Xf0s8jSf%@Vc6VdL0d1etRhejNY!N6T%-R0;gsOuN0 zRPx=z9|0IIcC{4%Oj(Vk@^X0jvJO)ogKg*iaDST#fJafn`>0%~IbGkAxe;6JeW%;1 z?7sm3N%%M%VuN?C94Ny?_J-Tv!c=Fx;r)b8OIY8 J4mlZ zCqO q}@mdkbXl7AVotR`Ry}a7f)q* zqusPI8f+qQR3gwX;PZV zFrwt?C@W3}TLW@`e|WWt|JUv;Lmu&+cn#26&I5t+XRBX(gm$d6VwSCeEE*a|AF8&V zpc>#s>JOA}RIV|Sa!r0U+<`Z$9`~A%dphT}hB}*?*}*z@1S&}tP!EW!NI9L*5N@&7 zI~GS5>Q#@>i1m;|FL{sXcm5W^w6Hm^Rno0PXT2uMtn;%#`D^o(=j-Hp m0>WA*`SRD8_?jf%w>{I;v5}n8=7pQ;gm2@N z6~TLLX%xVJ8X*nB{?tRxC6mcr#KgP@w_!2rgnPV8)2M>v1H_`Q%ND!yj$2bw9JaWz zaWK4q@TJU63HNzB)2LDO^DlYFr&GDn%$z?g$$3#bc?7eBgiK67?OmBp-9$5Ycn_pg zqfK((Nmd8>`mfO;@+Wa8;8#gvoe)9JtUtV#VKCqkX8u9!voSya73ZxSM&%T5wnUZt zVRVMwf5j(I4#c>)Sp0;S$wDr+ivy~Np8q;v9#PA?jiEjQc;)b=BXX_y4humo(Lnle zs10Ap8H~Sr4-cb~hnA5JeZPmO00=_?@|^`D4^3s7P)Mjf6B+L1GAQF6kAI4S5RYlu zSm?LQxVYQky{<=y5LM&6#wvHTmA+>^9h$Bx2_-uhl|$-X$VKliZhXj%IT+>I##OoF z0rp-=S8?&i&7ro?5(cd+b0-L|u-@l0DBhDAPPCd$*-S2Zw5TEtE#nd#BV+kp)03v$ zR?3D75B~v%`2@Q{jet rS=B^Zi0{6Tc9zxOQUj8G=janXk|8of7I5XXaA=v`o;;^|SJdY`mVS@9Oq@j5Qv z!Nt`U5#X~rcRY1yr?=HY-AId{dU19c7IrKQkC%^n3-DvKhiylg7KpD~)T*Se{hR5+ z(f}K_5R&p+V6HuMwQp53$NM?13y^PIDlu)Uki4&EQj=NvDEkOD{3V-&T|M3tnN)^n zMfl=PHVO`4I(ge=g^|Iz1%4i3prPe>4!(Bw EyTZ;kFwTp&Uw%BBm4*++)N`f@y?7To2Z*fu&hpc zaq=d_6lmB8@?OdkR%F~d@vEMkbtMqeQFC{C-_N4Bq&!lVWkaW+I#>i?kQ!~Zb#4ta zRPQ~WMNOHMMha|yZp8?daVw+=ta`~NTo!%_BmI$S!dZMBwsPI#O~$W*B@jM;idpH5 zcFCqE)(M|!ybFg@!|2E}-j{|`x2wfAjrYQEDxEH4y{T3zCyQxf`E|K)s~}d6B`l(v zA1q;GumQ-B$3i2CzkVSf#c|$cRw^s%=D2HYt5_5#=?oS =7qiw>R7g`kxDPc^wjwPPy@0 zk%dN_+Hv?;Qzu-I4Q-QQ;&m$&y>yOC>lz!<-P~+NZ=UNh_Cy53n0!v2K)fQ8!%yHC zD7+drb3{a;8IwH$20bbW&j}m+S(uIWK(?5o6k-}sNDG`5U=(q}%VCAT1pXgmR{|GR z_5I(yGjE3FvEzt>I&VN3kWpr^1O=TLeTWJrsDz~@WM-x1R#{oNJ+h)QF|Vcd!=+U% zx5CN{(Px@yWabu>nw6Rho8~_M@0}5){`LR;=kq!9-n;MIeR=1ebM86Xn?}Az#PYVg zKWAhsg5PO)E8}tZ8o?3e6kW5C4+6~ur!jccn9&q-0gCz@=My6~)nL0ljIX+~&?tqM zB=XIgTGD7*CuaV{t!QNwgD)MW>^9oBgIg+TYmK|OF$T4lU+T=fu#g;@Y<(a(Z#h!q zOwx4F?fh-E5edGA@vhA?T6Xv6ufmZ(Ugd9UF4^wk8`c?(rpbS1e#bKbJB_Ekokw`C zF)&X8&wiI(?a5OshBV=8o?IzU35py6NNAUwpIx81`Q|A#A6L|O7HdsoVMxO@}VL zV^xvZn8Pjw2s*I5g0WG@I;1ul^Bu;$@Ph)LzSdym+U*83kX}Mh1Sxt6^KxcJ@Tiap zAxh1)jA1mtNDm{ zv(P#dYwx6M+qqak*qaoVKk7 wpD?Y z&v}8|XpH%`_FNb?&OXg$**!8tYAFhuXw;38+-b43%P+e2H)a8 |=%9WD0A zTQu&z0)%-w*fbgFS)E#9 @>p?%V&jtY=)lmeVVi2f#aC(*&HKw4;z1$t@utC8%D?j$B{HD_;3 z2(*+>R4zwC;?m$bHa_w|N3(a2a48Z5f1Quuul4c8BxN_30z4eZ&Z02%d?X7?CaHev zeS8Cu7r=6pNp!J#;u @~Eqbp567z3iKPHNoK-#hWSFwi1o z#byL!ST3=oO12QVxX@-AaTn~^8l&W2E6uQWKpb1pZtOertFQje%Jm6>n+d2X=AP?( zHj#HUUV|lGo>hq-vVxWg_|vJ5O|gS58NsBP^P5f=L4Yh+JJjqpBh9PV?5}_WBaSih z4T{o^uO^Do(w$okMNE35c%>kQ6S{3DD2gfP>h^P<)|-~HJKpk3Y^C|A9)l(qzKs=u zRyj2Z2GZ?ma%y@)IyA!V;+=tPyq&ZgApY#tu1kkmVNAO0z? =94%qGSXjkcVU=qyZsHdP~Si46Y^~(mRlS;2>t3W((jOnmim~i@*^! zOSMx> GCax2G7$_H#L^<)xrJ;mszPj*rgN@H+OKH-4s40ZP$X}nCW5=f2nIUCpYIvoUT3X zsh$sCO}1dAyAg{J)^;>VH=cYSZ}J!-nu{kNVuXMqxb?mj=V^rp@jZ<>Ch#%Ihosc; z8dT(ykCml`!NqLsZ1$fXBvP_$^cKP@6Z1y)RS#^%+VGGBd#XL!7GLJ-nxwuRxtng9 z+|?$&9a#t2MRf*5(5OqT$1RgQnTjLetjH$i1c_(--VKk>vU4(-jIBT_r9~yMyc3LZ zkM`GCB(EQflsXz9Gvpx)d5E$wvUTZ;!?*Y>>Wu|SFacF~0`M|`Yp;3*K{y_K32hJE z0jjn~Yru7=sRP7Q+8sumS>-)_hmn7if5y_mpWJ97@v;GC+3-Qm#)HZi6)X`>et*$m zhqF3K9BkyH^k8o979(K78^~}m;B7!T-~+%ez{h}30iOf50=@=(1K9Wqv)oR`76%%A zYOR63Nj%qxRwLqR!@|0rmhyRtGGuuEMoak|ey93@be@LU$*=Iqp&D@=jc0xxhGzq= zS$O8xbc_I1G}^a%4ZQX;7L_k~*gh(VYraLPAP+!YY%yAj 27 zz2c=c#ZJ7wzZf7nMR|xfOnuHYQC9wJ$O+~;+H$7niMutJ_W6ZCTGZT=80R2@?CuJB- zlYx?9dY2ahiKywu{9^`?j~wSQj-&SW;q)<;boAjUy^v0a%(h;f2eH>s(u}2UbX!TF zU?1bjMnI}JU|0!c<%Jxfr}sGvtGNAMo~UxW(KHJrYC0N0@fd*d4R3hy8^gODXy%Ka zOiU2aMdD6YlS q113v>N#WdOID4)~Iu68g&NNzSd rKg}hHI`KyX3w(ie$xxkF{Oo~lDMF%Ha`^%Ao8C!L790rZjrTq-9rAMI3hr`> zxj+kz%GJy4(HBUM;>$4>Xt##rq={t|oK=AG9HqC6646l|u3$ln;`AX_GbQ#47cfiP zwDXs9V0{B2=P%2@d6Ww=n1ParL+jz{dG9^kzahP5++%r!>3&Cbl7vl<(s)^N! u`36z2%_Xt*gz6??ex9U$AtJ>8F zP9xmvB{^%CZ_KD2{&Ip;cJE@ddy!FL%9V)LRx*@k*3gS|4^#g9Yj8cY_a;f9YEwjN z&@Gc1r@hGi)N_A9R!^W1e6xKp%r`RDD6%suQ+CFPp-!kw_z1Ec_?W|bZn@rnlKMvk z`?)uX8m5l)`ULdZ1=@yK3lQs{Vko~yc#Ts0=>h*8e=>Mh$GzfDq7<}B1N>1oIGw<5 z40}4A+%BJJsuYCurugM&Km&jYa-2U)P4TfAn?AAylOs*+yL8evLaPUxqv!m%aiBYG zAvr;#f>m}Uo%HwDJ`K2rEwu6}%fhk{to}waD0%@lYb1lp7H5#W?&@X?W>MpR@AAJo z$oWQVy>S|Ah{FHlME-A9^Y9;~