Compare commits

..

No commits in common. "main" and "4.1.1" have entirely different histories.
main ... 4.1.1

69 changed files with 2842 additions and 9546 deletions

View File

@ -4,9 +4,9 @@
"name": "Node.js & TypeScript",
"build": {
"dockerfile": "Dockerfile",
// Update 'VARIANT' to pick a Node version: 12, 14, 16, 18, 20
// Update 'VARIANT' to pick a Node version: 12, 14, 16
"args": {
"VARIANT": "20"
"VARIANT": "16"
}
},

View File

@ -1,11 +1,3 @@
categories:
- title: "Breaking Changes"
labels:
- "breaking change"
- title: "Dependencies"
collapse-after: 1
labels:
- "dependencies"
template: |
## What's Changed

View File

@ -14,13 +14,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install jq tool
run: |
sudo apt-get update
sudo apt-get install jq
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v2.4.1
with:
node-version: 16
- run: npm ci

View File

@ -11,12 +11,8 @@ jobs:
publish-npm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install jq tool
run: |
sudo apt-get update
sudo apt-get install jq
- uses: actions/setup-node@v4
- uses: actions/checkout@v2
- uses: actions/setup-node@v2.4.1
with:
node-version: 16
registry-url: https://registry.npmjs.org/

View File

@ -9,6 +9,6 @@ jobs:
update_release_draft:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6
- uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

