Compare commits
No commits in common. "main" and "3.1.0" have entirely different histories.
@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
|
8
.github/release-drafter.yml
vendored
@ -1,11 +1,3 @@
|
||||
categories:
|
||||
- title: "Breaking Changes"
|
||||
labels:
|
||||
- "breaking change"
|
||||
- title: "Dependencies"
|
||||
collapse-after: 1
|
||||
labels:
|
||||
- "dependencies"
|
||||
template: |
|
||||
## What's Changed
|
||||
|
||||
|
8
.github/workflows/ci.yml
vendored
@ -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.1.5
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm ci
|
||||
|
8
.github/workflows/npmpublish.yml
vendored
@ -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.1.5
|
||||
with:
|
||||
node-version: 16
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
2
.github/workflows/release-drafter.yml
vendored
@ -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
@ -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.
|
116
README.md
@ -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,99 @@ 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",
|
||||
"improv": true,
|
||||
"parts": [
|
||||
{ "path": "bootloader_dout_40m.bin", "offset": 4096 },
|
||||
{ "path": "partitions.bin", "offset": 32768 },
|
||||
{ "path": "boot_app0.bin", "offset": 57344 },
|
||||
{ "path": "esp32.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 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"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 }
|
||||
{ "filename": "bootloader.bin", "offset": 4096 },
|
||||
{ "filename": "partitions.bin", "offset": 32768 },
|
||||
{ "filename": "ota.bin", "offset": 57344 },
|
||||
{ "filename": "firmware.bin", "offset": 65536 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"chipFamily": "ESP8266",
|
||||
"parts": [
|
||||
{ "path": "esp8266.bin", "offset": 0 }
|
||||
{ "filename": "esp8266.bin", "offset": 0 },
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Allows for optionally passing an attribute to trigger an erase before installation.
|
||||
|
||||
```html
|
||||
<esp-web-install-button
|
||||
manifest="firmware_esphome/manifest.json"
|
||||
erase-first
|
||||
></esp-web-install-button>
|
||||
```
|
||||
|
||||
All attributes can also be set via properties (`manifest`, `eraseFirst`)
|
||||
|
||||
## Styling
|
||||
|
||||
### Attributes
|
||||
|
||||
The following attributes are automatically added to `<esp-web-install-button>`:
|
||||
|
||||
| Attribute | Description |
|
||||
| -- | -- |
|
||||
| `install-supported` | Added if installing firmware is supported
|
||||
| `install-unsupported` | Added if installing firmware is not supported
|
||||
| `active` | Added when flashing is active
|
||||
|
||||
You can add the following attributes or properties to change the UI elements:
|
||||
|
||||
| Attribute | Property | Description |
|
||||
| -- | -- | -- |
|
||||
| `show-log` | `showLog` | Show a log style view of the progress instead of a progress bar
|
||||
| `hide-progress` | `hideProgress` | Hides all progress UI elements
|
||||
|
||||
### CSS custom properties (variables)
|
||||
|
||||
The following variables can be used to change the colors of the default UI elements:
|
||||
|
||||
- `--esp-tools-button-color`
|
||||
- `--esp-tools-button-text-color`
|
||||
- `--esp-tools-success-color`
|
||||
- `--esp-tools-error-color`
|
||||
- `--esp-tools-progress-color`
|
||||
- `--esp-tools-log-background`
|
||||
- `--esp-tools-log-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
|
||||
|
||||
## Events
|
||||
|
||||
When the state of flashing changes, a `state-changed` event is fired.
|
||||
|
||||
A `state-changed` event contains the following information:
|
||||
|
||||
Field | Description
|
||||
-- | --
|
||||
state | The current [state](https://github.com/esphome/esp-web-tools/blob/main/src/const.ts)
|
||||
message | A description of the current state
|
||||
manifest | The loaded manifest
|
||||
build | The manifest's build that was selected
|
||||
chipFamily | The chip that was detected; "ESP32" \| "ESP8266" \| "ESP32-S2" \| "Unknown Chip"
|
||||
details | An optional extra field that is different [per state](https://github.com/esphome/esp-web-tools/blob/main/src/const.ts)
|
||||
|
||||
## Development
|
||||
|
||||
Run `script/develop`. This starts a server. Open it on http://localhost:5001.
|
||||
|
||||
[](https://www.openhomefoundation.org/)
|
||||
Run `script/develop`. This starts a server. Open it on http://localhost:5000.
|
||||
|
704
index.html
@ -1,40 +1,12 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>ESP Web Tools</title>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Easily allow users to flash new firmware for their ESP-devices on the web."
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
||||
<meta property="og:title" content="ESP Web Tools" />
|
||||
<meta property="og:site_name" content="ESP Web Tools" />
|
||||
<meta
|
||||
property="og:url"
|
||||
content="https://esphome.github.io/esp-web-tools/"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Easily allow users to flash new firmware for their ESP-devices on the web."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://esphome.github.io/esp-web-tools/static/social.png"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="ESP Web Tools" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Easily allow users to flash new firmware for their ESP-devices on the web."
|
||||
/>
|
||||
<meta
|
||||
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 +20,32 @@
|
||||
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;
|
||||
width: 200px;
|
||||
}
|
||||
.project .logo img {
|
||||
height: 50px;
|
||||
}
|
||||
.project .name {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
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 {
|
||||
@ -121,26 +65,13 @@
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
a {
|
||||
color: #58a6ff;
|
||||
}
|
||||
}
|
||||
</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@3.1.0/dist/web/install-button.js?module"
|
||||
);
|
||||
</script>
|
||||
</head>
|
||||
@ -148,334 +79,131 @@
|
||||
<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 & 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">the ESPHome firmware</a>, connect an ESP 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
|
||||
>.
|
||||
erase-first
|
||||
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.
|
||||
</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>
|
||||
|
||||
<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.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/balloob/squeezelite-esp32-install"
|
||||
>Click here to see a full example.</a
|
||||
>
|
||||
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>
|
||||
<div class="videoWrapper">
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube-nocookie.com/embed/jGfa0xMhUn4"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
<h2>Projects using ESP Web Tools</h2>
|
||||
<div class="project">
|
||||
<div class="logo">
|
||||
<img src="static/wled.png" alt="WLED logo" />
|
||||
</div>
|
||||
<h3>WLED</h3>
|
||||
<p>
|
||||
A fast and feature-rich implementation of an ESP8266/ESP32 firmware to
|
||||
control NeoPixel (WS2812B, WS2811, SK6812) LEDs or also SPI based
|
||||
chipsets like the WS2801 and APA102.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://flash.wled.me" target="_blank"
|
||||
>Installation Website</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<h2>Adding ESP Web Tools to your website</h2>
|
||||
<p>
|
||||
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>
|
||||
<b>Step 1:</b> Load ESP Web Tools JavaScript on your website by adding
|
||||
the following HTML snippet.
|
||||
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
|
||||
>
|
||||
and put them on your website.
|
||||
</p>
|
||||
<pre>
|
||||
<script
|
||||
type="module"
|
||||
src="https://unpkg.com/esp-web-tools@10/dist/web/install-button.js?module"
|
||||
></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@3.1.0/dist/web/install-button.js?module"
|
||||
></script>
|
||||
|
||||
<esp-web-install-button
|
||||
manifest="https://firmware.esphome.io/esp-web-tools/manifest.json"
|
||||
manifest="static/firmware_build/manifest.json"
|
||||
></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
|
||||
>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Add the attribute <code>erase-first</code> if you want to first fully
|
||||
erase the ESP prior to installation.
|
||||
</p>
|
||||
<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 devices are ESP8266, ESP32 and ESP32-S2. 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,
|
||||
"builds": [
|
||||
{
|
||||
"chipFamily": "ESP32",
|
||||
"improv": true,
|
||||
"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,65 +216,35 @@ 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.
|
||||
</p>
|
||||
<p>
|
||||
If your firmware is supported by Home Assistant, you can add the
|
||||
optional key <code>home_assistant_domain</code>. If present, ESP Web
|
||||
Tools will link the user to add this device to Home Assistant.
|
||||
Each build also allows you to specify if it supports
|
||||
<a href="https://www.improv-wifi.com">the Improv WiFi standard</a>. If
|
||||
it does, the user will be offered to configure the WiFi after
|
||||
installation is done as can be seen in the video below.
|
||||
</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.
|
||||
</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.
|
||||
</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><esp-web-install-button></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
|
||||
>
|
||||
<div class="videoWrapper">
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube-nocookie.com/embed/jGfa0xMhUn4?start=186"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<h3 id="customize">Customizing the look and feel</h3>
|
||||
<h3>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 +252,14 @@ 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>
|
||||
|
||||
<li><code>--esp-tools-success-color</code></li>
|
||||
<li><code>--esp-tools-error-color</code></li>
|
||||
|
||||
<li><code>--esp-tools-progress-color</code></li>
|
||||
|
||||
<li><code>--esp-tools-log-background</code></li>
|
||||
<li><code>--esp-tools-log-text-color</code></li>
|
||||
</ul>
|
||||
<p>There are also some attributes that can be used for styling:</p>
|
||||
<table>
|
||||
@ -568,17 +273,27 @@ button.overrides = {
|
||||
</td>
|
||||
<td>Added if installing firmware is not supported</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>active</code></td>
|
||||
<td>Added when flashing is active</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>
|
||||
When you are using a custom button, you should disable it when the
|
||||
<code>active</code> attribute is present.
|
||||
</p>
|
||||
<h4>Replace the button and message with a custom one</h4>
|
||||
<p>
|
||||
You can replace both the activation button and the message that is shown
|
||||
when the user uses an unsupported browser or non-secure context with
|
||||
when the user uses an unsupported browser or non secure context with
|
||||
your own elements. This can be done using the <code>activate</code>,
|
||||
<code>unsupported</code> and <code>not-allowed</code> slots:
|
||||
</p>
|
||||
<pre>
|
||||
<esp-web-install-button
|
||||
manifest="https://firmware.esphome.io/esp-web-tools/manifest.json"
|
||||
manifest="static/firmware_build/manifest.json"
|
||||
show-log
|
||||
erase-first
|
||||
>
|
||||
<button slot="activate">Custom install button</button>
|
||||
<span slot="unsupported">Ah snap, your browser doesn't work!</span>
|
||||
@ -586,35 +301,126 @@ button.overrides = {
|
||||
</esp-web-install-button>
|
||||
</pre
|
||||
>
|
||||
<h4>Show or hide the progress bar and log</h4>
|
||||
<p>
|
||||
By default there is a progress bar showing the state and progress of the
|
||||
flashing progress, you can change this progress bar to a log view with
|
||||
the <code>show-log</code> attribute.
|
||||
</p>
|
||||
<p>
|
||||
You can also hide all progress indicators by adding the
|
||||
<code>hide-progress</code>
|
||||
attribute. This will hide both the progress bar and the log view. You
|
||||
can then implement your own progress elements using the
|
||||
<a href="#state-events">state events</a>.
|
||||
</p>
|
||||
|
||||
<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="state-events">State events</h3>
|
||||
<p>
|
||||
During the flash progress the button will fire
|
||||
<code>state-changed</code> events for every step of the progress and to
|
||||
signal progress in the writing.
|
||||
</p>
|
||||
<p>
|
||||
With these events you can create your own progress UI or trigger certain
|
||||
actions. You can also find the current state as the
|
||||
<code>state</code> property of the
|
||||
<code>esp-web-install-button</code> element.
|
||||
</p>
|
||||
<p>Events for the following states are fired:</p>
|
||||
<ul>
|
||||
<li>initializing</li>
|
||||
<li>manifest</li>
|
||||
<li>preparing</li>
|
||||
<li>erasing</li>
|
||||
<li>writing</li>
|
||||
<li>finished</li>
|
||||
<li>error</li>
|
||||
</ul>
|
||||
<p>
|
||||
A <code>state-changed</code> event contains the following information:
|
||||
</p>
|
||||
<table>
|
||||
<tr>
|
||||
<td><code>state</code></td>
|
||||
<td>The current state; one of the above</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>message</code></td>
|
||||
<td>A description of the current state</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>manifest</code></td>
|
||||
<td>The loaded manifest</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>build</code></td>
|
||||
<td>The manifest's build that was selected</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>chipFamily</code></td>
|
||||
<td>
|
||||
The chip that was detected;
|
||||
<code>"ESP32" | "ESP8266" | "ESP32-S2" | "Unknown Chip"</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>details</code></td>
|
||||
<td>
|
||||
An optional extra field that is different
|
||||
<a
|
||||
href="https://github.com/esphome/esp-web-tools/blob/main/src/const.ts"
|
||||
>per state</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>An example that logs all state events:</p>
|
||||
<pre>
|
||||
<esp-web-install-button
|
||||
manifest="static/firmware_build/manifest.json"
|
||||
></esp-web-install-button>
|
||||
<script>
|
||||
const espWebInstallButton = document.querySelector("esp-web-install-button");
|
||||
espWebInstallButton.addEventListener(
|
||||
"state-changed", (ev) => { console.log(ev.detail) }
|
||||
);
|
||||
</script>
|
||||
</pre>
|
||||
<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>
|
||||
|
8745
package-lock.json
generated
36
package.json
@ -1,34 +1,30 @@
|
||||
{
|
||||
"name": "esp-web-tools",
|
||||
"version": "10.1.0",
|
||||
"version": "3.1.0",
|
||||
"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": "^12.0.0",
|
||||
"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-base": "^0.21.0",
|
||||
"@material/mwc-linear-progress": "^0.21.0",
|
||||
"esp-web-flasher": "^1.0.4",
|
||||
"lit": "^2.0.0-rc.2",
|
||||
"tslib": "^2.2.0"
|
||||
}
|
||||
}
|
||||
|
28
rollup.config.js
Normal 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;
|
@ -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;
|
@ -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
|
||||
|
@ -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 &
|
||||
npm exec -- tsc --watch &
|
||||
npm exec -- rollup -c --watch
|
||||
npm exec -- rollup -c --watch &
|
||||
wait
|
||||
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -1,167 +0,0 @@
|
||||
import { ColoredConsole, coloredConsoleStyles } 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;
|
||||
}
|
||||
const shadowRoot = this.attachShadow({ mode: "open" });
|
||||
|
||||
shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host, input {
|
||||
background-color: #1c1c1c;
|
||||
color: #ddd;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
|
||||
monospace;
|
||||
line-height: 1.45;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px 0 16px;
|
||||
}
|
||||
input {
|
||||
flex: 1;
|
||||
padding: 4px;
|
||||
margin: 0 8px;
|
||||
border: 0;
|
||||
outline: none;
|
||||
}
|
||||
${coloredConsoleStyles}
|
||||
</style>
|
||||
<div class="log"></div>
|
||||
${
|
||||
this.allowInput
|
||||
? `<form>
|
||||
>
|
||||
<input autofocus>
|
||||
</form>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
|
||||
this._console = new ColoredConsole(this.shadowRoot!.querySelector("div")!);
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
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()))
|
||||
.pipeThrough(new TransformStream(new TimestampTransformer()))
|
||||
.pipeTo(
|
||||
new WritableStream({
|
||||
write: (chunk) => {
|
||||
this._console!.addLine(chunk.replace("\r", ""));
|
||||
},
|
||||
}),
|
||||
);
|
||||
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 + "\r\n"));
|
||||
this._console!.addLine(`> ${command}\r\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 sleep(250);
|
||||
await this.port.setSignals({
|
||||
dataTerminalReady: false,
|
||||
requestToSend: false,
|
||||
});
|
||||
await sleep(250);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ewt-console", EwtConsole);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ewt-console": EwtConsole;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
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);
|
@ -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>
|
||||
`;
|
@ -1,42 +0,0 @@
|
||||
import type { InstallButton } from "./install-button.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}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!port) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await port.open({ baudRate: 115200 });
|
||||
} catch (err: any) {
|
||||
alert(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
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 },
|
||||
);
|
||||
document.body.appendChild(el);
|
||||
};
|
52
src/const.ts
@ -1,19 +1,6 @@
|
||||
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"
|
||||
| "ESP32-C2"
|
||||
| "ESP32-C3"
|
||||
| "ESP32-C6"
|
||||
| "ESP32-H2"
|
||||
| "ESP32-S2"
|
||||
| "ESP32-S3"
|
||||
| "ESP8266";
|
||||
chipFamily: "ESP32" | "ESP8266";
|
||||
improv: boolean;
|
||||
parts: {
|
||||
path: string;
|
||||
offset: number;
|
||||
@ -22,64 +9,63 @@ export interface Build {
|
||||
|
||||
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[];
|
||||
}
|
||||
|
||||
export interface BaseFlashState {
|
||||
state: FlashStateType;
|
||||
interface BaseFlashState {
|
||||
state: State;
|
||||
message: string;
|
||||
manifest?: Manifest;
|
||||
build?: Build;
|
||||
chipFamily?: Build["chipFamily"] | "Unknown Chip";
|
||||
chipFamily?: "ESP32" | "ESP8266" | "ESP32-S2" | "Unknown Chip";
|
||||
}
|
||||
|
||||
export interface InitializingState extends BaseFlashState {
|
||||
state: FlashStateType.INITIALIZING;
|
||||
state: State.INITIALIZING;
|
||||
details: { done: boolean };
|
||||
}
|
||||
|
||||
export interface ManifestState extends BaseFlashState {
|
||||
state: State.MANIFEST;
|
||||
details: { done: boolean };
|
||||
}
|
||||
|
||||
export interface PreparingState extends BaseFlashState {
|
||||
state: FlashStateType.PREPARING;
|
||||
state: State.PREPARING;
|
||||
details: { done: boolean };
|
||||
}
|
||||
|
||||
export interface ErasingState extends BaseFlashState {
|
||||
state: FlashStateType.ERASING;
|
||||
state: State.ERASING;
|
||||
details: { done: boolean };
|
||||
}
|
||||
|
||||
export interface WritingState extends BaseFlashState {
|
||||
state: FlashStateType.WRITING;
|
||||
state: State.WRITING;
|
||||
details: { bytesTotal: number; bytesWritten: number; percentage: number };
|
||||
}
|
||||
|
||||
export interface FinishedState extends BaseFlashState {
|
||||
state: FlashStateType.FINISHED;
|
||||
state: State.FINISHED;
|
||||
}
|
||||
|
||||
export interface ErrorState extends BaseFlashState {
|
||||
state: FlashStateType.ERROR;
|
||||
state: State.ERROR;
|
||||
details: { error: FlashError; details: string | Error };
|
||||
}
|
||||
|
||||
export type FlashState =
|
||||
| InitializingState
|
||||
| ManifestState
|
||||
| PreparingState
|
||||
| ErasingState
|
||||
| WritingState
|
||||
| FinishedState
|
||||
| ErrorState;
|
||||
|
||||
export const enum FlashStateType {
|
||||
export const enum State {
|
||||
INITIALIZING = "initializing",
|
||||
MANIFEST = "manifest",
|
||||
PREPARING = "preparing",
|
||||
ERASING = "erasing",
|
||||
WRITING = "writing",
|
||||
|
138
src/flash-log.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { css, html, HTMLTemplateResult, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { FlashState, State } from "./const";
|
||||
|
||||
interface Row {
|
||||
state?: State;
|
||||
message: HTMLTemplateResult | string;
|
||||
error?: boolean;
|
||||
action?: boolean;
|
||||
}
|
||||
|
||||
@customElement("esp-web-flash-log")
|
||||
export class FlashLog extends LitElement {
|
||||
@state() private _rows: Row[] = [];
|
||||
|
||||
protected render() {
|
||||
return html`${this._rows.map(
|
||||
(row) =>
|
||||
html`<div
|
||||
class=${classMap({
|
||||
error: row.error === true,
|
||||
action: row.action === true,
|
||||
})}
|
||||
>
|
||||
${row.message}
|
||||
</div>`
|
||||
)}`;
|
||||
}
|
||||
|
||||
public willUpdate() {
|
||||
this.toggleAttribute("hidden", !this._rows.length);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this._rows = [];
|
||||
}
|
||||
|
||||
public processState(state: FlashState) {
|
||||
if (state.state === State.ERROR) {
|
||||
this.addError(state.message);
|
||||
return;
|
||||
}
|
||||
this.addRow(state);
|
||||
if (state.state === State.FINISHED) {
|
||||
this.addAction(
|
||||
html`<button @click=${this.clear}>Close this log</button>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or replace a row.
|
||||
*/
|
||||
public addRow(row: Row) {
|
||||
// If last entry has same ID, replace it.
|
||||
if (
|
||||
row.state &&
|
||||
this._rows.length > 0 &&
|
||||
this._rows[this._rows.length - 1].state === row.state
|
||||
) {
|
||||
const newRows = this._rows.slice(0, -1);
|
||||
newRows.push(row);
|
||||
this._rows = newRows;
|
||||
} else {
|
||||
this._rows = [...this._rows, row];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an error row
|
||||
*/
|
||||
public addError(message: Row["message"]) {
|
||||
this.addRow({ message, error: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an action row
|
||||
*/
|
||||
public addAction(message: Row["message"]) {
|
||||
this.addRow({ message, action: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove last row if state matches
|
||||
*/
|
||||
public removeRow(state: string) {
|
||||
if (
|
||||
this._rows.length > 0 &&
|
||||
this._rows[this._rows.length - 1].state === state
|
||||
) {
|
||||
this._rows = this._rows.slice(0, -1);
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
padding: 12px 16px;
|
||||
font-family: monospace;
|
||||
background: var(--esp-tools-log-background, black);
|
||||
color: var(--esp-tools-log-text-color, greenyellow);
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
}
|
||||
|
||||
:host([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--esp-tools-error-color, #dc3545);
|
||||
}
|
||||
|
||||
.error,
|
||||
.action {
|
||||
margin-top: 1em;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"esp-web-flash-log": FlashLog;
|
||||
}
|
||||
}
|
88
src/flash-progress.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { FlashState, State } from "./const";
|
||||
import "@material/mwc-linear-progress";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
|
||||
@customElement("esp-web-flash-progress")
|
||||
export class FlashProgress extends LitElement {
|
||||
@state() private _state?: FlashState;
|
||||
|
||||
@state() private _indeterminate = true;
|
||||
|
||||
@state() private _progress = 0;
|
||||
|
||||
public processState(state: FlashState) {
|
||||
this._state = state;
|
||||
if (this._state.state === State.WRITING) {
|
||||
this._indeterminate = false;
|
||||
this._progress = this._state.details.percentage / 100;
|
||||
}
|
||||
if (this._state.state === State.ERROR) {
|
||||
this._indeterminate = false;
|
||||
}
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this._state = undefined;
|
||||
this._progress = 0;
|
||||
this._indeterminate = true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._state) {
|
||||
return;
|
||||
}
|
||||
return html`<h2
|
||||
class=${classMap({
|
||||
error: this._state.state === State.ERROR,
|
||||
done: this._state.state === State.FINISHED,
|
||||
})}
|
||||
>
|
||||
${this._state.message}
|
||||
</h2>
|
||||
<p>
|
||||
${this._state.manifest
|
||||
? html`${this._state.manifest.name}: ${this._state.chipFamily}`
|
||||
: html` `}
|
||||
</p>
|
||||
<mwc-linear-progress
|
||||
class=${classMap({
|
||||
error: this._state.state === State.ERROR,
|
||||
done: this._state.state === State.FINISHED,
|
||||
})}
|
||||
.indeterminate=${this._indeterminate}
|
||||
.progress=${this._progress}
|
||||
></mwc-linear-progress>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
--mdc-theme-primary: var(--esp-tools-progress-color, #03a9f4);
|
||||
}
|
||||
.error {
|
||||
color: var(--esp-tools-error-color, #dc3545);
|
||||
--mdc-theme-primary: var(--esp-tools-error-color, #dc3545);
|
||||
}
|
||||
.done {
|
||||
color: var(--esp-tools-success-color, #28a745);
|
||||
--mdc-theme-primary: var(--esp-tools-success-color, #28a745);
|
||||
}
|
||||
mwc-linear-progress {
|
||||
text-align: left;
|
||||
}
|
||||
h2 {
|
||||
margin: 16px 0 0;
|
||||
}
|
||||
p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"esp-web-flash-progress": FlashProgress;
|
||||
}
|
||||
}
|
252
src/flash.ts
@ -1,213 +1,220 @@
|
||||
import { Transport, ESPLoader } from "esptool-js";
|
||||
import {
|
||||
Build,
|
||||
FlashError,
|
||||
FlashState,
|
||||
Manifest,
|
||||
FlashStateType,
|
||||
} from "./const";
|
||||
import { hardReset } from "./util/reset";
|
||||
import { connect, ESPLoader, Logger } from "esp-web-flasher";
|
||||
import { Build, FlashError, FlashState, Manifest, State } from "./const";
|
||||
import { fireEvent, getChipFamilyName, sleep } from "./util";
|
||||
|
||||
export const flash = async (
|
||||
onEvent: (state: FlashState) => void,
|
||||
port: SerialPort,
|
||||
eventTarget: EventTarget,
|
||||
logger: Logger,
|
||||
manifestPath: string,
|
||||
manifest: Manifest,
|
||||
eraseFirst: boolean,
|
||||
eraseFirst: boolean
|
||||
) => {
|
||||
let manifest: Manifest;
|
||||
let build: Build | undefined;
|
||||
let chipFamily: Build["chipFamily"];
|
||||
let chipFamily: "ESP32" | "ESP8266" | "ESP32-S2" | "Unknown Chip";
|
||||
|
||||
const fireStateEvent = (stateUpdate: FlashState) =>
|
||||
onEvent({
|
||||
const fireStateEvent = (stateUpdate: FlashState) => {
|
||||
fireEvent(eventTarget, "state-changed", {
|
||||
...stateUpdate,
|
||||
manifest,
|
||||
build,
|
||||
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()
|
||||
);
|
||||
|
||||
let esploader: ESPLoader | undefined;
|
||||
|
||||
try {
|
||||
esploader = await connect(logger);
|
||||
} catch (err) {
|
||||
// User pressed cancel on web serial
|
||||
return;
|
||||
}
|
||||
|
||||
// For debugging
|
||||
(window as any).esploader = esploader;
|
||||
|
||||
fireStateEvent({
|
||||
state: FlashStateType.INITIALIZING,
|
||||
state: State.INITIALIZING,
|
||||
message: "Initializing...",
|
||||
details: { done: false },
|
||||
});
|
||||
|
||||
try {
|
||||
await esploader.main();
|
||||
await esploader.flashId();
|
||||
} 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();
|
||||
await esploader.initialize();
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
if (esploader.connected) {
|
||||
fireStateEvent({
|
||||
state: State.ERROR,
|
||||
message:
|
||||
"Failed to initialize. Try resetting your device or holding the BOOT button before clicking connect.",
|
||||
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,
|
||||
state: State.INITIALIZING,
|
||||
message: `Initialized. Found ${chipFamily}`,
|
||||
details: { done: true },
|
||||
});
|
||||
fireStateEvent({
|
||||
state: State.MANIFEST,
|
||||
message: "Fetching manifest...",
|
||||
details: { done: false },
|
||||
});
|
||||
|
||||
try {
|
||||
manifest = await manifestProm;
|
||||
} catch (err) {
|
||||
fireStateEvent({
|
||||
state: State.ERROR,
|
||||
message: `Unable to fetch manifest: ${err.message}`,
|
||||
details: { error: FlashError.FAILED_MANIFEST_FETCH, details: err },
|
||||
});
|
||||
await esploader.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
build = manifest.builds.find((b) => b.chipFamily === chipFamily);
|
||||
|
||||
fireStateEvent({
|
||||
state: State.MANIFEST,
|
||||
message: `Found manifest for ${manifest.name}`,
|
||||
details: { done: true },
|
||||
});
|
||||
|
||||
if (!build) {
|
||||
fireStateEvent({
|
||||
state: FlashStateType.ERROR,
|
||||
state: State.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;
|
||||
}
|
||||
|
||||
fireStateEvent({
|
||||
state: FlashStateType.PREPARING,
|
||||
state: State.PREPARING,
|
||||
message: "Preparing installation...",
|
||||
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;
|
||||
} catch (err: any) {
|
||||
const data = await prom;
|
||||
files.push(data);
|
||||
totalSize += data.byteLength;
|
||||
} catch (err) {
|
||||
fireStateEvent({
|
||||
state: FlashStateType.ERROR,
|
||||
message: err.message,
|
||||
details: {
|
||||
error: FlashError.FAILED_FIRMWARE_DOWNLOAD,
|
||||
details: err.message,
|
||||
},
|
||||
state: State.ERROR,
|
||||
message: err,
|
||||
details: { error: FlashError.FAILED_FIRMWARE_DOWNLOAD, details: err },
|
||||
});
|
||||
await hardReset(transport);
|
||||
await transport.disconnect();
|
||||
await esploader.disconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fireStateEvent({
|
||||
state: FlashStateType.PREPARING,
|
||||
state: State.PREPARING,
|
||||
message: "Installation prepared",
|
||||
details: { done: true },
|
||||
});
|
||||
|
||||
if (eraseFirst) {
|
||||
fireStateEvent({
|
||||
state: FlashStateType.ERASING,
|
||||
state: State.ERASING,
|
||||
message: "Erasing device...",
|
||||
details: { done: false },
|
||||
});
|
||||
await esploader.eraseFlash();
|
||||
await espStub.eraseFlash();
|
||||
fireStateEvent({
|
||||
state: FlashStateType.ERASING,
|
||||
state: State.ERASING,
|
||||
message: "Device erased",
|
||||
details: { done: true },
|
||||
});
|
||||
}
|
||||
|
||||
let lastPct = 0;
|
||||
|
||||
fireStateEvent({
|
||||
state: FlashStateType.WRITING,
|
||||
message: `Writing progress: 0%`,
|
||||
state: State.WRITING,
|
||||
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) => {
|
||||
const newPct = Math.floor(
|
||||
((totalWritten + bytesWritten) / totalSize) * 100
|
||||
);
|
||||
if (newPct === lastPct) {
|
||||
return;
|
||||
}
|
||||
lastPct = newPct;
|
||||
fireStateEvent({
|
||||
state: State.WRITING,
|
||||
message: `Writing progress: ${newPct}%`,
|
||||
details: {
|
||||
bytesTotal: totalSize,
|
||||
bytesWritten: totalWritten + bytesWritten,
|
||||
percentage: newPct,
|
||||
},
|
||||
});
|
||||
},
|
||||
part.offset
|
||||
);
|
||||
} catch (err) {
|
||||
fireStateEvent({
|
||||
state: State.ERROR,
|
||||
message: err,
|
||||
details: { error: FlashError.WRITE_FAILED, details: err },
|
||||
});
|
||||
await esploader.disconnect();
|
||||
return;
|
||||
}
|
||||
totalWritten += file.byteLength;
|
||||
}
|
||||
|
||||
fireStateEvent({
|
||||
state: FlashStateType.WRITING,
|
||||
state: State.WRITING,
|
||||
message: "Writing complete",
|
||||
details: {
|
||||
bytesTotal: totalSize,
|
||||
@ -216,13 +223,12 @@ export const flash = async (
|
||||
},
|
||||
});
|
||||
|
||||
await hardReset(transport);
|
||||
|
||||
console.log("DISCONNECT");
|
||||
await transport.disconnect();
|
||||
await sleep(100);
|
||||
await esploader.softReset();
|
||||
await esploader.disconnect();
|
||||
|
||||
fireStateEvent({
|
||||
state: FlashStateType.FINISHED,
|
||||
state: State.FINISHED,
|
||||
message: "All done!",
|
||||
});
|
||||
};
|
||||
|
@ -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("./start-flash");
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
if (this.renderRoot) {
|
||||
@ -77,31 +84,36 @@ export class InstallButton extends HTMLElement {
|
||||
|
||||
if (!InstallButton.isSupported || !InstallButton.isAllowed) {
|
||||
this.toggleAttribute("install-unsupported", true);
|
||||
this.renderRoot.innerHTML = !InstallButton.isAllowed
|
||||
? "<slot name='not-allowed'>You can only install ESP devices on HTTPS websites or on the localhost.</slot>"
|
||||
: "<slot name='unsupported'>Your browser does not support installing things on ESP devices. Use Google Chrome or Microsoft Edge.</slot>";
|
||||
this.renderRoot.innerHTML = !InstallButton.isSupported
|
||||
? "<slot name='unsupported'>Your browser does not support installing things on ESP devices. Use Google Chrome or Microsoft Edge.</slot>"
|
||||
: "<slot name='not-allowed'>You can only install ESP devices on HTTPS websites or on the localhost.</slot>";
|
||||
return;
|
||||
}
|
||||
|
||||
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("./start-flash");
|
||||
mod.startFlash(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");
|
||||
|
@ -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;
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
126
src/start-flash.ts
Normal file
@ -0,0 +1,126 @@
|
||||
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";
|
||||
|
||||
let stateListenerAdded = false;
|
||||
|
||||
let logEl: FlashLog | undefined;
|
||||
|
||||
let progressEl: FlashProgress | undefined;
|
||||
|
||||
let improvEl: HTMLElement | undefined;
|
||||
|
||||
const addElement = <T extends HTMLElement>(
|
||||
button: InstallButton,
|
||||
element: T
|
||||
): T => {
|
||||
button.renderRoot!.append(element);
|
||||
return element;
|
||||
};
|
||||
|
||||
export const startFlash = async (button: InstallButton) => {
|
||||
if (button.hasAttribute("active")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const manifest = button.manifest || button.getAttribute("manifest");
|
||||
if (!manifest) {
|
||||
alert("No manifest defined!");
|
||||
return;
|
||||
}
|
||||
|
||||
let hasImprov = false;
|
||||
|
||||
if (!stateListenerAdded) {
|
||||
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);
|
||||
}
|
||||
progressEl?.processState(ev.detail);
|
||||
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 && !logEl) {
|
||||
logEl = addElement<FlashLog>(
|
||||
button,
|
||||
document.createElement("esp-web-flash-log")
|
||||
);
|
||||
} else if (!showLog && logEl) {
|
||||
logEl.remove();
|
||||
logEl = undefined;
|
||||
}
|
||||
|
||||
if (showProgress && !progressEl) {
|
||||
progressEl = addElement<FlashProgress>(
|
||||
button,
|
||||
document.createElement("esp-web-flash-progress")
|
||||
);
|
||||
} else if (!showProgress && progressEl) {
|
||||
progressEl.remove();
|
||||
progressEl = undefined;
|
||||
}
|
||||
|
||||
logEl?.clear();
|
||||
progressEl?.clear();
|
||||
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");
|
||||
|
||||
if (!customElements.get("improv-wifi-launch-button").isSupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!improvEl) {
|
||||
improvEl = document.createElement("improv-wifi-launch-button");
|
||||
const improvButton = document.createElement("button");
|
||||
improvButton.slot = "activate";
|
||||
improvButton.textContent = "CLICK HERE TO FINISH SETTING UP YOUR DEVICE";
|
||||
improvEl.appendChild(improvButton);
|
||||
addElement(button, improvEl);
|
||||
}
|
||||
improvEl.classList.toggle("hidden", false);
|
||||
};
|
@ -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);
|
||||
}
|
||||
`;
|
@ -1,3 +1,26 @@
|
||||
import {
|
||||
CHIP_FAMILY_ESP32,
|
||||
CHIP_FAMILY_ESP32S2,
|
||||
CHIP_FAMILY_ESP8266,
|
||||
ESPLoader,
|
||||
} from "esp-web-flasher";
|
||||
|
||||
export const getChipFamilyName = (esploader: ESPLoader) => {
|
||||
switch (esploader.chipFamily) {
|
||||
case CHIP_FAMILY_ESP32:
|
||||
return "ESP32";
|
||||
case CHIP_FAMILY_ESP8266:
|
||||
return "ESP8266";
|
||||
case CHIP_FAMILY_ESP32S2:
|
||||
return "ESP32-S2";
|
||||
default:
|
||||
return "Unknown Chip";
|
||||
}
|
||||
};
|
||||
|
||||
export const sleep = (time: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, time));
|
||||
|
||||
export const fireEvent = <Event extends keyof HTMLElementEventMap>(
|
||||
eventTarget: EventTarget,
|
||||
type: Event,
|
||||
@ -7,7 +30,7 @@ export const fireEvent = <Event extends keyof HTMLElementEventMap>(
|
||||
bubbles?: boolean;
|
||||
cancelable?: boolean;
|
||||
composed?: boolean;
|
||||
},
|
||||
}
|
||||
): void => {
|
||||
options = options || {};
|
||||
const event = new CustomEvent(type, {
|
@ -1,285 +0,0 @@
|
||||
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) {}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
const atBottom =
|
||||
this.targetElement.scrollTop >
|
||||
this.targetElement.scrollHeight - this.targetElement.offsetHeight - 50;
|
||||
|
||||
addSpan(line.substring(i));
|
||||
|
||||
// Keep scroll at bottom
|
||||
if (atBottom) {
|
||||
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);
|
||||
}
|
||||
`;
|
@ -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);
|
||||
};
|
@ -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;
|
||||
};
|
@ -1,20 +0,0 @@
|
||||
export class LineBreakTransformer implements Transformer<string, string> {
|
||||
private chunks = "";
|
||||
|
||||
transform(
|
||||
chunk: string,
|
||||
controller: TransformStreamDefaultController<string>,
|
||||
) {
|
||||
// Append new chunks to existing chunks.
|
||||
this.chunks += chunk;
|
||||
// For each line breaks in chunks, send the parsed lines out.
|
||||
const lines = this.chunks.split("\r\n");
|
||||
this.chunks = lines.pop()!;
|
||||
lines.forEach((line) => controller.enqueue(line + "\r\n"));
|
||||
}
|
||||
|
||||
flush(controller: TransformStreamDefaultController<string>) {
|
||||
// When the stream is closed, flush any remaining chunks out.
|
||||
controller.enqueue(this.chunks);
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
@ -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));
|
||||
};
|
@ -1,2 +0,0 @@
|
||||
export const sleep = (time: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, time));
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export const version = "dev";
|
BIN
static/firmware_build/bootloader.bin
Normal file
BIN
static/firmware_build/esp8266.bin
Normal file
BIN
static/firmware_build/firmware.bin
Normal file
19
static/firmware_build/manifest.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "ESPHome",
|
||||
"builds": [
|
||||
{
|
||||
"chipFamily": "ESP32",
|
||||
"improv": true,
|
||||
"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 }]
|
||||
}
|
||||
]
|
||||
}
|
BIN
static/firmware_build/ota.bin
Normal file
BIN
static/firmware_build/partitions.bin
Normal file
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 2.1 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 73"><g fill="none" fill-rule="evenodd"><path d="M72.63 16.18V12c0-1.47 1.19-2.66 2.66-2.66 1.47 0 2.66 1.19 2.66 2.66v4.18m-14.27 0V12c0-1.47 1.19-2.66 2.66-2.66A2.65 2.65 0 0 1 69 11.99v4.18m-14.28.01V12c0-1.47 1.19-2.66 2.66-2.66 1.47 0 2.66 1.19 2.66 2.66v4.18m-14.28 0V12c0-1.47 1.19-2.66 2.66-2.66 1.47 0 2.66 1.19 2.66 2.66v4.18m-14.28 0V12c0-1.47 1.19-2.66 2.66-2.66 1.47 0 2.66 1.19 2.66 2.66v4.18m-14.27 0V12c0-1.47 1.19-2.66 2.66-2.66 1.47 0 2.66 1.19 2.66 2.66v4.18M77.95 56.07v4.68c0 1.47-1.19 2.66-2.66 2.66-1.47 0-2.66-1.19-2.66-2.66v-4.68m-3.63 0v4.68c0 1.47-1.19 2.66-2.66 2.66-1.47 0-2.66-1.19-2.66-2.66v-4.68m-3.64 0v4.68c0 1.47-1.19 2.66-2.66 2.66-1.47 0-2.66-1.19-2.66-2.66v-4.68m-3.64 0v4.68c0 1.47-1.19 2.66-2.66 2.66-1.47 0-2.66-1.19-2.66-2.66v-4.68m-3.64 0v4.68c0 1.47-1.19 2.66-2.66 2.66-1.47 0-2.66-1.19-2.66-2.66v-4.68m-3.63.26v4.43c0 1.47-1.19 2.66-2.66 2.66-1.47 0-2.66-1.19-2.66-2.66v-4.43" fill="#fff" stroke="#000" stroke-width="2.178"/><path fill="#fff" stroke="#000" stroke-width="2.42" d="M79.29 16.18H26.51a.97.97 0 0 0-.97.97v37.96c0 .53.43.97.97.97h52.77c.53 0 .97-.43.97-.97V17.15c0-.54-.43-.97-.96-.97z"/><path fill="#000" fill-rule="nonzero" d="M61.6 35.42v-5.07h-1.8v3.28l-6.81-6.81-11.49 11.5h2.87v7.75h17.25v-7.75h2.87z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.961" d="M61.6 35.42v-5.07h-1.8v3.28l-6.81-6.81-11.49 11.5h2.87v7.75h17.25v-7.75h2.87z"/><path d="M25.34 53.77H9.52v-3.89h11.86v-3.89H9.52v-3.88h11.86v-3.89H9.52v-3.89h11.86v-3.88H9.52V18.47" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.159"/></g></svg>
|
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 6.2 KiB |
@ -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 |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 213 KiB |
Before Width: | Height: | Size: 691 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 170.38 170.38"><defs><style>.cls-1{fill:#010101;}</style></defs><title>Element 1</title><g id="Ebene_2" data-name="Ebene 2"><g id="Ebene_1-2" data-name="Ebene 1"><polygon class="cls-1" points="85.19 0 0 85.19 9.73 94.93 85.19 19.47 160.65 94.93 170.38 85.19 85.19 0"/><path class="cls-1" d="M85.19,60.08A52.6,52.6,0,0,0,66.45,161.84V146.7A38.79,38.79,0,0,1,78.31,74.5v95.88H92.08V74.5a38.79,38.79,0,0,1,11.85,72.2v15.14A52.6,52.6,0,0,0,85.19,60.08Z"/></g></g></svg>
|
Before Width: | Height: | Size: 519 B |
Before Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 57 KiB |
BIN
static/wled.png
Normal file
After Width: | Height: | Size: 22 KiB |
@ -13,8 +13,7 @@
|
||||
"noUnusedLocals": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"importHelpers": true
|
||||
"suppressImplicitAnyIndexErrors": true
|
||||
},
|
||||
"include": ["src/*"]
|
||||
}
|
||||
|