Repository: esphome/esp-web-tools
Branch: main
Commit: fad174ff6ec8
Files: 52
Total size: 144.4 KB
Directory structure:
gitextract_kivu888r/
├── .devcontainer/
│ ├── Dockerfile
│ └── devcontainer.json
├── .github/
│ ├── dependabot.yml
│ ├── release-drafter.yml
│ └── workflows/
│ ├── ci.yml
│ ├── npmpublish.yml
│ └── release-drafter.yml
├── .gitignore
├── .npmignore
├── .prettierignore
├── LICENSE
├── README.md
├── index.html
├── package.json
├── rollup.config.mjs
├── script/
│ ├── build
│ ├── develop
│ └── stubgen.py
├── src/
│ ├── components/
│ │ ├── ew-checkbox.ts
│ │ ├── ew-circular-progress.ts
│ │ ├── ew-dialog.ts
│ │ ├── ew-divider.ts
│ │ ├── ew-filled-select.ts
│ │ ├── ew-filled-text-field.ts
│ │ ├── ew-icon-button.ts
│ │ ├── ew-list-item.ts
│ │ ├── ew-list.ts
│ │ ├── ew-select-option.ts
│ │ ├── ew-text-button.ts
│ │ ├── ewt-console.ts
│ │ ├── ewt-dialog.ts
│ │ └── svg.ts
│ ├── connect.ts
│ ├── const.ts
│ ├── flash.ts
│ ├── install-button.ts
│ ├── install-dialog.ts
│ ├── no-port-picked/
│ │ ├── index.ts
│ │ └── no-port-picked-dialog.ts
│ ├── pages/
│ │ ├── ewt-page-message.ts
│ │ └── ewt-page-progress.ts
│ ├── styles.ts
│ ├── util/
│ │ ├── console-color.ts
│ │ ├── file-download.ts
│ │ ├── fire-event.ts
│ │ ├── get-operating-system.ts
│ │ ├── line-break-transformer.ts
│ │ ├── manifest.ts
│ │ ├── sleep.ts
│ │ └── timestamp-transformer.ts
│ └── version.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .devcontainer/Dockerfile
================================================
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.177.0/containers/typescript-node/.devcontainer/base.Dockerfile
# [Choice] Node.js version: 16, 14, 12
ARG VARIANT="16-buster"
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
# [Optional] Uncomment if you want to install more global node packages
# RUN su node -c "npm install -g <your-package-list -here>"
================================================
FILE: .devcontainer/devcontainer.json
================================================
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.177.0/containers/typescript-node
{
"name": "Node.js & TypeScript",
"build": {
"dockerfile": "Dockerfile",
// Update 'VARIANT' to pick a Node version: 12, 14, 16, 18, 20
"args": {
"VARIANT": "20"
}
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bierner.lit-html",
"runem.lit-plugin"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [5000],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "npm install",
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node",
"settings": {
"files.eol": "\n",
"editor.tabSize": 2,
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"files.trimTrailingWhitespace": true
}
}
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: weekly
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
================================================
FILE: .github/release-drafter.yml
================================================
categories:
- title: "Breaking Changes"
labels:
- "breaking change"
- title: "Dependencies"
collapse-after: 1
labels:
- "dependencies"
template: |
## What's Changed
$CHANGES
================================================
FILE: .github/workflows/ci.yml
================================================
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install jq tool
run: |
sudo apt-get update
sudo apt-get install jq
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: 16
- run: npm ci
- run: script/build
- run: npm exec -- prettier --check src
================================================
FILE: .github/workflows/npmpublish.yml
================================================
name: Node.js Package
on:
release:
types: [published]
jobs:
publish-npm:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
- name: Install jq tool
run: |
sudo apt-get update
sudo apt-get install jq
- uses: actions/setup-node@v6
with:
node-version: 20
registry-url: https://registry.npmjs.org/
- name: Update npm to latest
run: npm install -g npm@latest
- run: npm ci
- name: Set version from release tag
run: |
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}"
jq --arg v "$VERSION" '.version = $v' package.json > package.json.tmp && mv package.json.tmp package.json
- run: npm publish --provenance
================================================
FILE: .github/workflows/release-drafter.yml
================================================
name: Release Drafter
on:
push:
branches:
- main
jobs:
update_release_draft:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
dist
node_modules
================================================
FILE: .npmignore
================================================
index.html
firmware_build
demo
================================================
FILE: .prettierignore
================================================
src/vendor
================================================
FILE: LICENSE
================================================
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.
================================================
FILE: README.md
================================================
# 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/)
```html
<esp-web-install-button
manifest="firmware_esphome/manifest.json"
></esp-web-install-button>
```
Example manifest:
```json
{
"name": "ESPHome",
"version": "2021.10.3",
"home_assistant_domain": "esphome",
"funding_url": "https://esphome.io/guides/supporters.html",
"builds": [
{
"chipFamily": "ESP32",
"parts": [
{ "path": "bootloader_dout_40m.bin", "offset": 4096 },
{ "path": "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 }
]
},
{
"chipFamily": "ESP8266",
"parts": [
{ "path": "esp8266.bin", "offset": 0 }
]
}
]
}
```
## Development
Run `script/develop`. This starts a server. Open it on http://localhost:5001.
[](https://www.openhomefoundation.org/)
================================================
FILE: index.html
================================================
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>ESP Web Tools</title>
<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", Roboto,
Ubuntu, sans-serif;
padding: 0;
margin: 0;
line-height: 1.4;
}
.content {
max-width: 600px;
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 img {
height: 50px;
}
.project .name {
margin-top: 8px;
}
a {
color: #03a9f4;
}
.screenshot {
text-align: center;
}
.screenshot img {
max-width: 100%;
box-shadow:
rgb(0 0 0 / 20%) 0px 2px 1px -1px,
rgb(0 0 0 / 14%) 0px 1px 1px 0px,
rgb(0 0 0 / 12%) 0px 1px 3px 0px;
border-radius: 4px;
}
.screenshot i {
margin-top: 4px;
display: block;
}
.videoWrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
height: 0;
margin-bottom: 25px;
background: #ccc;
}
.hidden {
display: none;
}
.content pre {
display: block;
padding-left: 8px;
overflow-y: scroll;
}
.footer {
margin-top: 24px;
border-top: 1px solid #ccc;
padding-top: 24px;
text-align: center;
}
.footer .initiative {
font-style: italic;
margin-top: 16px;
}
table {
border-spacing: 0;
}
td {
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"
);
</script>
</head>
<body>
<div class="content">
<h1>ESP Web Tools</h1>
<p>
User friendly tools to manage ESP8266 and ESP32 devices in the browser:
</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:
</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
>.
</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>
<a
href="https://github.com/blak3r/treadspan"
target="_blank"
class="project"
>
<div class="logo">
<img src="static/logos/treadspan.png" alt="Treadspan logo" />
</div>
<div class="name">TreadSpan</div>
</a>
<a
href="https://opendisplay.org/firmware/install"
target="_blank"
class="project"
>
<div class="logo">
<img src="static/logos/opendisplay.png" alt="OpenDisplay logo" />
</div>
<div class="name">OpenDisplay</div>
</a>
</div>
<h2>How it works</h2>
<p>
ESP Web Tools works by combining
<a href="https://developer.mozilla.org/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
>
</p>
<p>
<b>Step 1:</b> Load ESP Web Tools JavaScript on your website by adding
the following HTML snippet.
</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>
<esp-web-install-button
manifest="https://firmware.esphome.io/esp-web-tools/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"
>the CORS-headers</a
>
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>
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>.
</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-C5</code>, <code>ESP32-C6</code>, <code>ESP32-C61</code>,
<code>ESP32-H2</code>, <code>ESP32-P4</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.
</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",
"parts": [
{ "path": "merged-firmware.bin", "offset": 0 },
]
},
{
"chipFamily": "ESP8266",
"parts": [
{ "path": "esp8266.bin", "offset": 0 }
]
}
]
}</pre
>
<p>
Each build contains a list of parts to be installed 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.
</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
>
<h4>Generating a manifest dynamically & version management</h4>
<p>
Alternatively to a static manifest JSON file, you can generate a Blob
URL of a JSON object using
<a
href="https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static"
><code>URL.createObjectURL</code></a
>
to use as the manifest url. This can be useful in situations where you
have many firmware bin files, e.g. previous software versions, where you
don't want to have a different static manifest json file for each bin.
If you are hosting on github.io, this can be paired nicely with
<a
href="https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#get-contents"
>github's REST API</a
>
to view all the bin files inside a folder.
</p>
<pre>
const manifest = {
"name": name,
"version": version,
"funding_url": funding_url,
"new_install_prompt_erase": true,
"builds": [
{
"chipFamily": "ESP32",
"improv": false,
"parts": [
{ "path": dependenciesDir+"bootloader.bin", "offset": 4096 },
{ "path": dependenciesDir+"partitions.bin", "offset": 32768 },
{ "path": dependenciesDir+"boot_app0.bin", "offset": 57344 },
{ "path": firmwareFile, "offset": 65536 }
]
}
]
}
const json = JSON.stringify(manifest);
const blob = new Blob([json], {type: "application/json"});
document.querySelector("esp-web-install-button").manifest = URL.createObjectURL(blob);
</pre>
<h3 id="customize">Customizing the look and feel</h3>
<p>
You can change the colors of the default UI elements with CSS custom
properties (variables), the following variables are available:
</p>
<ul>
<li><code>--esp-tools-button-color</code></li>
<li><code>--esp-tools-button-text-color</code></li>
<li><code>--esp-tools-button-border-radius</code></li>
</ul>
<p>There are also some attributes that can be used for styling:</p>
<table>
<tr>
<td><code>install-supported</code></td>
<td>Added if installing firmware is supported</td>
</tr>
<tr>
<td>
<code>install-unsupported</code>
</td>
<td>Added if installing firmware is not supported</td>
</tr>
</table>
<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
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"
>
<button slot="activate">Custom install button</button>
<span slot="unsupported">Ah snap, your browser doesn't work!</span>
<span slot="not-allowed">Ah snap, you are not allowed to use this on HTTP!</span>
</esp-web-install-button>
</pre
>
<h2>Why we created ESP Web Tools</h2>
<div class="videoWrapper">
<lite-youtube
videoid="6ZMXE5PXPqU"
videotitle="Why we created ESP Web Tools"
videoStartAt="1255"
></lite-youtube>
</div>
<div class="footer">
<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 />
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");
}
const search = new URLSearchParams(window.location.search);
if (search.has("manifest")) {
const button = document.querySelector("esp-web-install-button");
button.manifest = search.get("manifest");
}
}
</script>
</body>
</html>
================================================
FILE: package.json
================================================
{
"name": "esp-web-tools",
"version": "0.0.0",
"description": "Web tools for ESP devices",
"main": "dist/install-button.js",
"repository": {
"type": "git",
"url": "https://github.com/esphome/esp-web-tools"
},
"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": "^29.0.0",
"@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"
},
"dependencies": {
"@material/web": "^2.2.0",
"esptool-js": "^0.5.7",
"improv-wifi-serial-sdk": "^2.5.0",
"lit": "^3.2.1",
"pako": "^2.1.0",
"tslib": "^2.8.1"
}
}
================================================
FILE: rollup.config.mjs
================================================
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;
================================================
FILE: script/build
================================================
# Stop on errors
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
NODE_ENV=production npm exec -- rollup -c
================================================
FILE: script/develop
================================================
# Stop on errors
set -e
if [ -z "$PORT" ]; then
PORT=5001
fi
cd "$(dirname "$0")/.."
rm -rf dist
# Quit all background tasks when script exits
trap "kill 0" EXIT
# Run tsc once as rollup expects those files
npm exec -- tsc || true
npm exec -- serve -p "$PORT" &
npm exec -- tsc --watch &
npm exec -- rollup -c --watch
================================================
FILE: script/stubgen.py
================================================
import base64
import zlib
import json
stubs = {
"esp8266": b"""
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\
""",
"esp32": b"""
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\
""",
"esp32s2": b"""
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\
""",
}
for key, stub in stubs.items():
code = eval(zlib.decompress(base64.b64decode(stub)))
print("Processing " + key)
print("Text size:", str(len(code["text"])) + " bytes")
print("Data size:", str(len(code["data"])) + " bytes")
print(code["text"])
print(base64.b64encode(code["text"]))
code["text"] = base64.b64encode(code["text"]).decode("utf-8")
code["data"] = base64.b64encode(code["data"]).decode("utf-8")
jsondata = json.dumps(code, indent=2)
f = open(f"src/vendor/esptool/stubs/{key}.json", "w+")
f.write(jsondata)
f.close()
================================================
FILE: src/components/ew-checkbox.ts
================================================
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);
================================================
FILE: src/components/ew-circular-progress.ts
================================================
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);
================================================
FILE: src/components/ew-dialog.ts
================================================
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);
================================================
FILE: src/components/ew-divider.ts
================================================
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);
================================================
FILE: src/components/ew-filled-select.ts
================================================
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);
================================================
FILE: src/components/ew-filled-text-field.ts
================================================
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);
================================================
FILE: src/components/ew-icon-button.ts
================================================
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);
================================================
FILE: src/components/ew-list-item.ts
================================================
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);
================================================
FILE: src/components/ew-list.ts
================================================
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);
================================================
FILE: src/components/ew-select-option.ts
================================================
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);
================================================
FILE: src/components/ew-text-button.ts
================================================
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);
================================================
FILE: src/components/ewt-console.ts
================================================
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";
import { Transport, HardReset } from "esptool-js";
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() as ReadableWritablePair<string, Uint8Array>,
{
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");
const transport = new Transport(this.port);
// First assert RTS to enter reset state
await transport.setRTS(true);
await sleep(100);
// Use HardReset to release (same as esploader.after())
const resetStrategy = new HardReset(transport);
await resetStrategy.reset();
}
}
customElements.define("ewt-console", EwtConsole);
declare global {
interface HTMLElementTagNameMap {
"ewt-console": EwtConsole;
}
}
================================================
FILE: src/components/ewt-dialog.ts
================================================
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);
================================================
FILE: src/components/svg.ts
================================================
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>
`;
================================================
FILE: src/connect.ts
================================================
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, bufferSize: 8192 });
} 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);
};
================================================
FILE: src/const.ts
================================================
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-C5"
| "ESP32-C6"
| "ESP32-C61"
| "ESP32-H2"
| "ESP32-P4"
| "ESP32-S2"
| "ESP32-S3"
| "ESP8266";
parts: {
path: string;
offset: number;
}[];
}
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;
message: string;
manifest?: Manifest;
build?: Build;
chipFamily?: Build["chipFamily"] | "Unknown Chip";
}
export interface InitializingState extends BaseFlashState {
state: FlashStateType.INITIALIZING;
details: { done: boolean };
}
export interface PreparingState extends BaseFlashState {
state: FlashStateType.PREPARING;
details: { done: boolean };
}
export interface ErasingState extends BaseFlashState {
state: FlashStateType.ERASING;
details: { done: boolean };
}
export interface WritingState extends BaseFlashState {
state: FlashStateType.WRITING;
details: { bytesTotal: number; bytesWritten: number; percentage: number };
}
export interface FinishedState extends BaseFlashState {
state: FlashStateType.FINISHED;
}
export interface ErrorState extends BaseFlashState {
state: FlashStateType.ERROR;
details: { error: FlashError; details: string | Error };
}
export type FlashState =
| InitializingState
| PreparingState
| ErasingState
| WritingState
| FinishedState
| ErrorState;
export const enum FlashStateType {
INITIALIZING = "initializing",
PREPARING = "preparing",
ERASING = "erasing",
WRITING = "writing",
FINISHED = "finished",
ERROR = "error",
}
export const enum FlashError {
FAILED_INITIALIZING = "failed_initialize",
FAILED_MANIFEST_FETCH = "fetch_manifest_failed",
NOT_SUPPORTED = "not_supported",
FAILED_FIRMWARE_DOWNLOAD = "failed_firmware_download",
WRITE_FAILED = "write_failed",
}
declare global {
interface HTMLElementEventMap {
"state-changed": CustomEvent<FlashState>;
}
}
================================================
FILE: src/flash.ts
================================================
import { Transport, ESPLoader } from "esptool-js";
import {
Build,
FlashError,
FlashState,
Manifest,
FlashStateType,
} from "./const";
import { sleep } from "./util/sleep";
/**
* Perform a hard reset using esploader's chip-specific reset logic.
* First asserts RTS to enter reset, then calls esploader.after() which
* uses the chip-specific reset procedure to release.
*/
const hardResetDevice = async (transport: Transport, esploader: ESPLoader) => {
await transport.setRTS(true);
await sleep(100);
await esploader.after();
};
export const flash = async (
onEvent: (state: FlashState) => void,
port: SerialPort,
manifestPath: string,
manifest: Manifest,
eraseFirst: boolean,
) => {
let build: Build | undefined;
let chipFamily: Build["chipFamily"];
const fireStateEvent = (stateUpdate: FlashState) =>
onEvent({
...stateUpdate,
manifest,
build,
chipFamily,
});
const transport = new Transport(port);
const esploader = new ESPLoader({
transport,
baudrate: 115200,
romBaudrate: 115200,
enableTracing: false,
});
// For debugging
(window as any).esploader = esploader;
fireStateEvent({
state: FlashStateType.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 hardResetDevice(transport, esploader);
await transport.disconnect();
return;
}
chipFamily = esploader.chip.CHIP_NAME as any;
fireStateEvent({
state: FlashStateType.INITIALIZING,
message: `Initialized. Found ${chipFamily}`,
details: { done: true },
});
build = manifest.builds.find((b) => b.chipFamily === chipFamily);
if (!build) {
fireStateEvent({
state: FlashStateType.ERROR,
message: `Your ${chipFamily} board is not supported.`,
details: { error: FlashError.NOT_SUPPORTED, details: chipFamily },
});
await hardResetDevice(transport, esploader);
await transport.disconnect();
return;
}
fireStateEvent({
state: FlashStateType.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(
`Downloading 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);
});
});
const fileArray: Array<{ data: string; address: number }> = [];
let totalSize = 0;
for (let part = 0; part < filePromises.length; part++) {
try {
const data = await filePromises[part];
fileArray.push({ data, address: build.parts[part].offset });
totalSize += data.length;
} catch (err: any) {
fireStateEvent({
state: FlashStateType.ERROR,
message: err.message,
details: {
error: FlashError.FAILED_FIRMWARE_DOWNLOAD,
details: err.message,
},
});
await hardResetDevice(transport, esploader);
await transport.disconnect();
return;
}
}
fireStateEvent({
state: FlashStateType.PREPARING,
message: "Installation prepared",
details: { done: true },
});
if (eraseFirst) {
fireStateEvent({
state: FlashStateType.ERASING,
message: "Erasing device...",
details: { done: false },
});
await esploader.eraseFlash();
fireStateEvent({
state: FlashStateType.ERASING,
message: "Device erased",
details: { done: true },
});
}
fireStateEvent({
state: FlashStateType.WRITING,
message: `Writing progress: 0%`,
details: {
bytesTotal: totalSize,
bytesWritten: 0,
percentage: 0,
},
});
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 hardResetDevice(transport, esploader);
await transport.disconnect();
return;
}
fireStateEvent({
state: FlashStateType.WRITING,
message: "Writing complete",
details: {
bytesTotal: totalSize,
bytesWritten: totalWritten,
percentage: 100,
},
});
await hardResetDevice(transport, esploader);
console.log("DISCONNECT");
await transport.disconnect();
fireStateEvent({
state: FlashStateType.FINISHED,
message: "All done!",
});
};
================================================
FILE: src/install-button.ts
================================================
import type { FlashState } from "./const";
import type { EwtInstallDialog } from "./install-dialog";
import { connect } from "./connect";
export class InstallButton extends HTMLElement {
public static isSupported = "serial" in navigator;
public static isAllowed = window.isSecureContext;
private static style = `
button {
position: relative;
cursor: pointer;
font-size: 14px;
font-weight: 500;
padding: 10px 24px;
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);
}
button::before {
content: " ";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
opacity: 0.2;
border-radius: var(--esp-tools-button-border-radius, 9999px);
}
button:hover::before {
background-color: rgba(255,255,255,.8);
}
button:focus {
outline: none;
}
button:focus::before {
background-color: white;
}
button:active::before {
background-color: grey;
}
:host([active]) button {
color: rgba(0, 0, 0, 0.38);
background-color: rgba(0, 0, 0, 0.12);
box-shadow: none;
cursor: unset;
pointer-events: none;
}
.hidden {
display: none;
}`;
public manifest?: string;
public eraseFirst?: boolean;
public hideProgress?: boolean;
public showLog?: boolean;
public logConsole?: boolean;
public state?: FlashState;
public renderRoot?: ShadowRoot;
public overrides: EwtInstallDialog["overrides"];
public connectedCallback() {
if (this.renderRoot) {
return;
}
this.renderRoot = this.attachShadow({ mode: "open" });
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>";
return;
}
this.toggleAttribute("install-supported", true);
const slot = document.createElement("slot");
slot.addEventListener("click", async (ev) => {
ev.preventDefault();
connect(this);
});
slot.name = "activate";
const button = document.createElement("button");
button.innerText = "Connect";
slot.append(button);
if (
"adoptedStyleSheets" in Document.prototype &&
"replaceSync" in CSSStyleSheet.prototype
) {
const sheet = new CSSStyleSheet();
sheet.replaceSync(InstallButton.style);
this.renderRoot.adoptedStyleSheets = [sheet];
} else {
const styleSheet = document.createElement("style");
styleSheet.innerText = InstallButton.style;
this.renderRoot.append(styleSheet);
}
this.renderRoot.append(slot);
}
}
customElements.define("esp-web-install-button", InstallButton);
================================================
FILE: src/install-dialog.ts
================================================
import { LitElement, html, PropertyValues, css, TemplateResult } from "lit";
import { state } from "lit/decorators.js";
import "./components/ew-text-button";
import "./components/ew-list";
import "./components/ew-list-item";
import "./components/ew-divider";
import "./components/ew-checkbox";
import "./components/ewt-console";
import "./components/ew-dialog";
import "./components/ew-icon-button";
import "./components/ew-filled-text-field";
import type { EwFilledTextField } from "./components/ew-filled-text-field";
import "./components/ew-filled-select";
import "./components/ew-select-option";
import "./pages/ewt-page-progress";
import "./pages/ewt-page-message";
import {
closeIcon,
listItemConsole,
listItemEraseUserData,
listItemFundDevelopment,
listItemHomeAssistant,
listItemInstallIcon,
listItemVisitDevice,
listItemWifi,
refreshIcon,
} from "./components/svg";
import { Logger, Manifest, FlashStateType, FlashState } from "./const.js";
import { ImprovSerial, Ssid } from "improv-wifi-serial-sdk/dist/serial";
import {
ImprovSerialCurrentState,
ImprovSerialErrorState,
PortNotReady,
} from "improv-wifi-serial-sdk/dist/const";
import { flash } from "./flash";
import { textDownload } from "./util/file-download";
import { fireEvent } from "./util/fire-event";
import { sleep } from "./util/sleep";
import { downloadManifest } from "./util/manifest";
import { dialogStyles } from "./styles";
import { version } from "./version";
import type { EwFilledSelect } from "./components/ew-filled-select";
console.log(
`ESP Web Tools ${version} by Open Home Foundation; https://esphome.github.io/esp-web-tools/`,
);
const ERROR_ICON = "⚠️";
const OK_ICON = "🎉";
export class EwtInstallDialog extends LitElement {
public port!: SerialPort;
public manifestPath!: string;
public logger: Logger = console;
public overrides?: {
checkSameFirmware?: (
manifest: Manifest,
deviceImprov: ImprovSerial["info"],
) => boolean;
};
private _manifest!: Manifest;
private _info?: ImprovSerial["info"];
// null = NOT_SUPPORTED
@state() private _client?: ImprovSerial | null;
@state() private _state:
| "ERROR"
| "DASHBOARD"
| "PROVISION"
| "INSTALL"
| "ASK_ERASE"
| "LOGS" = "DASHBOARD";
@state() private _installErase = false;
@state() private _installConfirmed = false;
@state() private _installState?: FlashState;
@state() private _provisionForce = false;
private _wasProvisioned = false;
@state() private _error?: string;
@state() private _busy = false;
// undefined = not loaded
// null = not available
@state() private _ssids?: Ssid[] | null;
// Name of Ssid. Null = other
@state() private _selectedSsid: string | null = null;
private _bodyOverflow: string | null = null;
protected render() {
if (!this.port) {
return html``;
}
let heading: string | undefined;
let content: TemplateResult;
let allowClosing = false;
// During installation phase we temporarily remove the client
if (
this._client === undefined &&
this._state !== "INSTALL" &&
this._state !== "LOGS"
) {
if (this._error) {
[heading, content] = this._renderError(this._error);
} else {
content = this._renderProgress("Connecting");
}
} else if (this._state === "INSTALL") {
[heading, content, allowClosing] = this._renderInstall();
} else if (this._state === "ASK_ERASE") {
[heading, content] = this._renderAskErase();
} else if (this._state === "ERROR") {
[heading, content] = this._renderError(this._error!);
} else if (this._state === "DASHBOARD") {
[heading, content, allowClosing] = this._client
? this._renderDashboard()
: this._renderDashboardNoImprov();
} else if (this._state === "PROVISION") {
[heading, content] = this._renderProvision();
} else if (this._state === "LOGS") {
[heading, content] = this._renderLogs();
}
return html`
<ew-dialog
open
.heading=${heading!}
@cancel=${this._preventDefault}
@closed=${this._handleClose}
>
${heading ? html`<div slot="headline">${heading}</div>` : ""}
${allowClosing
? html`
<ew-icon-button slot="headline" @click=${this._closeDialog}>
${closeIcon}
</ew-icon-button>
`
: ""}
${content!}
</ew-dialog>
`;
}
_renderProgress(label: string | TemplateResult, progress?: number) {
return html`
<ewt-page-progress
slot="content"
.label=${label}
.progress=${progress}
></ewt-page-progress>
`;
}
_renderError(label: string): [string, TemplateResult] {
const heading = "Error";
const content = html`
<ewt-page-message
slot="content"
.icon=${ERROR_ICON}
.label=${label}
></ewt-page-message>
<div slot="actions">
<ew-text-button @click=${this._closeDialog}>Close</ew-text-button>
</div>
`;
return [heading, content];
}
_renderDashboard(): [string, TemplateResult, boolean] {
const heading = this._manifest.name;
let content: TemplateResult;
let allowClosing = true;
content = html`
<div slot="content">
<ew-list>
<ew-list-item>
<div slot="headline">Connected to ${this._info!.name}</div>
<div slot="supporting-text">
${this._info!.firmware} ${this._info!.version}
(${this._info!.chipFamily})
</div>
</ew-list-item>
${!this._isSameVersion
? html`
<ew-list-item
type="button"
@click=${() => {
if (this._isSameFirmware) {
this._startInstall(false);
} else if (this._manifest.new_install_prompt_erase) {
this._state = "ASK_ERASE";
} else {
this._startInstall(true);
}
}}
>
${listItemInstallIcon}
<div slot="headline">
${!this._isSameFirmware
? `Install ${this._manifest.name}`
: `Update ${this._manifest.name}`}
</div>
</ew-list-item>
`
: ""}
${this._client!.nextUrl === undefined
? ""
: html`
<ew-list-item
type="link"
href=${this._client!.nextUrl}
target="_blank"
>
${listItemVisitDevice}
<div slot="headline">Visit Device</div>
</ew-list-item>
`}
${!this._manifest.home_assistant_domain ||
this._client!.state !== ImprovSerialCurrentState.PROVISIONED
? ""
: html`
<ew-list-item
type="link"
href=${`https://my.home-assistant.io/redirect/config_flow_start/?domain=${this._manifest.home_assistant_domain}`}
target="_blank"
>
${listItemHomeAssistant}
<div slot="headline">Add to Home Assistant</div>
</ew-list-item>
`}
<ew-list-item
type="button"
@click=${() => {
this._state = "PROVISION";
if (
this._client!.state === ImprovSerialCurrentState.PROVISIONED
) {
this._provisionForce = true;
}
}}
>
${listItemWifi}
<div slot="headline">
${this._client!.state === ImprovSerialCurrentState.READY
? "Connect to Wi-Fi"
: "Change Wi-Fi"}
</div>
</ew-list-item>
<ew-list-item
type="button"
@click=${async () => {
const client = this._client;
if (client) {
await this._closeClientWithoutEvents(client);
await sleep(100);
}
// Also set `null` back to undefined.
this._client = undefined;
this._state = "LOGS";
}}
>
${listItemConsole}
<div slot="headline">Logs & Console</div>
</ew-list-item>
${this._isSameFirmware && this._manifest.funding_url
? html`
<ew-list-item
type="link"
href=${this._manifest.funding_url}
target="_blank"
>
${listItemFundDevelopment}
<div slot="headline">Fund Development</div>
</ew-list-item>
`
: ""}
${this._isSameVersion
? html`
<ew-list-item
type="button"
class="danger"
@click=${() => this._startInstall(true)}
>
${listItemEraseUserData}
<div slot="headline">Erase User Data</div>
</ew-list-item>
`
: ""}
</ew-list>
</div>
`;
return [heading, content, allowClosing];
}
_renderDashboardNoImprov(): [string, TemplateResult, boolean] {
const heading = this._manifest.name;
let content: TemplateResult;
let allowClosing = true;
content = html`
<div slot="content">
<ew-list>
<ew-list-item
type="button"
@click=${() => {
if (this._manifest.new_install_prompt_erase) {
this._state = "ASK_ERASE";
} else {
// Default is to erase a device that does not support Improv Serial
this._startInstall(true);
}
}}
>
${listItemInstallIcon}
<div slot="headline">${`Install ${this._manifest.name}`}</div>
</ew-list-item>
<ew-list-item
type="button"
@click=${async () => {
// Also set `null` back to undefined.
this._client = undefined;
this._state = "LOGS";
}}
>
${listItemConsole}
<div slot="headline">Logs & Console</div>
</ew-list-item>
</ew-list>
</div>
`;
return [heading, content, allowClosing];
}
_renderProvision(): [string | undefined, TemplateResult] {
let heading: string | undefined = "Configure Wi-Fi";
let content: TemplateResult;
if (this._busy) {
return [
heading,
this._renderProgress(
this._ssids === undefined
? "Scanning for networks"
: "Trying to connect",
),
];
}
if (
!this._provisionForce &&
this._client!.state === ImprovSerialCurrentState.PROVISIONED
) {
heading = undefined;
const showSetupLinks =
!this._wasProvisioned &&
(this._client!.nextUrl !== undefined ||
"home_assistant_domain" in this._manifest);
content = html`
<div slot="content">
<ewt-page-message
.icon=${OK_ICON}
label="Device connected to the network!"
></ewt-page-message>
${showSetupLinks
? html`
<ew-list>
${this._client!.nextUrl === undefined
? ""
: html`
<ew-list-item
type="link"
href=${this._client!.nextUrl}
target="_blank"
@click=${() => {
this._state = "DASHBOARD";
}}
>
${listItemVisitDevice}
<div slot="headline">Visit Device</div>
</ew-list-item>
`}
${!this._manifest.home_assistant_domain
? ""
: html`
<ew-list-item
type="link"
href=${`https://my.home-assistant.io/redirect/config_flow_start/?domain=${this._manifest.home_assistant_domain}`}
target="_blank"
@click=${() => {
this._state = "DASHBOARD";
}}
>
${listItemHomeAssistant}
<div slot="headline">Add to Home Assistant</div>
</ew-list-item>
`}
<ew-list-item
type="button"
@click=${() => {
this._state = "DASHBOARD";
}}
>
<div slot="start" class="fake-icon"></div>
<div slot="headline">Skip</div>
</ew-list-item>
</ew-list>
`
: ""}
</div>
${!showSetupLinks
? html`
<div slot="actions">
<ew-text-button
@click=${() => {
this._state = "DASHBOARD";
}}
>
Continue
</ew-text-button>
</div>
`
: ""}
`;
} else {
let error: string | undefined;
switch (this._client!.error) {
case ImprovSerialErrorState.UNABLE_TO_CONNECT:
error = "Unable to connect";
break;
case ImprovSerialErrorState.TIMEOUT:
error = "Timeout";
break;
case ImprovSerialErrorState.NO_ERROR:
// Happens when list SSIDs not supported.
case ImprovSerialErrorState.UNKNOWN_RPC_COMMAND:
break;
default:
error = `Unknown error (${this._client!.error})`;
}
const selectedSsid = this._ssids?.find(
(info) => info.name === this._selectedSsid,
);
content = html`
<ew-icon-button slot="headline" @click=${this._updateSsids}>
${refreshIcon}
</ew-icon-button>
<div slot="content">
<div>Connect your device to the network to start using it.</div>
${error ? html`<p class="error">${error}</p>` : ""}
${this._ssids !== null
? html`
<ew-filled-select
menu-positioning="fixed"
label="Network"
@change=${(ev: { target: EwFilledSelect }) => {
const index = ev.target.selectedIndex;
// The "Join Other" item is always the last item.
this._selectedSsid =
index === this._ssids!.length
? null
: this._ssids![index].name;
}}
>
${this._ssids!.map(
(info) => html`
<ew-select-option
.selected=${selectedSsid === info}
.value=${info.name}
>
${info.name}
</ew-select-option>
`,
)}
<ew-divider></ew-divider>
<ew-select-option .selected=${!selectedSsid}>
Join other…
</ew-select-option>
</ew-filled-select>
`
: ""}
${
// Show input box if command not supported or "Join Other" selected
!selectedSsid
? html`
<ew-filled-text-field
label="Network Name"
name="ssid"
></ew-filled-text-field>
`
: ""
}
${!selectedSsid || selectedSsid.secured
? html`
<ew-filled-text-field
label="Password"
name="password"
type="password"
></ew-filled-text-field>
`
: ""}
</div>
<div slot="actions">
<ew-text-button
@click=${() => {
this._state = "DASHBOARD";
}}
>
${this._installState && this._installErase ? "Skip" : "Back"}
</ew-text-button>
<ew-text-button @click=${this._doProvision}>Connect</ew-text-button>
</div>
`;
}
return [heading, content];
}
_renderAskErase(): [string | undefined, TemplateResult] {
const heading = "Erase device";
const content = html`
<div slot="content">
<div>
Do you want to erase the device before installing
${this._manifest.name}? All data on the device will be lost.
</div>
<label class="formfield">
<ew-checkbox touch-target="wrapper" class="danger"></ew-checkbox>
Erase device
</label>
</div>
<div slot="actions">
<ew-text-button
@click=${() => {
this._state = "DASHBOARD";
}}
>
Back
</ew-text-button>
<ew-text-button
@click=${() => {
const checkbox = this.shadowRoot!.querySelector("ew-checkbox")!;
this._startInstall(checkbox.checked);
}}
>
Next
</ew-text-button>
</div>
`;
return [heading, content];
}
_renderInstall(): [string | undefined, TemplateResult, boolean] {
let heading: string | undefined;
let content: TemplateResult;
const allowClosing = false;
const isUpdate = !this._installErase && this._isSameFirmware;
if (!this._installConfirmed && this._isSameVersion) {
heading = "Erase User Data";
content = html`
<div slot="content">
Do you want to reset your device and erase all user data from your
device?
</div>
<div slot="actions">
<ew-text-button class="danger" @click=${this._confirmInstall}>
Erase User Data
</ew-text-button>
</div>
`;
} else if (!this._installConfirmed) {
heading = "Confirm Installation";
const action = isUpdate ? "update to" : "install";
content = html`
<div slot="content">
${isUpdate
? html`Your device is running
${this._info!.firmware} ${this._info!.version}.<br /><br />`
: ""}
Do you want to ${action}
${this._manifest.name} ${this._manifest.version}?
${this._installErase
? html`<br /><br />All data on the device will be erased.`
: ""}
</div>
<div slot="actions">
<ew-text-button
@click=${() => {
this._state = "DASHBOARD";
}}
>
Back
</ew-text-button>
<ew-text-button @click=${this._confirmInstall}>
Install
</ew-text-button>
</div>
`;
} else if (
!this._installState ||
this._installState.state === FlashStateType.INITIALIZING ||
this._installState.state === FlashStateType.PREPARING
) {
heading = "Installing";
content = this._renderProgress("Preparing installation");
} else if (this._installState.state === FlashStateType.ERASING) {
heading = "Installing";
content = this._renderProgress("Erasing");
} else if (
this._installState.state === FlashStateType.WRITING ||
// When we're finished, keep showing this screen with 100% written
// until Improv is initialized / not detected.
(this._installState.state === FlashStateType.FINISHED &&
this._client === undefined)
) {
heading = "Installing";
let percentage: number | undefined;
let undeterminateLabel: string | undefined;
if (this._installState.state === FlashStateType.FINISHED) {
// We're done writing and detecting improv, show spinner
undeterminateLabel = "Wrapping up";
} else if (this._installState.details.percentage < 4) {
// We're writing the firmware under 4%, show spinner or else we don't show any pixels
undeterminateLabel = "Installing";
} else {
// We're writing the firmware over 4%, show progress bar
percentage = this._installState.details.percentage;
}
content = this._renderProgress(
html`
${undeterminateLabel ? html`${undeterminateLabel}<br />` : ""}
<br />
This will take
${this._installState.chipFamily === "ESP8266"
? "a minute"
: "2 minutes"}.<br />
Keep this page visible to prevent slow down
`,
percentage,
);
} else if (this._installState.state === FlashStateType.FINISHED) {
heading = undefined;
const supportsImprov = this._client !== null;
content = html`
<ewt-page-message
slot="content"
.icon=${OK_ICON}
label="Installation complete!"
></ewt-page-message>
<div slot="actions">
<ew-text-button
@click=${() => {
this._state =
supportsImprov && this._installErase
? "PROVISION"
: "DASHBOARD";
}}
>
Next
</ew-text-button>
</div>
`;
} else if (this._installState.state === FlashStateType.ERROR) {
heading = "Installation failed";
content = html`
<ewt-page-message
slot="content"
.icon=${ERROR_ICON}
.label=${this._installState.message}
></ewt-page-message>
<div slot="actions">
<ew-text-button
@click=${async () => {
this._initialize();
this._state = "DASHBOARD";
}}
>
Back
</ew-text-button>
</div>
`;
}
return [heading, content!, allowClosing];
}
_renderLogs(): [string | undefined, TemplateResult] {
let heading: string | undefined = `Logs`;
let content: TemplateResult;
content = html`
<div slot="content">
<ewt-console .port=${this.port} .logger=${this.logger}></ewt-console>
</div>
<div slot="actions">
<ew-text-button
@click=${async () => {
await this.shadowRoot!.querySelector("ewt-console")!.reset();
}}
>
Reset Device
</ew-text-button>
<ew-text-button
@click=${() => {
textDownload(
this.shadowRoot!.querySelector("ewt-console")!.logs(),
`esp-web-tools-logs.txt`,
);
this.shadowRoot!.querySelector("ewt-console")!.reset();
}}
>
Download Logs
</ew-text-button>
<ew-text-button
@click=${async () => {
await this.shadowRoot!.querySelector("ewt-console")!.disconnect();
this._state = "DASHBOARD";
this._initialize();
}}
>
Back
</ew-text-button>
</div>
`;
return [heading, content!];
}
public override willUpdate(changedProps: PropertyValues) {
if (!changedProps.has("_state")) {
return;
}
// Clear errors when changing between pages unless we change
// to the error page.
if (this._state !== "ERROR") {
this._error = undefined;
}
// Scan for SSIDs on provision
if (this._state === "PROVISION") {
this._updateSsids();
} else {
// Reset this value if we leave provisioning.
this._provisionForce = false;
}
if (this._state === "INSTALL") {
this._installConfirmed = false;
this._installState = undefined;
}
}
private async _updateSsids(tries = 0) {
const oldSsids = this._ssids;
this._ssids = undefined;
this._busy = true;
let ssids: Ssid[];
try {
ssids = await this._client!.scan();
} catch (err) {
// When we fail while loading, pick "Join other"
if (this._ssids === undefined) {
this._ssids = null;
this._selectedSsid = null;
}
this._busy = false;
return;
}
// We will retry a few times if we don't get any results
if (ssids.length === 0 && tries < 3) {
console.log("SCHEDULE RETRY", tries);
setTimeout(() => this._updateSsids(tries + 1), 2000);
return;
}
if (oldSsids) {
// If we had a previous list, ensure the selection is still valid
if (
this._selectedSsid &&
!ssids.find((s) => s.name === this._selectedSsid)
) {
this._selectedSsid = ssids[0].name;
}
} else {
this._selectedSsid = ssids.length ? ssids[0].name : null;
}
this._ssids = ssids;
this._busy = false;
}
protected override firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._bodyOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
this._initialize();
}
protected override updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("_state")) {
this.setAttribute("state", this._state);
}
if (this._state !== "PROVISION") {
return;
}
if (changedProps.has("_selectedSsid") && this._selectedSsid === null) {
// If we pick "Join other", select SSID input.
this._focusFormElement("ew-filled-text-field[name=ssid]");
} else if (changedProps.has("_ssids")) {
// Form is shown when SSIDs are loaded/marked not supported
this._focusFormElement();
}
}
private _focusFormElement(
selector = "ew-filled-text-field, ew-filled-select",
) {
const formEl = this.shadowRoot!.querySelector(
selector,
) as LitElement | null;
if (formEl) {
formEl.updateComplete.then(() => setTimeout(() => formEl.focus(), 100));
}
}
private async _initialize(justInstalled = false) {
if (this.port.readable === null || this.port.writable === null) {
this._state = "ERROR";
this._error =
"Serial port is not readable/writable. Close any other application using it and try again.";
return;
}
try {
this._manifest = await downloadManifest(this.manifestPath);
} catch (err: any) {
this._state = "ERROR";
this._error = "Failed to download manifest";
return;
}
if (this._manifest.new_install_improv_wait_time === 0) {
this._client = null;
return;
}
const client = new ImprovSerial(this.port!, this.logger);
client.addEventListener("state-changed", () => {
this.requestUpdate();
});
client.addEventListener("error-changed", () => this.requestUpdate());
try {
// If a device was just installed, give new firmware 10 seconds (overridable) to
// format the rest of the flash and do other stuff.
const timeout = !justInstalled
? 1000
: this._manifest.new_install_improv_wait_time !== undefined
? this._manifest.new_install_improv_wait_time * 1000
: 10000;
this._info = await client.initialize(timeout);
this._client = client;
client.addEventListener("disconnect", this._handleDisconnect);
} catch (err: any) {
// Clear old value
this._info = undefined;
if (err instanceof PortNotReady) {
this._state = "ERROR";
this._error =
"Serial port is not ready. Close any other application using it and try again.";
} else {
this._client = null; // not supported
this.logger.error("Improv initialization failed.", err);
}
}
}
private _startInstall(erase: boolean) {
this._state = "INSTALL";
this._installErase = erase;
this._installConfirmed = false;
}
private async _confirmInstall() {
this._installConfirmed = true;
this._installState = undefined;
if (this._client) {
await this._closeClientWithoutEvents(this._client);
}
this._client = undefined;
// Close port. ESPLoader likes opening it.
await this.port.close();
flash(
(state) => {
this._installState = state;
if (state.state === FlashStateType.FINISHED) {
sleep(100)
// Flashing closes the port
.then(() => this.port.open({ baudRate: 115200, bufferSize: 8192 }))
.then(() => this._initialize(true))
.then(() => this.requestUpdate());
} else if (state.state === FlashStateType.ERROR) {
sleep(100)
// Flashing closes the port
.then(() => this.port.open({ baudRate: 115200, bufferSize: 8192 }));
}
},
this.port,
this.manifestPath,
this._manifest,
this._installErase,
);
}
private async _doProvision() {
this._busy = true;
this._wasProvisioned =
this._client!.state === ImprovSerialCurrentState.PROVISIONED;
const ssid =
this._selectedSsid === null
? (
this.shadowRoot!.querySelector(
"ew-filled-text-field[name=ssid]",
) as EwFilledTextField
).value
: this._selectedSsid;
const password =
(
this.shadowRoot!.querySelector(
"ew-filled-text-field[name=password]",
) as EwFilledTextField | null
)?.value || "";
try {
await this._client!.provision(ssid, password, 30000);
} catch (err: any) {
return;
} finally {
this._busy = false;
this._provisionForce = false;
}
}
private _handleDisconnect = () => {
this._state = "ERROR";
this._error = "Disconnected";
};
private _closeDialog() {
this.shadowRoot!.querySelector("ew-dialog")!.close();
}
private async _handleClose() {
if (this._client) {
await this._closeClientWithoutEvents(this._client);
}
fireEvent(this, "closed" as any);
document.body.style.overflow = this._bodyOverflow!;
this.parentNode!.removeChild(this);
}
/**
* Return if the device runs same firmware as manifest.
*/
private get _isSameFirmware() {
return !this._info
? false
: this.overrides?.checkSameFirmware
? this.overrides.checkSameFirmware(this._manifest, this._info)
: this._info.firmware === this._manifest.name;
}
/**
* Return if the device runs same firmware and version as manifest.
*/
private get _isSameVersion() {
return (
this._isSameFirmware && this._info!.version === this._manifest.version
);
}
private async _closeClientWithoutEvents(client: ImprovSerial) {
client.removeEventListener("disconnect", this._handleDisconnect);
await client.close();
}
private _preventDefault(ev: Event) {
ev.preventDefault();
}
static styles = [
dialogStyles,
css`
:host {
--mdc-dialog-max-width: 390px;
}
div[slot="headline"] {
padding-right: 48px;
}
ew-icon-button[slot="headline"] {
position: absolute;
right: 4px;
top: 8px;
}
ew-icon-button[slot="headline"] svg {
padding: 8px;
color: var(--text-color);
}
.dialog-nav svg {
color: var(--text-color);
}
.table-row {
display: flex;
}
.table-row.last {
margin-bottom: 16px;
}
.table-row svg {
width: 20px;
margin-right: 8px;
}
ew-filled-text-field,
ew-filled-select {
display: block;
margin-top: 16px;
}
label.formfield {
display: inline-flex;
align-items: center;
padding-right: 8px;
}
ew-list {
margin: 0 -24px;
padding: 0;
}
ew-list-item svg {
height: 24px;
}
ewt-page-message + ew-list {
padding-top: 16px;
}
.fake-icon {
width: 24px;
}
.error {
color: var(--danger-color);
}
.danger {
--mdc-theme-primary: var(--danger-color);
--mdc-theme-secondary: var(--danger-color);
--md-sys-color-primary: var(--danger-color);
--md-sys-color-on-surface: var(--danger-color);
}
button.link {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
text-align: left;
text-decoration: underline;
cursor: pointer;
}
:host([state="LOGS"]) ew-dialog {
max-width: 90vw;
max-height: 90vh;
}
ewt-console {
width: calc(80vw - 48px);
height: calc(90vh - 168px);
}
`,
];
}
customElements.define("ewt-install-dialog", EwtInstallDialog);
declare global {
interface HTMLElementTagNameMap {
"ewt-install-dialog": EwtInstallDialog;
}
}
================================================
FILE: src/no-port-picked/index.ts
================================================
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;
};
================================================
FILE: src/no-port-picked/no-port-picked-dialog.ts
================================================
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;
}
}
================================================
FILE: src/pages/ewt-page-message.ts
================================================
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;
}
}
================================================
FILE: src/pages/ewt-page-progress.ts
================================================
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;
}
}
================================================
FILE: src/styles.ts
================================================
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);
}
`;
================================================
FILE: src/util/console-color.ts
================================================
interface ConsoleState {
bold: boolean;
italic: boolean;
underline: boolean;
strikethrough: boolean;
foregroundColor: string | null;
backgroundColor: string | null;
carriageReturn: boolean;
lines: string[];
secret: boolean;
}
export class ColoredConsole {
public state: ConsoleState = {
bold: false,
italic: false,
underline: false,
strikethrough: false,
foregroundColor: null,
backgroundColor: null,
carriageReturn: false,
lines: [],
secret: false,
};
constructor(public targetElement: HTMLElement) {}
logs(): string {
return this.targetElement.innerText;
}
processLine(line: string): Element {
// @ts-expect-error
const re = /(?:\033|\\033)(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
let i = 0;
const lineSpan = document.createElement("span");
lineSpan.classList.add("line");
const addSpan = (content: string) => {
if (content === "") return;
const span = document.createElement("span");
if (this.state.bold) span.classList.add("log-bold");
if (this.state.italic) span.classList.add("log-italic");
if (this.state.underline) span.classList.add("log-underline");
if (this.state.strikethrough) span.classList.add("log-strikethrough");
if (this.state.secret) span.classList.add("log-secret");
if (this.state.foregroundColor !== null)
span.classList.add(`log-fg-${this.state.foregroundColor}`);
if (this.state.backgroundColor !== null)
span.classList.add(`log-bg-${this.state.backgroundColor}`);
span.appendChild(document.createTextNode(content));
lineSpan.appendChild(span);
if (this.state.secret) {
const redacted = document.createElement("span");
redacted.classList.add("log-secret-redacted");
redacted.appendChild(document.createTextNode("[redacted]"));
lineSpan.appendChild(redacted);
}
};
while (true) {
const match = re.exec(line);
if (match === null) break;
const j = match.index;
addSpan(line.substring(i, j));
i = j + match[0].length;
if (match[1] === undefined) continue;
for (const colorCode of match[1].split(";")) {
switch (parseInt(colorCode)) {
case 0:
// reset
this.state.bold = false;
this.state.italic = false;
this.state.underline = false;
this.state.strikethrough = false;
this.state.foregroundColor = null;
this.state.backgroundColor = null;
this.state.secret = false;
break;
case 1:
this.state.bold = true;
break;
case 3:
this.state.italic = true;
break;
case 4:
this.state.underline = true;
break;
case 5:
this.state.secret = true;
break;
case 6:
this.state.secret = false;
break;
case 9:
this.state.strikethrough = true;
break;
case 22:
this.state.bold = false;
break;
case 23:
this.state.italic = false;
break;
case 24:
this.state.underline = false;
break;
case 29:
this.state.strikethrough = false;
break;
case 30:
this.state.foregroundColor = "black";
break;
case 31:
this.state.foregroundColor = "red";
break;
case 32:
this.state.foregroundColor = "green";
break;
case 33:
this.state.foregroundColor = "yellow";
break;
case 34:
this.state.foregroundColor = "blue";
break;
case 35:
this.state.foregroundColor = "magenta";
break;
case 36:
this.state.foregroundColor = "cyan";
break;
case 37:
this.state.foregroundColor = "white";
break;
case 39:
this.state.foregroundColor = null;
break;
case 41:
this.state.backgroundColor = "red";
break;
case 42:
this.state.backgroundColor = "green";
break;
case 43:
this.state.backgroundColor = "yellow";
break;
case 44:
this.state.backgroundColor = "blue";
break;
case 45:
this.state.backgroundColor = "magenta";
break;
case 46:
this.state.backgroundColor = "cyan";
break;
case 47:
this.state.backgroundColor = "white";
break;
case 40:
case 49:
this.state.backgroundColor = null;
break;
}
}
}
addSpan(line.substring(i));
return lineSpan;
}
processLines() {
const atBottom =
this.targetElement.scrollTop >
this.targetElement.scrollHeight - this.targetElement.offsetHeight - 50;
const prevCarriageReturn = this.state.carriageReturn;
const fragment = document.createDocumentFragment();
if (this.state.lines.length == 0) {
return;
}
for (const line of this.state.lines) {
if (this.state.carriageReturn && line !== "\n") {
if (fragment.childElementCount) {
fragment.removeChild(fragment.lastChild!);
}
}
fragment.appendChild(this.processLine(line));
this.state.carriageReturn = line.includes("\r");
}
if (prevCarriageReturn && this.state.lines[0] !== "\n") {
this.targetElement.replaceChild(fragment, this.targetElement.lastChild!);
} else {
this.targetElement.appendChild(fragment);
}
this.state.lines = [];
// Keep scroll at bottom
if (atBottom) {
this.targetElement.scrollTop = this.targetElement.scrollHeight;
}
}
addLine(line: string) {
// Processing of lines is deferred for performance reasons
if (this.state.lines.length == 0) {
setTimeout(() => this.processLines(), 0);
}
this.state.lines.push(line);
}
}
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);
}
`;
================================================
FILE: src/util/file-download.ts
================================================
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);
};
================================================
FILE: src/util/fire-event.ts
================================================
export const fireEvent = <Event extends keyof HTMLElementEventMap>(
eventTarget: EventTarget,
type: Event,
// @ts-ignore
detail?: HTMLElementEventMap[Event]["detail"],
options?: {
bubbles?: boolean;
cancelable?: boolean;
composed?: boolean;
},
): void => {
options = options || {};
const event = new CustomEvent(type, {
bubbles: options.bubbles === undefined ? true : options.bubbles,
cancelable: Boolean(options.cancelable),
composed: options.composed === undefined ? true : options.composed,
detail,
});
eventTarget.dispatchEvent(event);
};
================================================
FILE: src/util/get-operating-system.ts
================================================
// 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;
};
================================================
FILE: src/util/line-break-transformer.ts
================================================
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);
}
}
================================================
FILE: src/util/manifest.ts
================================================
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;
};
================================================
FILE: src/util/sleep.ts
================================================
export const sleep = (time: number) =>
new Promise((resolve) => setTimeout(resolve, time));
================================================
FILE: src/util/timestamp-transformer.ts
================================================
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}`);
}
}
================================================
FILE: src/version.ts
================================================
export const version = "dev";
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"lib": ["es2019", "dom"],
"target": "es2019",
"module": "es2020",
"moduleResolution": "node",
"resolveJsonModule": true,
"outDir": "dist",
"declaration": true,
"experimentalDecorators": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"importHelpers": true
},
"include": ["src/*"]
}
gitextract_kivu888r/ ├── .devcontainer/ │ ├── Dockerfile │ └── devcontainer.json ├── .github/ │ ├── dependabot.yml │ ├── release-drafter.yml │ └── workflows/ │ ├── ci.yml │ ├── npmpublish.yml │ └── release-drafter.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── LICENSE ├── README.md ├── index.html ├── package.json ├── rollup.config.mjs ├── script/ │ ├── build │ ├── develop │ └── stubgen.py ├── src/ │ ├── components/ │ │ ├── ew-checkbox.ts │ │ ├── ew-circular-progress.ts │ │ ├── ew-dialog.ts │ │ ├── ew-divider.ts │ │ ├── ew-filled-select.ts │ │ ├── ew-filled-text-field.ts │ │ ├── ew-icon-button.ts │ │ ├── ew-list-item.ts │ │ ├── ew-list.ts │ │ ├── ew-select-option.ts │ │ ├── ew-text-button.ts │ │ ├── ewt-console.ts │ │ ├── ewt-dialog.ts │ │ └── svg.ts │ ├── connect.ts │ ├── const.ts │ ├── flash.ts │ ├── install-button.ts │ ├── install-dialog.ts │ ├── no-port-picked/ │ │ ├── index.ts │ │ └── no-port-picked-dialog.ts │ ├── pages/ │ │ ├── ewt-page-message.ts │ │ └── ewt-page-progress.ts │ ├── styles.ts │ ├── util/ │ │ ├── console-color.ts │ │ ├── file-download.ts │ │ ├── fire-event.ts │ │ ├── get-operating-system.ts │ │ ├── line-break-transformer.ts │ │ ├── manifest.ts │ │ ├── sleep.ts │ │ └── timestamp-transformer.ts │ └── version.ts └── tsconfig.json
SYMBOL INDEX (100 symbols across 22 files)
FILE: src/components/ew-checkbox.ts
type HTMLElementTagNameMap (line 5) | interface HTMLElementTagNameMap {
class EwCheckbox (line 10) | class EwCheckbox extends Checkbox {
FILE: src/components/ew-circular-progress.ts
type HTMLElementTagNameMap (line 5) | interface HTMLElementTagNameMap {
class EwCircularProgress (line 10) | class EwCircularProgress extends CircularProgress {
FILE: src/components/ew-dialog.ts
type HTMLElementTagNameMap (line 5) | interface HTMLElementTagNameMap {
class EwDialog (line 10) | class EwDialog extends Dialog {
FILE: src/components/ew-divider.ts
type HTMLElementTagNameMap (line 5) | interface HTMLElementTagNameMap {
class EwDivider (line 10) | class EwDivider extends Divider {
FILE: src/components/ew-filled-select.ts
type HTMLElementTagNameMap (line 6) | interface HTMLElementTagNameMap {
class EwFilledSelect (line 11) | class EwFilledSelect extends FilledSelect {
FILE: src/components/ew-filled-text-field.ts
type HTMLElementTagNameMap (line 7) | interface HTMLElementTagNameMap {
class EwFilledTextField (line 12) | class EwFilledTextField extends FilledTextField {
FILE: src/components/ew-icon-button.ts
type HTMLElementTagNameMap (line 6) | interface HTMLElementTagNameMap {
class EwIconButton (line 11) | class EwIconButton extends IconButton {
FILE: src/components/ew-list-item.ts
type HTMLElementTagNameMap (line 5) | interface HTMLElementTagNameMap {
class EwListItem (line 10) | class EwListItem extends ListItem {
FILE: src/components/ew-list.ts
type HTMLElementTagNameMap (line 5) | interface HTMLElementTagNameMap {
class EwList (line 10) | class EwList extends List {
FILE: src/components/ew-select-option.ts
type HTMLElementTagNameMap (line 5) | interface HTMLElementTagNameMap {
class EwSelectOption (line 10) | class EwSelectOption extends SelectOptionEl {
FILE: src/components/ew-text-button.ts
type HTMLElementTagNameMap (line 6) | interface HTMLElementTagNameMap {
class EwTextButton (line 11) | class EwTextButton extends TextButton {
FILE: src/components/ewt-console.ts
class EwtConsole (line 8) | class EwtConsole extends HTMLElement {
method logs (line 16) | public logs(): string {
method connectedCallback (line 20) | public connectedCallback() {
method _connect (line 92) | private async _connect(abortSignal: AbortSignal) {
method _sendCommand (line 126) | private async _sendCommand() {
method disconnect (line 142) | public async disconnect() {
method reset (line 149) | public async reset() {
type HTMLElementTagNameMap (line 164) | interface HTMLElementTagNameMap {
FILE: src/components/ewt-dialog.ts
type HTMLElementTagNameMap (line 6) | interface HTMLElementTagNameMap {
class EwtDialog (line 11) | class EwtDialog extends DialogBase {
FILE: src/const.ts
type Logger (line 1) | interface Logger {
type Build (line 7) | interface Build {
type Manifest (line 26) | interface Manifest {
type BaseFlashState (line 39) | interface BaseFlashState {
type InitializingState (line 47) | interface InitializingState extends BaseFlashState {
type PreparingState (line 52) | interface PreparingState extends BaseFlashState {
type ErasingState (line 57) | interface ErasingState extends BaseFlashState {
type WritingState (line 62) | interface WritingState extends BaseFlashState {
type FinishedState (line 67) | interface FinishedState extends BaseFlashState {
type ErrorState (line 71) | interface ErrorState extends BaseFlashState {
type FlashState (line 76) | type FlashState =
type FlashStateType (line 84) | const enum FlashStateType {
type FlashError (line 93) | const enum FlashError {
type HTMLElementEventMap (line 102) | interface HTMLElementEventMap {
FILE: src/install-button.ts
class InstallButton (line 5) | class InstallButton extends HTMLElement {
method connectedCallback (line 71) | public connectedCallback() {
FILE: src/install-dialog.ts
constant ERROR_ICON (line 48) | const ERROR_ICON = "⚠️";
constant OK_ICON (line 49) | const OK_ICON = "🎉";
class EwtInstallDialog (line 51) | class EwtInstallDialog extends LitElement {
method render (line 100) | protected render() {
method _renderProgress (line 155) | _renderProgress(label: string | TemplateResult, progress?: number) {
method _renderError (line 165) | _renderError(label: string): [string, TemplateResult] {
method _renderDashboard (line 180) | _renderDashboard(): [string, TemplateResult, boolean] {
method _renderDashboardNoImprov (line 307) | _renderDashboardNoImprov(): [string, TemplateResult, boolean] {
method _renderProvision (line 347) | _renderProvision(): [string | undefined, TemplateResult] {
method _renderAskErase (line 535) | _renderAskErase(): [string | undefined, TemplateResult] {
method _renderInstall (line 570) | _renderInstall(): [string | undefined, TemplateResult, boolean] {
method _renderLogs (line 706) | _renderLogs(): [string | undefined, TemplateResult] {
method willUpdate (line 749) | public override willUpdate(changedProps: PropertyValues) {
method _updateSsids (line 772) | private async _updateSsids(tries = 0) {
method firstUpdated (line 814) | protected override firstUpdated(changedProps: PropertyValues) {
method updated (line 821) | protected override updated(changedProps: PropertyValues) {
method _focusFormElement (line 841) | private _focusFormElement(
method _initialize (line 852) | private async _initialize(justInstalled = false) {
method _startInstall (line 903) | private _startInstall(erase: boolean) {
method _confirmInstall (line 909) | private async _confirmInstall() {
method _doProvision (line 942) | private async _doProvision() {
method _closeDialog (line 975) | private _closeDialog() {
method _handleClose (line 979) | private async _handleClose() {
method _isSameFirmware (line 991) | private get _isSameFirmware() {
method _isSameVersion (line 1002) | private get _isSameVersion() {
method _closeClientWithoutEvents (line 1008) | private async _closeClientWithoutEvents(client: ImprovSerial) {
method _preventDefault (line 1013) | private _preventDefault(ev: Event) {
type HTMLElementTagNameMap (line 1105) | interface HTMLElementTagNameMap {
FILE: src/no-port-picked/no-port-picked-dialog.ts
class EwtNoPortPickedDialog (line 30) | @customElement("ewt-no-port-picked-dialog")
method render (line 34) | public render() {
method tryAgain (line 139) | private tryAgain() {
method close (line 144) | private close() {
method _handleClose (line 148) | private async _handleClose() {
type HTMLElementTagNameMap (line 173) | interface HTMLElementTagNameMap {
FILE: src/pages/ewt-page-message.ts
class EwtPageMessage (line 4) | class EwtPageMessage extends LitElement {
method render (line 9) | render() {
type HTMLElementTagNameMap (line 32) | interface HTMLElementTagNameMap {
FILE: src/pages/ewt-page-progress.ts
class EwtPageProgress (line 5) | class EwtPageProgress extends LitElement {
method render (line 10) | render() {
type HTMLElementTagNameMap (line 40) | interface HTMLElementTagNameMap {
FILE: src/util/console-color.ts
type ConsoleState (line 1) | interface ConsoleState {
class ColoredConsole (line 13) | class ColoredConsole {
method constructor (line 26) | constructor(public targetElement: HTMLElement) {}
method logs (line 28) | logs(): string {
method processLine (line 32) | processLine(line: string): Element {
method processLines (line 175) | processLines() {
method addLine (line 210) | addLine(line: string) {
FILE: src/util/line-break-transformer.ts
class LineBreakTransformer (line 1) | class LineBreakTransformer implements Transformer<string, string> {
method transform (line 4) | transform(
method flush (line 16) | flush(controller: TransformStreamDefaultController<string>) {
FILE: src/util/timestamp-transformer.ts
class TimestampTransformer (line 1) | class TimestampTransformer implements Transformer<string, string> {
method transform (line 2) | transform(
Condensed preview — 52 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (157K chars).
[
{
"path": ".devcontainer/Dockerfile",
"chars": 842,
"preview": "# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.177.0/containers/typescript-no"
},
{
"path": ".devcontainer/devcontainer.json",
"chars": 1348,
"preview": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:\n// https://github.co"
},
{
"path": ".github/dependabot.yml",
"chars": 203,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"github-actions\"\n directory: \"/\"\n schedule:\n interval: weekly\n - "
},
{
"path": ".github/release-drafter.yml",
"chars": 207,
"preview": "categories:\n - title: \"Breaking Changes\"\n labels:\n - \"breaking change\"\n - title: \"Dependencies\"\n collapse-a"
},
{
"path": ".github/workflows/ci.yml",
"chars": 729,
"preview": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versi"
},
{
"path": ".github/workflows/npmpublish.yml",
"chars": 855,
"preview": "name: Node.js Package\n\non:\n release:\n types: [published]\n\njobs:\n publish-npm:\n runs-on: ubuntu-latest\n permis"
},
{
"path": ".github/workflows/release-drafter.yml",
"chars": 245,
"preview": "name: Release Drafter\n\non:\n push:\n branches:\n - main\n\njobs:\n update_release_draft:\n runs-on: ubuntu-latest\n"
},
{
"path": ".gitignore",
"chars": 18,
"preview": "dist\nnode_modules\n"
},
{
"path": ".npmignore",
"chars": 31,
"preview": "index.html\nfirmware_build\ndemo\n"
},
{
"path": ".prettierignore",
"chars": 11,
"preview": "src/vendor\n"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 2074,
"preview": "# ESP Web Tools\n\nAllow flashing ESPHome or other ESP-based firmwares via the browser. Will automatically detect the boar"
},
{
"path": "index.html",
"chars": 24206,
"preview": "<!doctype html>\n<html>\n <head>\n <meta charset=\"utf-8\" />\n <title>ESP Web Tools</title>\n <meta\n name=\"desc"
},
{
"path": "package.json",
"chars": 989,
"preview": "{\n \"name\": \"esp-web-tools\",\n \"version\": \"0.0.0\",\n \"description\": \"Web tools for ESP devices\",\n \"main\": \"dist/install"
},
{
"path": "rollup.config.mjs",
"chars": 1073,
"preview": "import nodeResolve from \"@rollup/plugin-node-resolve\";\nimport json from \"@rollup/plugin-json\";\nimport terser from \"@roll"
},
{
"path": "script/build",
"chars": 217,
"preview": "# Stop on errors\nset -e\n\ncd \"$(dirname \"$0\")/..\"\necho 'export const version =' `jq .version package.json`\";\" > src/versi"
},
{
"path": "script/develop",
"chars": 326,
"preview": "# Stop on errors\nset -e\n\nif [ -z \"$PORT\" ]; then\n PORT=5001\nfi\n\ncd \"$(dirname \"$0\")/..\"\n\nrm -rf dist\n\n# Quit all backgr"
},
{
"path": "script/stubgen.py",
"chars": 21982,
"preview": "import base64\nimport zlib\nimport json\n\nstubs = {\n\"esp8266\": b\"\"\"\neNq9PHt/1Da2X8WehCQzJEWyPR6ZxzKZJNNQoIVwSeluehtbtunlVyg"
},
{
"path": "src/components/ew-checkbox.ts",
"chars": 374,
"preview": "import { Checkbox } from \"@material/web/checkbox/internal/checkbox.js\";\nimport { styles } from \"@material/web/checkbox/i"
},
{
"path": "src/components/ew-circular-progress.ts",
"chars": 450,
"preview": "import { CircularProgress } from \"@material/web/progress/internal/circular-progress.js\";\nimport { styles } from \"@materi"
},
{
"path": "src/components/ew-dialog.ts",
"chars": 352,
"preview": "import { Dialog } from \"@material/web/dialog/internal/dialog.js\";\nimport { styles } from \"@material/web/dialog/internal/"
},
{
"path": "src/components/ew-divider.ts",
"chars": 363,
"preview": "import { Divider } from \"@material/web/divider/internal/divider.js\";\nimport { styles } from \"@material/web/divider/inter"
},
{
"path": "src/components/ew-filled-select.ts",
"chars": 513,
"preview": "import { FilledSelect } from \"@material/web/select/internal/filled-select.js\";\nimport { styles } from \"@material/web/sel"
},
{
"path": "src/components/ew-filled-text-field.ts",
"chars": 677,
"preview": "import { styles as filledStyles } from \"@material/web/textfield/internal/filled-styles.js\";\nimport { FilledTextField } f"
},
{
"path": "src/components/ew-icon-button.ts",
"chars": 504,
"preview": "import { IconButton } from \"@material/web/iconbutton/internal/icon-button.js\";\nimport { styles as sharedStyles } from \"@"
},
{
"path": "src/components/ew-list-item.ts",
"chars": 402,
"preview": "import { ListItemEl as ListItem } from \"@material/web/list/internal/listitem/list-item.js\";\nimport { styles } from \"@mat"
},
{
"path": "src/components/ew-list.ts",
"chars": 330,
"preview": "import { List } from \"@material/web/list/internal/list.js\";\nimport { styles } from \"@material/web/list/internal/list-sty"
},
{
"path": "src/components/ew-select-option.ts",
"chars": 430,
"preview": "import { styles } from \"@material/web/menu/internal/menuitem/menu-item-styles.js\";\nimport { SelectOptionEl } from \"@mate"
},
{
"path": "src/components/ew-text-button.ts",
"chars": 506,
"preview": "import { styles as sharedStyles } from \"@material/web/button/internal/shared-styles.js\";\nimport { TextButton } from \"@ma"
},
{
"path": "src/components/ewt-console.ts",
"chars": 4620,
"preview": "import { ColoredConsole, coloredConsoleStyles } from \"../util/console-color\";\nimport { sleep } from \"../util/sleep\";\nimp"
},
{
"path": "src/components/ewt-dialog.ts",
"chars": 467,
"preview": "import { DialogBase } from \"@material/mwc-dialog/mwc-dialog-base\";\nimport { styles } from \"@material/mwc-dialog/mwc-dial"
},
{
"path": "src/components/svg.ts",
"chars": 3979,
"preview": "import { svg } from \"lit\";\n\nexport const closeIcon = svg`\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\n <path\n "
},
{
"path": "src/connect.ts",
"chars": 1006,
"preview": "import type { InstallButton } from \"./install-button.js\";\n\nexport const connect = async (button: InstallButton) => {\n i"
},
{
"path": "src/const.ts",
"chars": 2465,
"preview": "export interface Logger {\n log(msg: string, ...args: any[]): void;\n error(msg: string, ...args: any[]): void;\n debug("
},
{
"path": "src/flash.ts",
"chars": 6037,
"preview": "import { Transport, ESPLoader } from \"esptool-js\";\nimport {\n Build,\n FlashError,\n FlashState,\n Manifest,\n FlashStat"
},
{
"path": "src/install-button.ts",
"chars": 3049,
"preview": "import type { FlashState } from \"./const\";\nimport type { EwtInstallDialog } from \"./install-dialog\";\nimport { connect } "
},
{
"path": "src/install-dialog.ts",
"chars": 33266,
"preview": "import { LitElement, html, PropertyValues, css, TemplateResult } from \"lit\";\nimport { state } from \"lit/decorators.js\";\n"
},
{
"path": "src/no-port-picked/index.ts",
"chars": 287,
"preview": "import \"./no-port-picked-dialog\";\n\nexport const openNoPortPickedDialog = async (\n doTryAgain?: () => void,\n): Promise<b"
},
{
"path": "src/no-port-picked/no-port-picked-dialog.ts",
"chars": 6186,
"preview": "import { LitElement, html, css, svg } from \"lit\";\nimport { customElement } from \"lit/decorators.js\";\nimport \"../componen"
},
{
"path": "src/pages/ewt-page-message.ts",
"chars": 703,
"preview": "import { LitElement, html, css, TemplateResult } from \"lit\";\nimport { property } from \"lit/decorators.js\";\n\nclass EwtPag"
},
{
"path": "src/pages/ewt-page-progress.ts",
"chars": 1047,
"preview": "import { LitElement, html, css, TemplateResult } from \"lit\";\nimport { property } from \"lit/decorators.js\";\nimport \"../co"
},
{
"path": "src/styles.ts",
"chars": 943,
"preview": "import { css } from \"lit\";\n\n// We set font-size to 16px and all the mdc typography styles\n// because it defaults to rem,"
},
{
"path": "src/util/console-color.ts",
"chars": 7980,
"preview": "interface ConsoleState {\n bold: boolean;\n italic: boolean;\n underline: boolean;\n strikethrough: boolean;\n foregroun"
},
{
"path": "src/util/file-download.ts",
"chars": 538,
"preview": "export const fileDownload = (href: string, filename = \"\"): void => {\n const a = document.createElement(\"a\");\n a.target"
},
{
"path": "src/util/fire-event.ts",
"chars": 591,
"preview": "export const fireEvent = <Event extends keyof HTMLElementEventMap>(\n eventTarget: EventTarget,\n type: Event,\n // @ts-"
},
{
"path": "src/util/get-operating-system.ts",
"chars": 822,
"preview": "// From https://stackoverflow.com/a/38241481\nexport const getOperatingSystem = () => {\n const userAgent = window.naviga"
},
{
"path": "src/util/line-break-transformer.ts",
"chars": 652,
"preview": "export class LineBreakTransformer implements Transformer<string, string> {\n private chunks = \"\";\n\n transform(\n chun"
},
{
"path": "src/util/manifest.ts",
"chars": 571,
"preview": "import { Manifest } from \"../const\";\n\nexport const downloadManifest = async (manifestPath: string) => {\n const manifest"
},
{
"path": "src/util/sleep.ts",
"chars": 94,
"preview": "export const sleep = (time: number) =>\n new Promise((resolve) => setTimeout(resolve, time));\n"
},
{
"path": "src/util/timestamp-transformer.ts",
"chars": 439,
"preview": "export class TimestampTransformer implements Transformer<string, string> {\n transform(\n chunk: string,\n controlle"
},
{
"path": "src/version.ts",
"chars": 30,
"preview": "export const version = \"dev\";\n"
},
{
"path": "tsconfig.json",
"chars": 496,
"preview": "{\n \"compilerOptions\": {\n \"lib\": [\"es2019\", \"dom\"],\n \"target\": \"es2019\",\n \"module\": \"es2020\",\n \"moduleResolu"
}
]
About this extraction
This page contains the full source code of the esphome/esp-web-tools GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 52 files (144.4 KB), approximately 48.1k tokens, and a symbol index with 100 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.