201
LICENSE
View File

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,6 +1,6 @@
# ESP Web Tools
Allow flashing ESPHome or other ESP-based firmwares via the browser. Will automatically detect the board type and select a supported firmware. [See website for full documentation.](https://esphome.github.io/esp-web-tools/)
Allow flashing ESPHome or other ESP-based firmwares via the browser. Will automatically detect the board type and select a supported firmware.
```html
<esp-web-install-button
@ -8,63 +8,73 @@ Allow flashing ESPHome or other ESP-based firmwares via the browser. Will automa
></esp-web-install-button>
```
Example manifest:
Manifest definition:
```json
{
"name": "ESPHome",
"version": "2021.10.3",
"home_assistant_domain": "esphome",
"funding_url": "https://esphome.io/guides/supporters.html",
"builds": [
{
"chipFamily": "ESP32",
"parts": [
{ "path": "bootloader_dout_40m.bin", "offset": 4096 },
{ "path": "bootloader.bin", "offset": 4096 },
{ "path": "partitions.bin", "offset": 32768 },
{ "path": "boot_app0.bin", "offset": 57344 },
{ "path": "esp32.bin", "offset": 65536 }
{ "path": "ota.bin", "offset": 57344 },
{ "path": "firmware.bin", "offset": 65536 }
]
},
{
"chipFamily": "ESP32-C3",
"parts": [
{ "path": "bootloader_dout_40m.bin", "offset": 0 },
{ "path": "partitions.bin", "offset": 32768 },
{ "path": "boot_app0.bin", "offset": 57344 },
{ "path": "esp32-c3.bin", "offset": 65536 }
{ "path": "esp32-c3.bin", "offset": 0 },
]
},
{
"chipFamily": "ESP32-S2",
"parts": [
{ "path": "bootloader_dout_40m.bin", "offset": 4096 },
{ "path": "partitions.bin", "offset": 32768 },
{ "path": "boot_app0.bin", "offset": 57344 },
{ "path": "esp32-s2.bin", "offset": 65536 }
]
},
{
"chipFamily": "ESP32-S3",
"parts": [
{ "path": "bootloader_dout_40m.bin", "offset": 4096 },
{ "path": "partitions.bin", "offset": 32768 },
{ "path": "boot_app0.bin", "offset": 57344 },
{ "path": "esp32-s3.bin", "offset": 65536 }
{ "path": "esp32-s2.bin", "offset": 0 },
]
},
{
"chipFamily": "ESP8266",
"parts": [
{ "path": "esp8266.bin", "offset": 0 }
{ "path": "esp8266.bin", "offset": 0 },
]
}
]
}
```
## Styling
### Attributes
The following attributes are automatically added to `<esp-web-install-button>` and can be used for styling:
| Attribute | Description |
| -- | -- |
| `install-supported` | Added if installing firmware is supported
| `install-unsupported` | Added if installing firmware is not supported
### CSS custom properties (variables)
The following variables can be used to change the colors of the default UI elements:
- `--esp-tools-button-color`
- `--esp-tools-button-text-color`
### Slots
The following slots are available:
| Slot name | Description |
| -- | -- |
| `activate` | Button to start the flash progress
| `unsupported` | Message to show when the browser is not supported
| `not-allowed` | Message to show when not a secure context
## Development
Run `script/develop`. This starts a server. Open it on http://localhost:5001.
[![ESPHome - A project from the Open Home Foundation](https://www.openhomefoundation.org/badges/esphome.png)](https://www.openhomefoundation.org/)

View File

@ -1,4 +1,3 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
@ -34,7 +33,6 @@
name="twitter:image"
content="https://esphome.github.io/esp-web-tools/static/social.png"
/>
<meta name="color-scheme" content="dark light" />
<style>
body {
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI",
@ -48,60 +46,31 @@
margin: 0 auto;
padding: 12px;
}
h2 {
margin-top: 2em;
}
h3 {
margin-top: 1.5em;
}
.projects {
display: flex;
text-align: center;
flex-wrap: wrap;
gap: 24px;
justify-content: center;
}
.projects a {
color: initial;
text-decoration: none;
.project .logo {
float: right;
}
.project .logo img {
height: 50px;
}
.project .name {
margin-top: 8px;
}
a {
color: #03a9f4;
}
.screenshot {
text-align: center;
}
.screenshot img {
max-width: 100%;
box-shadow:
rgb(0 0 0 / 20%) 0px 2px 1px -1px,
rgb(0 0 0 / 14%) 0px 1px 1px 0px,
rgb(0 0 0 / 12%) 0px 1px 3px 0px;
border-radius: 4px;
}
.screenshot i {
margin-top: 4px;
display: block;
}
.videoWrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
height: 0;
margin-bottom: 25px;
background: #ccc;
background: #000;
}
.hidden {
display: none;
.videoWrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.content pre {
display: block;
padding-left: 8px;
max-width: 100%;
overflow-y: scroll;
}
.footer {
@ -131,16 +100,12 @@
}
}
</style>
<script
type="module"
src="https://unpkg.com/@justinribeiro/lite-youtube@1.4.0/lite-youtube.js"
></script>
<script module>
import(
// In development we import locally.
window.location.hostname === "localhost"
? "/dist/web/install-button.js"
: "https://unpkg.com/esp-web-tools/dist/web/install-button.js?module"
: "https://unpkg.com/esp-web-tools@4.1.0/dist/web/install-button.js?module"
);
</script>
</head>
@ -148,267 +113,136 @@
<div class="content">
<h1>ESP Web Tools</h1>
<p>
User friendly tools to manage ESP8266 and ESP32 devices in the browser:
ESP Web Tools is a set of open source tools to allow working with ESP
devices in the browser.
<a href="https://github.com/esphome/esp-web-tools"
>The code is available on GitHub.</a
>
</p>
<ul>
<li>Install &amp; update firmware</li>
<li>Connect device to the Wi-Fi network</li>
<li>Visit the device's hosted web interface</li>
<li>Access logs and send terminal commands</li>
<li>
Add devices to
<a href="https://www.home-assistant.io">Home Assistant</a>
</li>
</ul>
<div class="videoWrapper">
<lite-youtube
videoid="E8bdATqXM8c"
videotitle="ESP Web Tools in action"
></lite-youtube>
</div>
<h2 id="demo">Try a live demo</h2>
<p>
This demo will install
<a href="https://esphome.io">ESPHome</a>. To get started, connect an ESP
device to your computer and hit the button:
To try it out and install
<a href="https://esphome.io">ESPHome</a> on an ESP, connect it to your
computer and hit the button:
</p>
<esp-web-install-button
manifest="https://firmware.esphome.io/esp-web-tools/manifest.json"
>
<i slot="unsupported">
The demo is not available because your browser does not support Web
Serial. Open this page in Google Chrome or Microsoft Edge instead<span
class="not-supported-i hidden"
>
(but not on your iOS device)</span
>.
manifest="static/firmware_build/manifest.json"
></esp-web-install-button>
<p>
<i>
Note, this only works in desktop Chrome and Edge. Android support
should be possible but has not been implemented yet. If you don't see
your ESP device, you might miss <a href="#drivers">drivers</a>.
</i>
</esp-web-install-button>
<h2 id="used-by">Products using ESP Web Tools</h2>
<div class="projects">
<a href="https://install.wled.me" target="_blank" class="project">
<div class="logo">
<img src="static/logos/wled.png" alt="WLED logo" />
</div>
<div class="name">WLED</div>
</a>
<a
href="https://arendst.github.io/Tasmota-firmware/"
target="_blank"
class="project"
>
<div class="logo">
<img src="static/logos/tasmota.svg" alt="Tasmota logo" />
</div>
<div class="name">Tasmota</div>
</a>
<a href="https://td-er.nl/ESPEasy/" target="_blank" class="project">
<div class="logo">
<img src="static/logos/espeasy.png" alt="ESPEasy logo" />
</div>
<div class="name">ESPEasy</div>
</a>
<a
href="https://canair.io/installer.html"
target="_blank"
class="project"
>
<div class="logo">
<img src="static/logos/canairio.png" alt="CanAirIO logo" />
</div>
<div class="name">CanAirIO</div>
</a>
<a href="https://web.esphome.io" target="_blank" class="project">
<div class="logo">
<img src="static/logos/esphome.svg" alt="ESPHome logo" />
</div>
<div class="name">ESPHome</div>
</a>
<a
href="https://sle118.github.io/squeezelite-esp32-installer/"
target="_blank"
class="project"
>
<div class="logo">
<img
src="static/logos/squeezelite-esp32.png"
alt="Squeezelite-ESP32 logo"
/>
</div>
<div class="name">Squeezelite-ESP32</div>
</a>
<a
href="https://2smart.com/docs-resources/platform-updates/platform-updates-13-07-2022"
target="_blank"
class="project"
>
<div class="logo">
<img src="static/logos/2smart.png" alt="2Smart logo" />
</div>
<div class="name">2Smart</div>
</a>
<a href="https://clockwise.page" target="_blank" class="project">
<div class="logo">
<img src="static/logos/clockwise.png" alt="Clockwise logo" />
</div>
<div class="name">Clockwise</div>
</a>
<a
href="https://sblantipodi.github.io/glow_worm_luciferin"
target="_blank"
class="project"
>
<div class="logo">
<img
src="static/logos/luciferin_logo.png"
alt="Firefly Luciferin logo"
/>
</div>
<div class="name">Luciferin</div>
</a>
<a
href="https://install.openepaperlink.de"
target="_blank"
class="project"
>
<div class="logo">
<img
src="static/logos/openepaperlink.png"
alt="OpenEpaperLink logo"
/>
</div>
<div class="name">OpenEpaperLink</div>
</a>
<a href="https://openspool.io" target="_blank" class="project">
<div class="logo">
<img src="static/logos/openspool.png" alt="OpenSpool logo" />
</div>
<div class="name">OpenSpool</div>
</a>
<a href="https://usetrmnl.com/flash" target="_blank" class="project">
<div class="logo">
<img src="static/logos/trmnl.png" alt="TRMNL logo" />
</div>
<div class="name">TRMNL</div>
</a>
<a href="https://nspanelmanager.com" target="_blank" class="project">
<div class="logo">
<img src="static/logos/nspanelmanager.svg" alt="NSPanelManager logo" />
</div>
<div class="name">NSPanel Manager</div>
</a>
</div>
<h2>How it works</h2>
</p>
<p>
ESP Web Tools works by combining
<a href="https://developer.mozilla.org/docs/Web/API/Web_Serial_API"
This works by combining
<a
href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API"
>Web Serial</a
>, <a href="https://www.improv-wifi.com/">Improv Wi-Fi</a> (optional),
and a manifest which describes the firmware. ESP Web Tools detects the
chipset of the connected ESP device and automatically selects the right
firmware variant from the manifest.
>
with a manifest which describes the firmware. It will automatically
detect the type of the connected ESP device and find the right firmware
files in the manifest.
</p>
<p>
Web Serial is available in Google Chrome and Microsoft Edge
browsers<span class="not-supported-i hidden">
(but not on your iOS device)</span
>. Android support should be possible but has not been implemented yet.
</p>
<h3 id="improv">Configuring Wi-Fi</h3>
<p>
ESP Web Tools supports the
<a href="https://www.improv-wifi.com/serial"
>Improv Wi-Fi serial standard</a
>. This is an open standard to allow configuring Wi-Fi via the serial
port.
</p>
<p>
If the firmware supports Improv, a user will be asked to connect the
device to the network after installing the firmware. Once connected, the
device can send the user to a URL to finish configuration. For example,
this can be a link to the device's IP address where it serves a local
UI.
</p>
<p>
At any time in the future a user can use ESP Web Tools to find the
device link or to reconfigure the Wi-Fi settings without doing a
reinstall.
</p>
<p class="screenshot">
<img
src="./static/screenshots/dashboard.png"
alt="Screenshot showing ESP Web Tools dialog offering visting the device, adding it to Home Assistant, change Wi-Fi, show logs and console and reset data."
/>
<i>Screenshot showing the ESP Web Tools interface</i>
</p>
<h3 id="logs">Viewing logs & sending commands</h3>
<p>
ESP Web Tools allows users to open a serial console to see the logs and
send commands.
</p>
<p class="screenshot">
<img
src="./static/screenshots/logs.png"
alt="Screenshot showing ESP Web Tools dialog with a console showing ESPHome logs and a terminal prompt to sent commands."
/>
<i>Screenshot showing the ESP Web Tools logs & console</i>
</p>
<div class="videoWrapper">
<iframe
width="560"
height="315"
src="https://www.youtube-nocookie.com/embed/k88BS8zgWq0"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</div>
<h2 id="used-by">Products using ESP Web Tools</h2>
<div class="project">
<a href="https://wled.me" class="logo"
><img src="static/logos/wled.png" alt="WLED logo"
/></a>
<h3>WLED</h3>
<p>
Fast and feature-rich firmware to control NeoPixel (WS2812B, WS2811,
SK6812) LEDs and SPI based chipsets like the WS2801 and APA102.
</p>
<p>
<a href="https://install.wled.me" target="_blank"
>Installation Website</a
>
</p>
</div>
<div class="project">
<a href="https://tasmota.github.io" class="logo"
><img src="static/logos/tasmota.svg" alt="Tasmota logo"
/></a>
<h3>Tasmota</h3>
<p>
Firmware with easy configuration using webUI, OTA updates, automation
using timers or rules, expandability and entirely local control over
MQTT, HTTP, Serial or KNX.
</p>
<p>
<a href="https://arendst.github.io/Tasmota-firmware/" target="_blank"
>Installation Website</a
>
</p>
</div>
<div class="project">
<a href="http://www.espeasy.com/" class="logo"
><img src="static/logos/espeasy.png" alt="ESPEasy logo"
/></a>
<h3>ESPEasy</h3>
<p>Easy MultiSensor device based on ESP8266/ESP32.</p>
<p>
<a href="https://td-er.nl/ESPEasy/" target="_blank"
>Installation Website</a
>
</p>
</div>
<div class="project">
<a href="https://esphome.io" class="logo"
><img src="static/logos/esphome.svg" alt="ESPHome logo"
/></a>
<h3>ESPHome</h3>
<p>
No-code platform for ESP devices. Uses ESP Web Tools in their
dashboard to install ESPHome on devices.
</p>
<p>
<a href="https://esphome.io" target="_blank">Website</a>
</p>
</div>
<h2 id="add-website">Adding ESP Web Tools to your website</h2>
<p>
To add this to your own website, you need to include the ESP Web Tools
JavaScript files on your website, create a manifest file and add the ESP
Web Tools button HTML.
To add this to your own website, create a manifest and add the button to
your website. Make sure you update the manifest attribute to point at
your manifest.
</p>
<p>
<a href="https://github.com/balloob/squeezelite-esp32-install"
>Click here to see a full example.</a
You can import ESP Web Tools directly from the unpkg CDN or
<a href="https://unpkg.com/browse/esp-web-tools/dist/web/"
>download the files</a
>
</p>
<p>
<b>Step 1:</b> Load ESP Web Tools JavaScript on your website by adding
the following HTML snippet.
and put them on your website.
</p>
<pre>
&lt;script
type="module"
src="https://unpkg.com/esp-web-tools@10/dist/web/install-button.js?module"
>&lt;/script></pre
>
<p>
(If you prefer to locally host the JavaScript,
<a href="https://unpkg.com/browse/esp-web-tools/dist/web/"
>download it here</a
>)
</p>
<p>
<b>Step 2:</b> Find a place on your page where you want the button to
appear and include the following bit of HTML. Update the
<code>manifest</code> attribute to point at your manifest file.
</p>
<pre>
src="https://unpkg.com/esp-web-tools@4.1.0/dist/web/install-button.js?module"
>&lt;/script>
&lt;esp-web-install-button
manifest="https://firmware.esphome.io/esp-web-tools/manifest.json"
manifest="static/firmware_build/manifest.json"
>&lt;/esp-web-install-button></pre
>
<p>
<b>Note:</b> ESP Web Tools requires that your website is served over
<code>https://</code> to work. This is a Web Serial security
requirement.
</p>
<p>
If your manifest or the firmware files are hosted on another server,
make sure you configure
<a href="https://developer.mozilla.org/docs/Web/HTTP/CORS"
Your website needs to be served over <code>https://</code>. If your
manifest is hosted on another server, make sure you configure
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS"
>the CORS-headers</a
>
such that your website is allowed to fetch those files by adding the
header
for your manifest and firmware files such that your website is allowed
to fetch those files by adding the header
<code
>Access-Control-Allow-Origin: https://domain-of-your-website.com</code
>.
@ -416,66 +250,32 @@
<p>
ESP Web Tools can also be integrated in your projects by installing it
<a href="https://www.npmjs.com/package/esp-web-tools">via NPM</a>.
</p>
<h3 id="preparing-firmware">Preparing your firmware</h3>
<p>
If you have ESP32 firmware and are using ESP-IDF framework v4 or later,
you will need to create a merged version of your firmware before being
able to use it with ESP Web Tools. If you use ESP8266 or ESP32 with
ESP-IDF v3 or earlier, you can skip this section.
</p>
<p>
ESP32 firmware is split into 4 different files. When these files are
installed using the command-line tool <code>esptool</code>, it will
patch flash frequency, flash size and flash mode to match the target
device. ESP Web Tools is not able to do this on the fly, so you will
need to use <code>esptool</code> to create the single binary file and
use that with ESP Web Tools.
</p>
<p>
Create a single binary using <code>esptool</code> with the following
command:
</p>
<pre>
esptool --chip esp32 merge_bin \
-o merged-firmware.bin \
--flash_mode dio \
--flash_freq 40m \
--flash_size 4MB \
0x1000 bootloader.bin \
0x8000 partitions.bin \
0xe000 boot.bin \
0x10000 your_app.bin</pre
>
<p>
If your memory type is <code>opi_opi</code> or <code>opi_qspi</code>,
set your flash mode to be <code>dout</code>. Else, if your flash mode is
<code>qio</code> or <code>qout</code>, override your flash mode to be
<code>dio</code>.
via NPM:<br />
<code>npm install --save esp-web-tools</code>
</p>
<h3 id="manifest">Creating your manifest</h3>
<p>
Manifests describe the firmware that you want to offer the user to
install. It allows specifying different builds for the different types
of ESP devices. Current supported chip families are
<code>ESP8266</code>, <code>ESP32</code>, <code>ESP32-C2</code>,
<code>ESP32-C3</code>, <code>ESP32-C6</code>, <code>ESP32-H2</code>,
<code>ESP32-S2</code> and <code>ESP32-S3</code>. The correct build will
be automatically selected based on the type of the connected ESP device.
ESP Web Tools manifest describe the firmware that you want to install.
It allows specifying different builds for the different types of ESP
devices. Current supported chip families are <code>ESP8266</code>,
<code>ESP32</code>, <code>ESP32C3</code> and <code>ESP32S2</code>. The
correct build will be automatically selected based on the type of the
ESP device we detect via the serial port.
</p>
<pre>
{
"name": "ESPHome",
"version": "2021.11.0",
"home_assistant_domain": "esphome",
"funding_url": "https://esphome.io/guides/supporters.html",
"new_install_prompt_erase": false,
"new_install_skip_erase": false,
"builds": [
{
"chipFamily": "ESP32",
"parts": [
{ "path": "merged-firmware.bin", "offset": 0 },
{ "path": "bootloader.bin", "offset": 4096 },
{ "path": "partitions.bin", "offset": 32768 },
{ "path": "ota.bin", "offset": 57344 },
{ "path": "firmware.bin", "offset": 65536 }
]
},
{
@ -488,7 +288,7 @@ esptool --chip esp32 merge_bin \
}</pre
>
<p>
Each build contains a list of parts to be installed to the ESP device.
Each build contains a list of parts to be flashed to the ESP device.
Each part consists of a path to the file and an offset on the flash
where it should be installed. Part paths are resolved relative to the
path of the manifest, but can also be URLs to other hosts.
@ -499,54 +299,34 @@ esptool --chip esp32 merge_bin \
Tools will link the user to add this device to Home Assistant.
</p>
<p>
By default a new installation will erase all data before installation.
If you want to leave this choice to the user, set the optional manifest
key
<code>new_install_prompt_erase</code> to <code>true</code>. ESP Web
Tools offers users a new installation if it is unable to detect the
current firmware of the device (via Improv Serial) or if the detected
firmware does not match the name specififed in the manifest.
By default a new installation will erase the entire flash. If you want
to skip this step, set the optional key
<code>new_install_skip_erase</code> to <code>true</code>. ESP Web Tools
considers it a new installation if it is unable to detect the current
firmware of the device (via Improv Serial) or if the detected firmware
does not match the name specififed in the manifest.
</p>
<h3 id="improv">Wi-Fi provisioning</h3>
<p>
ESP Web Tools has support for the
<a href="https://www.improv-wifi.com/serial"
>Improv Wi-Fi serial standard</a
>. This is an open standard to allow configuring Wi-Fi via the serial
port.
</p>
<p>
When a firmware is first installed on a device, it might need to do some
time consuming tasks like initializing the file system. By default ESP
Web Tools will wait 10 seconds to receive an Improv Serial response to
indicate that the boot is completed. You can increase this timeout by
setting the optional manifest key
<code>new_install_improv_wait_time</code> to the number of seconds to
wait. Set to <code>0</code> to disable Improv Serial detection.
If Improv is supported, a user will be guided to connect the device to
the network after installation. It also allows the user to connect
already installed devices and re-configure the wireless network
settings.
</p>
<p>
If your product accepts donations you can add
<code>funding_url</code> to your manifest. This allows you to link to
your page explaining the user how they can fund development. This link
is visible in the ESP Web Tools menu when connected to a device running
your firmware (as detected via Improv).
</p>
<p>
ESP Web Tools allows you to provide your own check if the device is
running the same firmware as specified in the manifest. This check can
be setting the <code>overrides</code> property on
<code>&lt;esp-web-install-button&gt;</code>. The value is an object
containing a
<code>checkSameFirmware(manifest, improvInfo)</code> function. The
<code>manifest</code> parameter is your manifest and
<code>improvInfo</code> is the information returned from Improv:
<code>{ name, firmware, version, chipFamily }</code>. This check is only
called if the device firmware was detected via Improv.
</p>
<pre>
const button = document.querySelector('esp-web-install-button');
button.overrides = {
checkSameFirmware(manifest, improvInfo) {
const manifestFirmware = manifest.name.toLowerCase();
const deviceFirmware = improvInfo.firmware.toLowerCase();
return manifestFirmware.includes(deviceFirmware);
}
};</pre
>
<h3 id="customize">Customizing the look and feel</h3>
<p>
There are multiple options to change the look and feel of the button and
other elements.
</p>
<h4>Change colors</h4>
<p>
You can change the colors of the default UI elements with CSS custom
properties (variables), the following variables are available:
@ -554,7 +334,6 @@ button.overrides = {
<ul>
<li><code>--esp-tools-button-color</code></li>
<li><code>--esp-tools-button-text-color</code></li>
<li><code>--esp-tools-button-border-radius</code></li>
</ul>
<p>There are also some attributes that can be used for styling:</p>
<table>
@ -578,7 +357,7 @@ button.overrides = {
</p>
<pre>
&lt;esp-web-install-button
manifest="https://firmware.esphome.io/esp-web-tools/manifest.json"
manifest="static/firmware_build/manifest.json"
>
&lt;button slot="activate">Custom install button&lt;/button>
&lt;span slot="unsupported">Ah snap, your browser doesn't work!&lt;/span>
@ -586,35 +365,40 @@ button.overrides = {
&lt;/esp-web-install-button>
</pre
>
<h2>Why we created ESP Web Tools</h2>
<div class="videoWrapper">
<lite-youtube
videoid="6ZMXE5PXPqU"
videotitle="Why we created ESP Web Tools"
videoStartAt="1255"
></lite-youtube>
</div>
<h3 id="drivers">USB Serial Drivers</h3>
<p>
If the serial port is not showing up, your computer might be missing the
drivers for the USB serial chip used in your ESP device. These drivers
work for most ESP devices:
</p>
<ul>
<li>
CP2102 (square chip):
<a
href="https://www.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers"
>driver</a
>
</li>
<li>
CH341:
<a
href="https://github.com/nodemcu/nodemcu-devkit/tree/master/Drivers"
>driver</a
>
</li>
</ul>
<div class="footer">
<div>
ESP Web Tools
<a href="https://github.com/esphome/esp-web-tools">GitHub</a>
</div>
<div class="initiative">
ESP Web Tools is a project by
<a href="https://esphome.io">ESPHome</a>,
<a href="https://www.openhomefoundation.org">Open Home Foundation</a
>.<br />
<a href="https://esphome.io">ESPHome</a>.<br />
Development is funded by
<a href="https://www.nabucasa.com">Nabu Casa</a>.
</div>
<div>
ESP Web Tools is
<a href="https://github.com/esphome/esp-web-tools">open source</a>.
</div>
</div>
</div>
<script>
if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
document.querySelector(".not-supported-i").classList.remove("hidden");
}
</script>
</body>
</html>

8754
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +1,36 @@
{
"name": "esp-web-tools",
"version": "10.1.0",
"version": "4.1.1",
"description": "Web tools for ESP devices",
"main": "dist/install-button.js",
"repository": "https://github.com/esphome/esp-web-tools",
"repository": "https://github.com/esphome/web",
"author": "ESPHome maintainers",
"license": "Apache-2.0",
"scripts": {
"prepublishOnly": "script/build"
},
"devDependencies": {
"@babel/preset-env": "^7.26.0",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^28.0.2",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.2",
"@types/w3c-web-serial": "^1.0.7",
"prettier": "^3.4.2",
"rollup": "^4.29.1",
"serve": "^14.2.4",
"typescript": "^5.7.2"
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-typescript": "^8.2.1",
"@types/w3c-web-serial": "^1.0.1",
"prettier": "^2.3.0",
"rollup": "^2.50.2",
"rollup-plugin-terser": "^7.0.2",
"serve": "^13.0.2",
"typescript": "^4.3.2"
},
"dependencies": {
"@material/web": "^2.2.0",
"esptool-js": "^0.5.3",
"improv-wifi-serial-sdk": "^2.5.0",
"lit": "^3.2.1",
"pako": "^2.1.0",
"tslib": "^2.8.1"
"@material/mwc-button": "^0.25.3",
"@material/mwc-checkbox": "^0.25.3",
"@material/mwc-circular-progress": "^0.25.3",
"@material/mwc-dialog": "^0.25.3",
"@material/mwc-icon-button": "^0.25.3",
"@material/mwc-linear-progress": "^0.25.1",
"@material/mwc-textfield": "^0.25.3",
"esp-web-flasher": "^4.0.0",
"improv-wifi-serial-sdk": "^1.0.1",
"lit": "^2.0.0",
"tslib": "^2.3.1"
}
}

28
rollup.config.js Normal file
View File

@ -0,0 +1,28 @@
import { nodeResolve } from "@rollup/plugin-node-resolve";
import json from "@rollup/plugin-json";
import { terser } from "rollup-plugin-terser";
const config = {
input: "dist/install-button.js",
output: {
dir: "dist/web",
format: "module",
},
external: ["https://www.improv-wifi.com/sdk-js/launch-button.js"],
preserveEntrySignatures: false,
plugins: [nodeResolve(), json()],
};
if (process.env.NODE_ENV === "production") {
config.plugins.push(
terser({
ecma: 2019,
toplevel: true,
output: {
comments: false,
},
})
);
}
export default config;

View File

@ -1,51 +0,0 @@
import nodeResolve from "@rollup/plugin-node-resolve";
import json from "@rollup/plugin-json";
import terser from "@rollup/plugin-terser";
import babel from "@rollup/plugin-babel";
import commonjs from "@rollup/plugin-commonjs";
const config = {
input: "dist/install-button.js",
output: {
dir: "dist/web",
format: "module",
},
external: ["https://www.improv-wifi.com/sdk-js/launch-button.js"],
preserveEntrySignatures: false,
plugins: [
commonjs(),
nodeResolve({
browser: true,
preferBuiltins: false,
}),
babel({
babelHelpers: "bundled",
presets: [
[
"@babel/preset-env",
{
targets: {
// We use unpkg as CDN and it doesn't bundle modern syntax
chrome: "84",
},
},
],
],
}),
json(),
],
};
if (process.env.NODE_ENV === "production") {
config.plugins.push(
terser({
ecma: 2019,
toplevel: true,
format: {
comments: false,
},
})
);
}
export default config;

View File

@ -2,7 +2,6 @@
set -e
cd "$(dirname "$0")/.."
echo 'export const version =' `jq .version package.json`";" > src/version.ts
rm -rf dist
NODE_ENV=production npm exec -- tsc

View File

@ -1,10 +1,6 @@
# Stop on errors
set -e
if [ -z "$PORT" ]; then
PORT=5001
fi
cd "$(dirname "$0")/.."
rm -rf dist
@ -13,8 +9,9 @@ rm -rf dist
trap "kill 0" EXIT
# Run tsc once as rollup expects those files
npm exec -- tsc || true
tsc || true
npm exec -- serve -p "$PORT" &
npm exec -- serve -p 5001 &
npm exec -- tsc --watch &
npm exec -- rollup -c --watch
npm exec -- rollup -c --watch &
wait

View File

@ -1,14 +0,0 @@
import { Checkbox } from "@material/web/checkbox/internal/checkbox.js";
import { styles } from "@material/web/checkbox/internal/checkbox-styles.js";
declare global {
interface HTMLElementTagNameMap {
"ew-checkbox": EwCheckbox;
}
}
export class EwCheckbox extends Checkbox {
static override styles = [styles];
}
customElements.define("ew-checkbox", EwCheckbox);

View File

@ -1,14 +0,0 @@
import { CircularProgress } from "@material/web/progress/internal/circular-progress.js";
import { styles } from "@material/web/progress/internal/circular-progress-styles.js";
declare global {
interface HTMLElementTagNameMap {
"ew-circular-progress": EwCircularProgress;
}
}
export class EwCircularProgress extends CircularProgress {
static override styles = [styles];
}
customElements.define("ew-circular-progress", EwCircularProgress);

View File

@ -1,14 +0,0 @@
import { Dialog } from "@material/web/dialog/internal/dialog.js";
import { styles } from "@material/web/dialog/internal/dialog-styles.js";
declare global {
interface HTMLElementTagNameMap {
"ew-dialog": EwDialog;
}
}
export class EwDialog extends Dialog {
static override styles = [styles];
}
customElements.define("ew-dialog", EwDialog);

View File

@ -1,14 +0,0 @@
import { Divider } from "@material/web/divider/internal/divider.js";
import { styles } from "@material/web/divider/internal/divider-styles.js";
declare global {
interface HTMLElementTagNameMap {
"ew-divider": EwDivider;
}
}
export class EwDivider extends Divider {
static override styles = [styles];
}
customElements.define("ew-divider", EwDivider);

View File

@ -1,15 +0,0 @@
import { FilledSelect } from "@material/web/select/internal/filled-select.js";
import { styles } from "@material/web/select/internal/filled-select-styles.js";
import { styles as sharedStyles } from "@material/web/select/internal/shared-styles.js";
declare global {
interface HTMLElementTagNameMap {
"ew-filled-select": EwFilledSelect;
}
}
export class EwFilledSelect extends FilledSelect {
static override styles = [sharedStyles, styles];
}
customElements.define("ew-filled-select", EwFilledSelect);

View File

@ -1,17 +0,0 @@
import { styles as filledStyles } from "@material/web/textfield/internal/filled-styles.js";
import { FilledTextField } from "@material/web/textfield/internal/filled-text-field.js";
import { styles as sharedStyles } from "@material/web/textfield/internal/shared-styles.js";
import { literal } from "lit/static-html.js";
declare global {
interface HTMLElementTagNameMap {
"ew-filled-text-field": EwFilledTextField;
}
}
export class EwFilledTextField extends FilledTextField {
static override styles = [sharedStyles, filledStyles];
protected override readonly fieldTag = literal`md-filled-field`;
}
customElements.define("ew-filled-text-field", EwFilledTextField);

View File

@ -1,15 +0,0 @@
import { IconButton } from "@material/web/iconbutton/internal/icon-button.js";
import { styles as sharedStyles } from "@material/web/iconbutton/internal/shared-styles.js";
import { styles } from "@material/web/iconbutton/internal/standard-styles.js";
declare global {
interface HTMLElementTagNameMap {
"ew-icon-button": EwIconButton;
}
}
export class EwIconButton extends IconButton {
static override styles = [sharedStyles, styles];
}
customElements.define("ew-icon-button", EwIconButton);

View File

@ -1,14 +0,0 @@
import { ListItemEl as ListItem } from "@material/web/list/internal/listitem/list-item.js";
import { styles } from "@material/web/list/internal/listitem/list-item-styles.js";
declare global {
interface HTMLElementTagNameMap {
"ew-list-item": EwListItem;
}
}
export class EwListItem extends ListItem {
static override styles = [styles];
}
customElements.define("ew-list-item", EwListItem);

View File

@ -1,14 +0,0 @@
import { List } from "@material/web/list/internal/list.js";
import { styles } from "@material/web/list/internal/list-styles.js";
declare global {
interface HTMLElementTagNameMap {
"ew-list": EwList;
}
}
export class EwList extends List {
static override styles = [styles];
}
customElements.define("ew-list", EwList);

View File

@ -1,14 +0,0 @@
import { styles } from "@material/web/menu/internal/menuitem/menu-item-styles.js";
import { SelectOptionEl } from "@material/web/select/internal/selectoption/select-option.js";
declare global {
interface HTMLElementTagNameMap {
"ew-select-option": EwSelectOption;
}
}
export class EwSelectOption extends SelectOptionEl {
static override styles = [styles];
}
customElements.define("ew-select-option", EwSelectOption);

View File

@ -1,15 +0,0 @@
import { styles as sharedStyles } from "@material/web/button/internal/shared-styles.js";
import { TextButton } from "@material/web/button/internal/text-button.js";
import { styles as textStyles } from "@material/web/button/internal/text-styles.js";
declare global {
interface HTMLElementTagNameMap {
"ew-text-button": EwTextButton;
}
}
export class EwTextButton extends TextButton {
static override styles = [sharedStyles, textStyles];
}
customElements.define("ew-text-button", EwTextButton);

View File

@ -0,0 +1,22 @@
import { css } from "lit";
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,
css`
.mdc-button {
min-width: initial;
}
`,
];
}
customElements.define("ewt-button", EwtButton);

View File

@ -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);

View File

@ -1,21 +1,15 @@
import { ColoredConsole, coloredConsoleStyles } from "../util/console-color";
import { ColoredConsole } from "../util/console-color";
import { sleep } from "../util/sleep";
import { LineBreakTransformer } from "../util/line-break-transformer";
import { TimestampTransformer } from "../util/timestamp-transformer";
import { Logger } from "../const";
export class EwtConsole extends HTMLElement {
public port!: SerialPort;
public logger!: Logger;
public allowInput = true;
private _console?: ColoredConsole;
private _cancelConnection?: () => Promise<void>;
public logs(): string {
return this._console?.logs() || "";
}
public connectedCallback() {
if (this._console) {
return;
@ -30,8 +24,90 @@ export class EwtConsole extends HTMLElement {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
monospace;
line-height: 1.45;
display: flex;
flex-direction: column;
}
.log {
box-sizing: border-box;
height: calc(100% - 28px);
font-size: 12px;
padding: 16px;
overflow: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.log-bold {
font-weight: bold;
}
.log-italic {
font-style: italic;
}
.log-underline {
text-decoration: underline;
}
.log-strikethrough {
text-decoration: line-through;
}
.log-underline.log-strikethrough {
text-decoration: underline line-through;
}
.log-secret {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.log-secret-redacted {
opacity: 0;
width: 1px;
font-size: 1px;
}
.log-fg-black {
color: rgb(128, 128, 128);
}
.log-fg-red {
color: rgb(255, 0, 0);
}
.log-fg-green {
color: rgb(0, 255, 0);
}
.log-fg-yellow {
color: rgb(255, 255, 0);
}
.log-fg-blue {
color: rgb(0, 0, 255);
}
.log-fg-magenta {
color: rgb(255, 0, 255);
}
.log-fg-cyan {
color: rgb(0, 255, 255);
}
.log-fg-white {
color: rgb(187, 187, 187);
}
.log-bg-black {
background-color: rgb(0, 0, 0);
}
.log-bg-red {
background-color: rgb(255, 0, 0);
}
.log-bg-green {
background-color: rgb(0, 255, 0);
}
.log-bg-yellow {
background-color: rgb(255, 255, 0);
}
.log-bg-blue {
background-color: rgb(0, 0, 255);
}
.log-bg-magenta {
background-color: rgb(255, 0, 255);
}
.log-bg-cyan {
background-color: rgb(0, 255, 255);
}
.log-bg-white {
background-color: rgb(255, 255, 255);
}
form {
display: flex;
@ -45,40 +121,31 @@ export class EwtConsole extends HTMLElement {
border: 0;
outline: none;
}
${coloredConsoleStyles}
</style>
<div class="log"></div>
${
this.allowInput
? `<form>
>
<input autofocus>
</form>
`
: ""
}
<form>
>
<input autofocus>
</form>
`;
this._console = new ColoredConsole(this.shadowRoot!.querySelector("div")!);
const input = this.shadowRoot!.querySelector("input")!;
if (this.allowInput) {
const input = this.shadowRoot!.querySelector("input")!;
this.addEventListener("click", () => {
// Only focus input if user didn't select some text
if (getSelection()?.toString() === "") {
input.focus();
}
});
this.addEventListener("click", () => {
// Only focus input if user didn't select some text
if (getSelection()?.toString() === "") {
input.focus();
}
});
input.addEventListener("keydown", (ev) => {
if (ev.key === "Enter") {
ev.preventDefault();
ev.stopPropagation();
this._sendCommand();
}
});
}
input.addEventListener("keydown", (ev) => {
if (ev.key === "Enter") {
ev.preventDefault();
ev.stopPropagation();
this._sendCommand();
}
});
const abortController = new AbortController();
const connection = this._connect(abortController.signal);
@ -96,13 +163,12 @@ export class EwtConsole extends HTMLElement {
signal: abortSignal,
})
.pipeThrough(new TransformStream(new LineBreakTransformer()))
.pipeThrough(new TransformStream(new TimestampTransformer()))
.pipeTo(
new WritableStream({
write: (chunk) => {
this._console!.addLine(chunk.replace("\r", ""));
this._console!.addLine(chunk);
},
}),
})
);
if (!abortSignal.aborted) {
this._console!.addLine("");
@ -143,17 +209,15 @@ export class EwtConsole extends HTMLElement {
}
public async reset() {
this.logger.debug("Triggering reset");
this.logger.debug("Triggering reset.");
await this.port.setSignals({
dataTerminalReady: false,
requestToSend: true,
});
await sleep(250);
await this.port.setSignals({
dataTerminalReady: false,
requestToSend: false,
});
await sleep(250);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}

View File

@ -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);

View File

@ -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);

View File

@ -1,61 +0,0 @@
import { svg } from "lit";
export const closeIcon = svg`
<svg width="24" height="24" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
/>
</svg>
`;
export const refreshIcon = svg`
<svg viewBox="0 0 24 24">
<path
fill="currentColor"
d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"
/>
</svg>
`;
export const listItemInstallIcon = svg`
<svg slot="start" viewBox="0 0 24 24">
<path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" />
</svg>
`;
export const listItemWifi = svg`
<svg slot="start" viewBox="0 0 24 24">
<path d="M12,21L15.6,16.2C14.6,15.45 13.35,15 12,15C10.65,15 9.4,15.45 8.4,16.2L12,21M12,3C7.95,3 4.21,4.34 1.2,6.6L3,9C5.5,7.12 8.62,6 12,6C15.38,6 18.5,7.12 21,9L22.8,6.6C19.79,4.34 16.05,3 12,3M12,9C9.3,9 6.81,9.89 4.8,11.4L6.6,13.8C8.1,12.67 9.97,12 12,12C14.03,12 15.9,12.67 17.4,13.8L19.2,11.4C17.19,9.89 14.7,9 12,9Z" />
</svg>
`;
export const listItemConsole = svg`
<svg slot="start" viewBox="0 0 24 24">
<path d="M20,19V7H4V19H20M20,3A2,2 0 0,1 22,5V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V5C2,3.89 2.9,3 4,3H20M13,17V15H18V17H13M9.58,13L5.57,9H8.4L11.7,12.3C12.09,12.69 12.09,13.33 11.7,13.72L8.42,17H5.59L9.58,13Z" />
</svg>
`;
export const listItemVisitDevice = svg`
<svg slot="start" viewBox="0 0 24 24">
<path d="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</svg>
`;
export const listItemHomeAssistant = svg`
<svg slot="start" viewBox="0 0 24 24">
<path d="m12.151 1.5882c-.3262 0-.6523.1291-.8996.3867l-8.3848 8.7354c-.0619.0644-.1223.1368-.1807.2154-.0588.0789-.1151.1638-.1688.2534-.2593.4325-.4552.9749-.5232 1.4555-.0026.018-.0076.0369-.0094.0548-.0121.0987-.0184.1944-.0184.2857v8.0124a1.2731 1.2731 0 001.2731 1.2731h7.8313l-3.4484-3.593a1.7399 1.7399 0 111.0803-1.125l2.6847 2.7972v-10.248a1.7399 1.7399 0 111.5276-0v7.187l2.6702-2.782a1.7399 1.7399 0 111.0566 1.1505l-3.7269 3.8831v2.7299h8.174a1.2471 1.2471 0 001.2471-1.2471v-8.0375c0-.0912-.0059-.1868-.0184-.2855-.0603-.4935-.2636-1.0617-.5326-1.5105-.0537-.0896-.1101-.1745-.1684-.253-.0588-.079-.1191-.1513-.181-.2158l-8.3848-8.7363c-.2473-.2577-.5735-.3866-.8995-.3864" />
</svg>
`;
export const listItemEraseUserData = svg`
<svg slot="start" viewBox="0 0 24 24">
<path d="M15,14C17.67,14 23,15.33 23,18V20H7V18C7,15.33 12.33,14 15,14M15,12A4,4 0 0,1 11,8A4,4 0 0,1 15,4A4,4 0 0,1 19,8A4,4 0 0,1 15,12M5,9.59L7.12,7.46L8.54,8.88L6.41,11L8.54,13.12L7.12,14.54L5,12.41L2.88,14.54L1.46,13.12L3.59,11L1.46,8.88L2.88,7.46L5,9.59Z" />
</svg>
`;
export const listItemFundDevelopment = svg`
<svg slot="start" viewBox="0 0 24 24">
<path d="M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z" />
</svg>
`;

View File

@ -1,18 +1,12 @@
import type { InstallButton } from "./install-button.js";
import "./install-dialog.js";
export const connect = async (button: InstallButton) => {
import("./install-dialog.js");
let port: SerialPort | undefined;
try {
port = await navigator.serial.requestPort();
} catch (err: any) {
if ((err as DOMException).name === "NotFoundError") {
import("./no-port-picked/index").then((mod) =>
mod.openNoPortPickedDialog(() => connect(button)),
);
return;
}
alert(`Error: ${err.message}`);
} catch (err) {
console.error("User cancelled request", err);
return;
}
@ -20,23 +14,17 @@ export const connect = async (button: InstallButton) => {
return;
}
try {
await port.open({ baudRate: 115200 });
} catch (err: any) {
alert(err.message);
return;
}
await port.open({ baudRate: 115200 });
const el = document.createElement("ewt-install-dialog");
el.port = port;
el.manifestPath = button.manifest || button.getAttribute("manifest")!;
el.overrides = button.overrides;
el.addEventListener(
"closed",
() => {
port!.close();
},
{ once: true },
{ once: true }
);
document.body.appendChild(el);
};

View File

@ -5,15 +5,7 @@ export interface Logger {
}
export interface Build {
chipFamily:
| "ESP32"
| "ESP32-C2"
| "ESP32-C3"
| "ESP32-C6"
| "ESP32-H2"
| "ESP32-S2"
| "ESP32-S3"
| "ESP8266";
chipFamily: "ESP32" | "ESP8266" | "ESP32-S2" | "ESP32-C3";
parts: {
path: string;
offset: number;
@ -24,12 +16,7 @@ export interface Manifest {
name: string;
version: string;
home_assistant_domain?: string;
funding_url?: string;
/** @deprecated use `new_install_prompt_erase` instead */
new_install_skip_erase?: boolean;
new_install_prompt_erase?: boolean;
/* Time to wait to detect Improv Wi-Fi. Set to 0 to disable. */
new_install_improv_wait_time?: number;
builds: Build[];
}
@ -46,6 +33,11 @@ export interface InitializingState extends BaseFlashState {
details: { done: boolean };
}
export interface ManifestState extends BaseFlashState {
state: FlashStateType.MANIFEST;
details: { done: boolean };
}
export interface PreparingState extends BaseFlashState {
state: FlashStateType.PREPARING;
details: { done: boolean };
@ -72,6 +64,7 @@ export interface ErrorState extends BaseFlashState {
export type FlashState =
| InitializingState
| ManifestState
| PreparingState
| ErasingState
| WritingState
@ -80,6 +73,7 @@ export type FlashState =
export const enum FlashStateType {
INITIALIZING = "initializing",
MANIFEST = "manifest",
PREPARING = "preparing",
ERASING = "erasing",
WRITING = "writing",

View File

@ -1,4 +1,4 @@
import { Transport, ESPLoader } from "esptool-js";
import { ESPLoader, Logger } from "esp-web-flasher";
import {
Build,
FlashError,
@ -6,17 +6,19 @@ import {
Manifest,
FlashStateType,
} from "./const";
import { hardReset } from "./util/reset";
import { getChipFamilyName } from "./util/chip-family-name";
import { sleep } from "./util/sleep";
export const flash = async (
onEvent: (state: FlashState) => void,
port: SerialPort,
logger: Logger,
manifestPath: string,
manifest: Manifest,
eraseFirst: boolean,
eraseFirst: boolean
) => {
let manifest: Manifest;
let build: Build | undefined;
let chipFamily: Build["chipFamily"];
let chipFamily: ReturnType<typeof getChipFamilyName>;
const fireStateEvent = (stateUpdate: FlashState) =>
onEvent({
@ -26,13 +28,12 @@ export const flash = async (
chipFamily,
});
const transport = new Transport(port);
const esploader = new ESPLoader({
transport,
baudrate: 115200,
romBaudrate: 115200,
enableTracing: false,
});
const manifestURL = new URL(manifestPath, location.toString()).toString();
const manifestProm = fetch(manifestURL).then(
(resp): Promise<Manifest> => resp.json()
);
const esploader = new ESPLoader(port, logger);
// For debugging
(window as any).esploader = esploader;
@ -44,40 +45,61 @@ export const flash = async (
});
try {
await esploader.main();
await esploader.flashId();
await esploader.initialize();
} catch (err: any) {
console.error(err);
fireStateEvent({
state: FlashStateType.ERROR,
message:
"Failed to initialize. Try resetting your device or holding the BOOT button while clicking INSTALL.",
details: { error: FlashError.FAILED_INITIALIZING, details: err },
});
await hardReset(transport);
await transport.disconnect();
logger.error(err);
if (esploader.connected) {
fireStateEvent({
state: FlashStateType.ERROR,
message:
"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();
}
return;
}
chipFamily = esploader.chip.CHIP_NAME as any;
chipFamily = getChipFamilyName(esploader);
fireStateEvent({
state: FlashStateType.INITIALIZING,
message: `Initialized. Found ${chipFamily}`,
details: { done: true },
});
fireStateEvent({
state: FlashStateType.MANIFEST,
message: "Fetching manifest...",
details: { done: false },
});
try {
manifest = await manifestProm;
} catch (err: any) {
fireStateEvent({
state: FlashStateType.ERROR,
message: `Unable to fetch manifest: ${err}`,
details: { error: FlashError.FAILED_MANIFEST_FETCH, details: err },
});
await esploader.disconnect();
return;
}
build = manifest.builds.find((b) => b.chipFamily === chipFamily);
fireStateEvent({
state: FlashStateType.MANIFEST,
message: `Found manifest for ${manifest.name}`,
details: { done: true },
});
if (!build) {
fireStateEvent({
state: FlashStateType.ERROR,
message: `Your ${chipFamily} board is not supported.`,
details: { error: FlashError.NOT_SUPPORTED, details: chipFamily },
});
await hardReset(transport);
await transport.disconnect();
await esploader.disconnect();
return;
}
@ -87,33 +109,28 @@ export const flash = async (
details: { done: false },
});
const manifestURL = new URL(manifestPath, location.toString()).toString();
const filePromises = build.parts.map(async (part) => {
const url = new URL(part.path, manifestURL).toString();
const resp = await fetch(url);
if (!resp.ok) {
throw new Error(
`Downlading firmware ${part.path} failed: ${resp.status}`,
`Downlading firmware ${part.path} failed: ${resp.status}`
);
}
const reader = new FileReader();
const blob = await resp.blob();
return new Promise<string>((resolve) => {
reader.addEventListener("load", () => resolve(reader.result as string));
reader.readAsBinaryString(blob);
});
return resp.arrayBuffer();
});
const fileArray: Array<{ data: string; address: number }> = [];
// Run the stub while we wait for files to download
const espStub = await esploader.runStub();
const files: ArrayBuffer[] = [];
let totalSize = 0;
for (let part = 0; part < filePromises.length; part++) {
for (const prom of filePromises) {
try {
const data = await filePromises[part];
fileArray.push({ data, address: build.parts[part].offset });
totalSize += data.length;
const data = await prom;
files.push(data);
totalSize += data.byteLength;
} catch (err: any) {
fireStateEvent({
state: FlashStateType.ERROR,
@ -123,8 +140,7 @@ export const flash = async (
details: err.message,
},
});
await hardReset(transport);
await transport.disconnect();
await esploader.disconnect();
return;
}
}
@ -141,7 +157,7 @@ export const flash = async (
message: "Erasing device...",
details: { done: false },
});
await esploader.eraseFlash();
await espStub.eraseFlash();
fireStateEvent({
state: FlashStateType.ERASING,
message: "Device erased",
@ -149,61 +165,56 @@ export const flash = async (
});
}
let lastPct = 0;
fireStateEvent({
state: FlashStateType.WRITING,
message: `Writing progress: 0%`,
message: `Writing progress: ${lastPct}%`,
details: {
bytesTotal: totalSize,
bytesWritten: 0,
percentage: 0,
percentage: lastPct,
},
});
let totalWritten = 0;
try {
await esploader.writeFlash({
fileArray,
flashSize: "keep",
flashMode: "keep",
flashFreq: "keep",
eraseAll: false,
compress: true,
// report progress
reportProgress: (fileIndex: number, written: number, total: number) => {
const uncompressedWritten =
(written / total) * fileArray[fileIndex].data.length;
const newPct = Math.floor(
((totalWritten + uncompressedWritten) / totalSize) * 100,
);
// we're done with this file
if (written === total) {
totalWritten += uncompressedWritten;
return;
}
fireStateEvent({
state: FlashStateType.WRITING,
message: `Writing progress: ${newPct}%`,
details: {
bytesTotal: totalSize,
bytesWritten: totalWritten + written,
percentage: newPct,
},
});
},
});
} catch (err: any) {
fireStateEvent({
state: FlashStateType.ERROR,
message: err.message,
details: { error: FlashError.WRITE_FAILED, details: err },
});
await hardReset(transport);
await transport.disconnect();
return;
for (const part of build.parts) {
const file = files.shift()!;
try {
await espStub.flashData(
file,
(bytesWritten: number) => {
const newPct = Math.floor(
((totalWritten + bytesWritten) / totalSize) * 100
);
if (newPct === lastPct) {
return;
}
lastPct = newPct;
fireStateEvent({
state: FlashStateType.WRITING,
message: `Writing progress: ${newPct}%`,
details: {
bytesTotal: totalSize,
bytesWritten: totalWritten + bytesWritten,
percentage: newPct,
},
});
},
part.offset,
true
);
} catch (err: any) {
fireStateEvent({
state: FlashStateType.ERROR,
message: err.message,
details: { error: FlashError.WRITE_FAILED, details: err },
});
await esploader.disconnect();
return;
}
totalWritten += file.byteLength;
}
fireStateEvent({
@ -216,10 +227,11 @@ export const flash = async (
},
});
await hardReset(transport);
await sleep(100);
console.log("DISCONNECT");
await transport.disconnect();
await esploader.disconnect();
console.log("HARD RESET");
await esploader.hardReset();
fireStateEvent({
state: FlashStateType.FINISHED,

View File

@ -1,6 +1,4 @@
import type { FlashState } from "./const";
import type { EwtInstallDialog } from "./install-dialog";
import { connect } from "./connect";
import { FlashState } from "./const";
export class InstallButton extends HTMLElement {
public static isSupported = "serial" in navigator;
@ -12,12 +10,12 @@ export class InstallButton extends HTMLElement {
position: relative;
cursor: pointer;
font-size: 14px;
font-weight: 500;
padding: 10px 24px;
padding: 8px 28px;
color: var(--esp-tools-button-text-color, #fff);
background-color: var(--esp-tools-button-color, #03a9f4);
border: none;
border-radius: var(--esp-tools-button-border-radius, 9999px);
border-radius: 4px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.12), 0 1px 5px 0 rgba(0,0,0,.2);
}
button::before {
content: " ";
@ -27,7 +25,10 @@ export class InstallButton extends HTMLElement {
left: 0;
right: 0;
opacity: 0.2;
border-radius: var(--esp-tools-button-border-radius, 9999px);
border-radius: 4px;
}
button:hover {
box-shadow: 0 4px 8px 0 rgba(0,0,0,.14), 0 1px 7px 0 rgba(0,0,0,.12), 0 3px 1px -1px rgba(0,0,0,.2);
}
button:hover::before {
background-color: rgba(255,255,255,.8);
@ -48,6 +49,10 @@ export class InstallButton extends HTMLElement {
cursor: unset;
pointer-events: none;
}
improv-wifi-launch-button {
display: block;
margin-top: 16px;
}
.hidden {
display: none;
}`;
@ -66,7 +71,9 @@ export class InstallButton extends HTMLElement {
public renderRoot?: ShadowRoot;
public overrides: EwtInstallDialog["overrides"];
public static preload() {
import("./connect");
}
public connectedCallback() {
if (this.renderRoot) {
@ -85,23 +92,28 @@ export class InstallButton extends HTMLElement {
this.toggleAttribute("install-supported", true);
this.addEventListener("mouseover", InstallButton.preload);
const slot = document.createElement("slot");
slot.addEventListener("click", async (ev) => {
ev.preventDefault();
connect(this);
const mod = await import("./connect");
mod.connect(this);
});
slot.name = "activate";
const button = document.createElement("button");
button.innerText = "Connect";
button.innerText = "INSTALL";
slot.append(button);
if (
"adoptedStyleSheets" in Document.prototype &&
"replaceSync" in CSSStyleSheet.prototype
) {
const sheet = new CSSStyleSheet();
// @ts-expect-error
sheet.replaceSync(InstallButton.style);
// @ts-expect-error
this.renderRoot.adoptedStyleSheets = [sheet];
} else {
const styleSheet = document.createElement("style");

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +0,0 @@
import "./no-port-picked-dialog";
export const openNoPortPickedDialog = async (
doTryAgain?: () => void,
): Promise<boolean> => {
const dialog = document.createElement("ewt-no-port-picked-dialog");
dialog.doTryAgain = doTryAgain;
document.body.append(dialog);
return true;
};

View File

@ -1,176 +0,0 @@
import { LitElement, html, css, svg } from "lit";
import { customElement } from "lit/decorators.js";
import "../components/ew-dialog";
import "../components/ew-text-button";
import { dialogStyles } from "../styles";
import { getOperatingSystem } from "../util/get-operating-system";
const cloudDownload = svg`
<svg
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 510.322 510.322"
xml:space="preserve"
style="width: 28px; vertical-align: middle;"
>
<g>
<path
style="fill:currentColor;"
d="M429.064,159.505c0-0.151,0.086-1.057,0.086-1.057c0-75.282-61.261-136.521-136.543-136.521 c-52.244,0-97.867,30.587-120.753,76.339c-11.67-9.081-25.108-15.682-40.273-15.682c-37.166,0-67.387,30.199-67.387,67.387 c0,0,0.453,3.279,0.798,5.824C27.05,168.716,0,203.423,0,244.516c0,25.389,9.901,49.268,27.848,67.171 c17.968,17.99,41.804,27.869,67.193,27.869h130.244v46.83h-54.66l97.694,102.008l95.602-102.008h-54.66v-46.83H419.25 c50.174,0,91.072-40.855,91.072-90.986C510.3,201.827,474.428,164.639,429.064,159.505z M419.207,312.744H309.26v-55.545h-83.975 v55.545H95.019c-18.184,0-35.333-7.075-48.211-19.996c-12.878-12.878-19.953-30.005-19.953-48.189 c0-32.68,23.21-60.808,55.264-66.956l12.511-2.394l-2.092-14.431l-1.488-10.785c0-22.347,18.184-40.51,40.531-40.51 c13.266,0,25.691,6.514,33.305,17.408l15.229,21.873l8.52-25.303c15.013-44.652,56.796-74.656,103.906-74.656 c60.506,0,109.709,49.203,109.709,109.644l-1.337,25.712l15.121,0.302l3.149-0.086c35.419,0,64.216,28.797,64.216,64.216 C483.401,283.969,454.604,312.744,419.207,312.744z"
/>
</g>
</svg>
`;
@customElement("ewt-no-port-picked-dialog")
class EwtNoPortPickedDialog extends LitElement {
public doTryAgain?: () => void;
public render() {
const OS = getOperatingSystem();
return html`
<ew-dialog open @closed=${this._handleClose}>
<div slot="headline">No port selected</div>
<div slot="content">
<div>
If you didn't select a port because you didn't see your device
listed, try the following steps:
</div>
<ol>
<li>
Make sure that the device is connected to this computer (the one
that runs the browser that shows this website)
</li>
<li>
Most devices have a tiny light when it is powered on. If yours has
one, make sure it is on.
</li>
<li>
Make sure that the USB cable you use can be used for data and is
not a power-only cable.
</li>
${OS === "Linux"
? html`
<li>
If you are using a Linux flavor, make sure that your user is
part of the <code>dialout</code> group so it has permission
to access the device.
<code class="block"
>sudo usermod -a -G dialout YourUserName</code
>
You may need to log out & back in or reboot to activate the
new group access.
</li>
`
: ""}
<li>
Make sure you have the right drivers installed. Below are the
drivers for common chips used in ESP devices:
<ul>
<li>
CP2102 drivers:
<a
href="https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers"
target="_blank"
rel="noopener"
>Windows & Mac</a
>
</li>
<li>
CH342, CH343, CH9102 drivers:
<a
href="https://www.wch.cn/downloads/CH343SER_ZIP.html"
target="_blank"
rel="noopener"
>Windows</a
>,
<a
href="https://www.wch.cn/downloads/CH34XSER_MAC_ZIP.html"
target="_blank"
rel="noopener"
>Mac</a
>
<br />
(download via blue button with ${cloudDownload} icon)
</li>
<li>
CH340, CH341 drivers:
<a
href="https://www.wch.cn/downloads/CH341SER_ZIP.html"
target="_blank"
rel="noopener"
>Windows</a
>,
<a
href="https://www.wch.cn/downloads/CH341SER_MAC_ZIP.html"
target="_blank"
rel="noopener"
>Mac</a
>
<br />
(download via blue button with ${cloudDownload} icon)
</li>
</ul>
</li>
</ol>
</div>
<div slot="actions">
${this.doTryAgain
? html`
<ew-text-button @click=${this.close}>Cancel</ew-text-button>
<ew-text-button @click=${this.tryAgain}>
Try Again
</ew-text-button>
`
: html`
<ew-text-button @click=${this.close}>Close</ew-text-button>
`}
</div>
</ew-dialog>
`;
}
private tryAgain() {
this.close();
this.doTryAgain?.();
}
private close() {
this.shadowRoot!.querySelector("ew-dialog")!.close();
}
private async _handleClose() {
this.parentNode!.removeChild(this);
}
static styles = [
dialogStyles,
css`
li + li,
li > ul {
margin-top: 8px;
}
ul,
ol {
margin-bottom: 0;
padding-left: 1.5em;
}
li code.block {
display: block;
margin: 0.5em 0;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ewt-no-port-picked-dialog": EwtNoPortPickedDialog;
}
}

View File

@ -1,35 +0,0 @@
import { LitElement, html, css, TemplateResult } from "lit";
import { property } from "lit/decorators.js";
class EwtPageMessage extends LitElement {
@property() icon!: string;
@property() label!: string | TemplateResult;
render() {
return html`
<div class="icon">${this.icon}</div>
${this.label}
`;
}
static styles = css`
:host {
display: flex;
flex-direction: column;
text-align: center;
}
.icon {
font-size: 50px;
line-height: 80px;
color: black;
}
`;
}
customElements.define("ewt-page-message", EwtPageMessage);
declare global {
interface HTMLElementTagNameMap {
"ewt-page-message": EwtPageMessage;
}
}

View File

@ -1,43 +0,0 @@
import { LitElement, html, css, TemplateResult } from "lit";
import { property } from "lit/decorators.js";
import "../components/ew-circular-progress";
class EwtPageProgress extends LitElement {
@property() label!: string | TemplateResult;
@property() progress: number | undefined;
render() {
return html`
<div>
<ew-circular-progress
active
?indeterminate=${this.progress === undefined}
.value=${this.progress !== undefined
? this.progress / 100
: undefined}
></ew-circular-progress>
${this.progress !== undefined ? html`<div>${this.progress}%</div>` : ""}
</div>
${this.label}
`;
}
static styles = css`
:host {
display: flex;
flex-direction: column;
text-align: center;
}
ew-circular-progress {
margin-bottom: 16px;
}
`;
}
customElements.define("ewt-page-progress", EwtPageProgress);
declare global {
interface HTMLElementTagNameMap {
"ewt-page-progress": EwtPageProgress;
}
}

View File

@ -1,31 +0,0 @@
import { css } from "lit";
// We set font-size to 16px and all the mdc typography styles
// because it defaults to rem, which means that the font-size
// of the host website would influence the ESP Web Tools dialog.
export const dialogStyles = css`
:host {
--roboto-font: Roboto, system-ui;
--text-color: rgba(0, 0, 0, 0.6);
--danger-color: #db4437;
--md-sys-color-primary: #03a9f4;
--md-sys-color-on-primary: #fff;
--md-ref-typeface-brand: var(--roboto-font);
--md-ref-typeface-plain: var(--roboto-font);
--md-sys-color-surface: #fff;
--md-sys-color-surface-container: #fff;
--md-sys-color-surface-container-high: #fff;
--md-sys-color-surface-container-highest: #f5f5f5;
--md-sys-color-secondary-container: #e0e0e0;
--md-sys-typescale-headline-font: var(--roboto-font);
--md-sys-typescale-title-font: var(--roboto-font);
}
a {
color: var(--md-sys-color-primary);
}
`;

View File

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

View File

@ -5,7 +5,7 @@ interface ConsoleState {
strikethrough: boolean;
foregroundColor: string | null;
backgroundColor: string | null;
carriageReturn: boolean;
// carriageReturn: boolean;
secret: boolean;
}
@ -17,32 +17,28 @@ export class ColoredConsole {
strikethrough: false,
foregroundColor: null,
backgroundColor: null,
carriageReturn: false,
// carriageReturn: false,
secret: false,
};
constructor(public targetElement: HTMLElement) {}
logs(): string {
return this.targetElement.innerText;
}
addLine(line: string) {
// @ts-expect-error
const re = /(?:\033|\\033)(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
let i = 0;
if (this.state.carriageReturn) {
if (line !== "\n") {
// don't remove if \r\n
this.targetElement.removeChild(this.targetElement.lastChild!);
}
this.state.carriageReturn = false;
}
// 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;
}
// if (line.includes("\r")) {
// this.state.carriageReturn = true;
// }
const lineSpan = document.createElement("span");
lineSpan.classList.add("line");
@ -179,107 +175,14 @@ export class ColoredConsole {
}
}
}
const atBottom =
this.targetElement.scrollTop >
this.targetElement.scrollHeight - this.targetElement.offsetHeight - 50;
addSpan(line.substring(i));
// Keep scroll at bottom
if (atBottom) {
if (
this.targetElement.scrollTop + 56 >=
this.targetElement.scrollHeight - this.targetElement.offsetHeight
) {
// at bottom
this.targetElement.scrollTop = this.targetElement.scrollHeight;
}
}
}
export const coloredConsoleStyles = `
.log {
flex: 1;
background-color: #1c1c1c;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
monospace;
font-size: 12px;
padding: 16px;
overflow: auto;
line-height: 1.45;
border-radius: 3px;
white-space: pre-wrap;
overflow-wrap: break-word;
color: #ddd;
}
.log-bold {
font-weight: bold;
}
.log-italic {
font-style: italic;
}
.log-underline {
text-decoration: underline;
}
.log-strikethrough {
text-decoration: line-through;
}
.log-underline.log-strikethrough {
text-decoration: underline line-through;
}
.log-secret {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.log-secret-redacted {
opacity: 0;
width: 1px;
font-size: 1px;
}
.log-fg-black {
color: rgb(128, 128, 128);
}
.log-fg-red {
color: rgb(255, 0, 0);
}
.log-fg-green {
color: rgb(0, 255, 0);
}
.log-fg-yellow {
color: rgb(255, 255, 0);
}
.log-fg-blue {
color: rgb(0, 0, 255);
}
.log-fg-magenta {
color: rgb(255, 0, 255);
}
.log-fg-cyan {
color: rgb(0, 255, 255);
}
.log-fg-white {
color: rgb(187, 187, 187);
}
.log-bg-black {
background-color: rgb(0, 0, 0);
}
.log-bg-red {
background-color: rgb(255, 0, 0);
}
.log-bg-green {
background-color: rgb(0, 255, 0);
}
.log-bg-yellow {
background-color: rgb(255, 255, 0);
}
.log-bg-blue {
background-color: rgb(0, 0, 255);
}
.log-bg-magenta {
background-color: rgb(255, 0, 255);
}
.log-bg-cyan {
background-color: rgb(0, 255, 255);
}
.log-bg-white {
background-color: rgb(255, 255, 255);
}
`;

View File

@ -1,17 +0,0 @@
export const fileDownload = (href: string, filename = ""): void => {
const a = document.createElement("a");
a.target = "_blank";
a.href = href;
a.download = filename;
document.body.appendChild(a);
a.dispatchEvent(new MouseEvent("click"));
document.body.removeChild(a);
};
export const textDownload = (text: string, filename = ""): void => {
const blob = new Blob([text], { type: "text/plain" });
const url = URL.createObjectURL(blob);
fileDownload(url, filename);
setTimeout(() => URL.revokeObjectURL(url), 0);
};

View File

@ -7,7 +7,7 @@ export const fireEvent = <Event extends keyof HTMLElementEventMap>(
bubbles?: boolean;
cancelable?: boolean;
composed?: boolean;
},
}
): void => {
options = options || {};
const event = new CustomEvent(type, {

View File

@ -1,24 +0,0 @@
// From https://stackoverflow.com/a/38241481
export const getOperatingSystem = () => {
const userAgent = window.navigator.userAgent;
const platform =
// @ts-expect-error
window.navigator?.userAgentData?.platform || window.navigator.platform;
const macosPlatforms = ["macOS", "Macintosh", "MacIntel", "MacPPC", "Mac68K"];
const windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"];
const iosPlatforms = ["iPhone", "iPad", "iPod"];
if (macosPlatforms.indexOf(platform) !== -1) {
return "Mac OS";
} else if (iosPlatforms.indexOf(platform) !== -1) {
return "iOS";
} else if (windowsPlatforms.indexOf(platform) !== -1) {
return "Windows";
} else if (/Android/.test(userAgent)) {
return "Android";
} else if (/Linux/.test(platform)) {
return "Linux";
}
return null;
};

View File

@ -3,7 +3,7 @@ export class LineBreakTransformer implements Transformer<string, string> {
transform(
chunk: string,
controller: TransformStreamDefaultController<string>,
controller: TransformStreamDefaultController<string>
) {
// Append new chunks to existing chunks.
this.chunks += chunk;

View File

@ -1,18 +0,0 @@
import { Manifest } from "../const";
export const downloadManifest = async (manifestPath: string) => {
const manifestURL = new URL(manifestPath, location.toString()).toString();
const resp = await fetch(manifestURL);
const manifest: Manifest = await resp.json();
if ("new_install_skip_erase" in manifest) {
console.warn(
'Manifest option "new_install_skip_erase" is deprecated. Use "new_install_prompt_erase" instead.',
);
if (manifest.new_install_skip_erase) {
manifest.new_install_prompt_erase = true;
}
}
return manifest;
};

View File

@ -1,17 +0,0 @@
import { Transport } from "esptool-js";
import { sleep } from "./sleep";
export const hardReset = async (transport: Transport) => {
console.log("Triggering reset");
await transport.device.setSignals({
dataTerminalReady: false,
requestToSend: true,
});
await sleep(250);
await transport.device.setSignals({
dataTerminalReady: false,
requestToSend: false,
});
await sleep(250);
await new Promise((resolve) => setTimeout(resolve, 1000));
};

View File

@ -1,12 +0,0 @@
export class TimestampTransformer implements Transformer<string, string> {
transform(
chunk: string,
controller: TransformStreamDefaultController<string>,
) {
const date = new Date();
const h = date.getHours().toString().padStart(2, "0");
const m = date.getMinutes().toString().padStart(2, "0");
const s = date.getSeconds().toString().padStart(2, "0");
controller.enqueue(`[${h}:${m}:${s}]${chunk}`);
}
}

View File

@ -1 +0,0 @@
export const version = "dev";

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,20 @@
{
"name": "ESPHome",
"version": "2021.12.0-dev",
"home_assistant_domain": "esphome",
"builds": [
{
"chipFamily": "ESP32",
"parts": [
{ "path": "bootloader.bin", "offset": 4096 },
{ "path": "partitions.bin", "offset": 32768 },
{ "path": "ota.bin", "offset": 57344 },
{ "path": "firmware.bin", "offset": 65536 }
]
},
{
"chipFamily": "ESP8266",
"parts": [{ "path": "esp8266.bin", "offset": 0 }]
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -1,156 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="95.541344mm"
height="29.999447mm"
viewBox="0 0 95.54134 29.999447"
version="1.1"
id="svg5"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="logo.svg"
inkscape:export-filename="/home/erik/Documents/Projekt/NSPanel Manager/Logos/logo250.png"
inkscape:export-xdpi="76.010269"
inkscape:export-ydpi="76.010269"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#000000"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="1"
inkscape:pagecheckerboard="false"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.3404872"
inkscape:cx="174.5634"
inkscape:cy="71.988753"
inkscape:window-width="1366"
inkscape:window-height="704"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="layer2"
showguides="true"
inkscape:guide-bbox="true"
fit-margin-top="5"
fit-margin-left="7"
fit-margin-right="7"
fit-margin-bottom="5" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(-23.806324,-37.155967)">
<rect
style="fill:#000000;stroke:#000000;stroke-width:0.264583"
id="rect91625"
width="16.863066"
height="1.0996726"
x="30.938616"
y="59.758064" />
<rect
style="fill:#000000;stroke:#000000;stroke-width:0.473039"
id="rect91761"
width="66.510048"
height="0.89121634"
x="31.042845"
y="42.392487" />
<rect
style="fill:#000000;stroke:#000000;stroke-width:0.34752"
id="rect91763"
width="31.46484"
height="1.0167363"
x="80.709068"
y="59.79953" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(-23.806324,-37.155967)">
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:0;font-family:sans-serif;letter-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="64.189873"
y="56.76144"
id="text39249"><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.4056px;line-height:0.55;font-family:Hamlin;-inkscape-font-specification:'Hamlin, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#000000;stroke-width:0.264583"
x="64.189873"
y="56.76144"
id="tspan62001">NSPANEL</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93889px;line-height:1.55;font-family:Hamlin;-inkscape-font-specification:'Hamlin Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#000000;stroke-width:0.264583"
x="64.189873"
y="62.091209"
id="tspan73527">MANAGER</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
transform="translate(-23.806324,-37.155967)">
<g
id="g91894"
transform="matrix(0.15910033,0,0,0.15910033,102.25849,44.830316)">
<g
inkscape:label="Layer 1"
id="layer1-1"
transform="translate(-30.269979,-9.7200098)"
style="display:inline;fill:#ffc101;fill-opacity:1">
<path
style="fill:#ffc101;fill-opacity:1;stroke-width:0.103892"
d="m 55.789326,41.355207 -7.50621,-0.0036 c -8.143469,-0.0039 -15.508914,-0.135689 -16.902471,-0.302478 -0.482408,-0.05774 -0.940146,-0.168019 -1.017198,-0.245069 -0.115522,-0.115523 -0.121188,-0.512246 -0.03231,-2.261925 0.23677,-4.661121 1.011239,-7.815267 2.954845,-12.034057 2.163281,-4.695621 5.19604,-8.340625 9.101253,-10.938592 2.710535,-1.8032 6.982591,-3.536303 9.582241,-3.887359 0.270422,-0.03652 0.543941,-0.129371 0.60782,-0.20634 0.06388,-0.07697 0.286764,-0.168201 0.495299,-0.202735 0.208537,-0.03453 0.449284,-0.09017 0.534995,-0.123628 0.168484,-0.06577 0.381034,-0.08553 1.215776,-0.112981 l 0.540477,-0.01778 0.03093,-0.649326 0.03093,-0.6493272 h 1.610329 1.610329 l 0.03093,0.6493272 0.03093,0.649326 0.540477,0.01778 c 0.834742,0.02745 1.047292,0.04721 1.215776,0.112981 0.08571,0.03346 0.326458,0.08909 0.534995,0.123628 0.208535,0.03453 0.43142,0.125765 0.495299,0.202735 0.06388,0.07697 0.337398,0.169823 0.60782,0.20634 1.199505,0.161981 3.423686,0.820696 4.997483,1.480058 3.507719,1.469604 6.221358,3.303143 8.667596,5.856479 2.140092,2.233788 3.602852,4.416791 5.018415,7.489412 1.943606,4.21879 2.718075,7.372936 2.954845,12.034057 0.08888,1.749679 0.08321,2.146402 -0.03231,2.261925 -0.07705,0.07705 -0.53479,0.187332 -1.017198,0.245069 -1.393557,0.166789 -8.759002,0.298597 -16.902471,0.302478 l -7.50621,0.0036 z"
id="path1465"
inkscape:connector-curvature="0"
inkscape:export-xdpi="15.183412"
inkscape:export-ydpi="15.183412"
sodipodi:nodetypes="csssssssssscccccccccscssssssssccc" />
</g>
<g
inkscape:label="Layer 1 copy"
id="g2878"
transform="translate(-30.269979,-9.7200098)"
style="display:inline">
<path
style="fill:#000000;fill-opacity:1;stroke-width:0.103892"
d="m 54.334834,101.13608 c -3.956539,-0.21732 -5.98252,-0.58093 -6.463066,-1.159955 -0.09761,-0.11762 -0.123549,-0.57148 -0.09916,-1.73541 0.02799,-1.33577 0.008,-1.59689 -0.130816,-1.71212 -0.09011,-0.0748 -0.16384,-0.29294 -0.16384,-0.48477 0,-0.30436 0.05486,-0.37678 0.430667,-0.5685 0.694102,-0.3541 2.301256,-0.71174 4.060659,-0.9036 l 1.638312,-0.17866 V 70.783119 c 0,-15.670705 -0.03494,-23.609951 -0.103892,-23.609951 -0.05714,0 -0.103892,-0.0935 -0.103892,-0.207784 0,-0.194592 0.06926,-0.207784 1.090868,-0.207784 0.900399,0 1.090868,-0.02721 1.090868,-0.155839 0,-0.08571 0.04675,-0.155838 0.103892,-0.155838 0.06644,0 0.103892,-3.463072 0.103892,-5.090716 l 2.493412,-2e-6 c 0,1.627644 0.03746,5.090716 0.103892,5.090716 0.05714,0 0.103892,0.07013 0.103892,0.155838 0,0.128629 0.190469,0.155839 1.090868,0.155839 1.021606,0 1.090868,0.01319 1.090868,0.207784 0,0.114281 -0.04675,0.207784 -0.103892,0.207784 -0.06896,0 -0.103892,7.939246 -0.103892,23.609951 v 23.609948 l 1.638312,0.17866 c 1.759403,0.19186 3.366557,0.54949 4.060659,0.9036 0.375812,0.19172 0.430667,0.26413 0.430667,0.5685 0,0.19183 -0.07373,0.40998 -0.16384,0.48476 -0.138668,0.11509 -0.158971,0.37643 -0.132144,1.70097 0.01743,0.86075 -0.0055,1.62411 -0.05099,1.69635 -0.27864,0.442605 -1.632284,0.804945 -3.803075,1.017985 -1.460176,0.14331 -6.726732,0.26812 -8.109225,0.19219 z"
id="path2876"
inkscape:connector-curvature="0"
inkscape:export-xdpi="15.183412"
inkscape:export-ydpi="15.183412"
sodipodi:nodetypes="ssssssscssssssccsssssscssssscsss" />
</g>
<g
id="layer3-5"
inkscape:label="Layer 3">
<circle
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:17.1351;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="path1510"
cx="34.837666"
cy="55.072739"
r="1.2872558" />
</g>
<g
id="layer2-9"
inkscape:label="Layer 2">
<rect
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:12.8043;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect1508"
width="1.3904994"
height="22.681686"
x="34.147842"
y="31.601988" />
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -13,8 +13,8 @@
"noUnusedLocals": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"importHelpers": true
"suppressImplicitAnyIndexErrors": true,
"skipLibCheck": true
},
"include": ["src/*"]
}