Compare commits
326 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2c6c705018 | ||
![]() |
dedf3ef1f7 | ||
![]() |
11a1574849 | ||
![]() |
c1b2732ca9 | ||
![]() |
479c496240 | ||
![]() |
003bd76912 | ||
![]() |
789ad03a6f | ||
![]() |
ab718869a0 | ||
![]() |
f301c4c055 | ||
![]() |
50e3fe5771 | ||
![]() |
b25f250c90 | ||
![]() |
c2894b8f4c | ||
![]() |
e06c3f3f1a | ||
![]() |
b46f3f298d | ||
![]() |
7bc7959cf3 | ||
![]() |
bd670d1510 | ||
![]() |
ce5650244d | ||
![]() |
cd74083edf | ||
![]() |
b328166ea4 | ||
![]() |
350a696012 | ||
![]() |
a7c3d303b5 | ||
![]() |
f943773ae2 | ||
![]() |
32a9ff78ea | ||
![]() |
a0a98026cd | ||
![]() |
b3547b5abf | ||
![]() |
a77da4285f | ||
![]() |
2ec657d2ce | ||
![]() |
21535c6d98 | ||
![]() |
8c4bf5ca3b | ||
![]() |
0434b154da | ||
![]() |
2e3589a938 | ||
![]() |
95ab287d67 | ||
![]() |
5d19b2d5f3 | ||
![]() |
052c7828c0 | ||
![]() |
c71d7a347c | ||
![]() |
6b2ff90895 | ||
![]() |
b7b4b5beba | ||
![]() |
9261c2c24e | ||
![]() |
ec35b6aa69 | ||
![]() |
10c47dc916 | ||
![]() |
df7b4c69e0 | ||
![]() |
a69aa84df6 | ||
![]() |
9b50ac6e81 | ||
![]() |
df179092df | ||
![]() |
7de3b6a521 | ||
![]() |
e107f0f5b6 | ||
![]() |
93fe093c5e | ||
![]() |
d08bd0aac6 | ||
![]() |
86b4b88717 | ||
![]() |
fb6524c967 | ||
![]() |
21c23206d0 | ||
![]() |
cb8f72e4d0 | ||
![]() |
2d747f29e7 | ||
![]() |
1588f60e3e | ||
![]() |
9fd7cb9f35 | ||
![]() |
fff135bb4f | ||
![]() |
4527133413 | ||
![]() |
60ea3a5e2c | ||
![]() |
192dd8f47c | ||
![]() |
ac589b2717 | ||
![]() |
e948ccf9eb | ||
![]() |
0f53cc7ca9 | ||
![]() |
bf0de9fdf3 | ||
![]() |
01119fee0c | ||
![]() |
c00b93cc64 | ||
![]() |
60fb3f0c0e | ||
![]() |
2830666121 | ||
![]() |
a4b0c54f31 | ||
![]() |
2e7cd2651a | ||
![]() |
e02fd6351c | ||
![]() |
5906467412 | ||
![]() |
98ed4d00cf | ||
![]() |
305b958fcf | ||
![]() |
f7957ff6f0 | ||
![]() |
4465b0be6c | ||
![]() |
a5cb686b47 | ||
![]() |
26b38ffc6d | ||
![]() |
5beb5cf4ad | ||
![]() |
ba62801bea | ||
![]() |
fa1a4c5927 | ||
![]() |
43d2f4df97 | ||
![]() |
1bf4bffb31 | ||
![]() |
f92e0988d3 | ||
![]() |
2f736cbc80 | ||
![]() |
1e4cd0d5e3 | ||
![]() |
b3de9ecb8b | ||
![]() |
36533b9c85 | ||
![]() |
02c16906e9 | ||
![]() |
69c668facd | ||
![]() |
2efbfbf1bc | ||
![]() |
542b492f2b | ||
![]() |
b430c71264 | ||
![]() |
867ff64e07 | ||
![]() |
4694cc1d42 | ||
![]() |
d3875d4f27 | ||
![]() |
98f7641604 | ||
![]() |
c16a51bb28 | ||
![]() |
0460a4a3f3 | ||
![]() |
5390216549 | ||
![]() |
e25b30551d | ||
![]() |
63d73f7a71 | ||
![]() |
ac56f4dc20 | ||
![]() |
afd9e23abe | ||
![]() |
ccf7cefe5a | ||
![]() |
99a6320d3e | ||
![]() |
74ee646590 | ||
![]() |
0e0bc1d65d | ||
![]() |
ccf9c3be2f | ||
![]() |
a2d2f6bac1 | ||
![]() |
bccfa6c1a6 | ||
![]() |
12fc656f15 | ||
![]() |
38e1398e0f | ||
![]() |
652537eff1 | ||
![]() |
d06ee1a023 | ||
![]() |
1570b1f117 | ||
![]() |
7c8def7663 | ||
![]() |
c39d358701 | ||
![]() |
06979ee419 | ||
![]() |
53094e254e | ||
![]() |
ec93a0b15d | ||
![]() |
dbbb015d4f | ||
![]() |
d09d54be4b | ||
![]() |
f1fb9a2733 | ||
![]() |
8e0803788f | ||
![]() |
80823261e4 | ||
![]() |
495651f324 | ||
![]() |
32cdf3e752 | ||
![]() |
47f9543831 | ||
![]() |
2d0af6c9d5 | ||
![]() |
d9bcb20e3c | ||
![]() |
c39082f5d5 | ||
![]() |
34b8ff2da0 | ||
![]() |
8a71ccaa0d | ||
![]() |
ba85feb548 | ||
![]() |
39ae5dc40c | ||
![]() |
de749bd83e | ||
![]() |
98891ab73f | ||
![]() |
554d1a82b3 | ||
![]() |
55d6bdd1b2 | ||
![]() |
9bb5e0e8e8 | ||
![]() |
195c8613ed | ||
![]() |
ffe6514d6a | ||
![]() |
d6ce1dbc98 | ||
![]() |
2e9a9706df | ||
![]() |
9a7731d838 | ||
![]() |
42aa4f0ecf | ||
![]() |
882154d6ed | ||
![]() |
dc739701ed | ||
![]() |
9fee26fbab | ||
![]() |
1a8bee6604 | ||
![]() |
67dca17e0b | ||
![]() |
a64f1f0f40 | ||
![]() |
5496383bb8 | ||
![]() |
086921e5ed | ||
![]() |
8c17d20aea | ||
![]() |
4e19973bb1 | ||
![]() |
42b6d3bb8f | ||
![]() |
d5be41ea57 | ||
![]() |
28e3867582 | ||
![]() |
d3ca1ba308 | ||
![]() |
df2975dbff | ||
![]() |
26482848bd | ||
![]() |
8001808ddf | ||
![]() |
0803c830fe | ||
![]() |
744791d3d4 | ||
![]() |
013fe768ca | ||
![]() |
38d6102768 | ||
![]() |
d273fbfc84 | ||
![]() |
7b4af648db | ||
![]() |
b3f47371d7 | ||
![]() |
f1085f4fa4 | ||
![]() |
ac2014217c | ||
![]() |
c7200294db | ||
![]() |
ce9452b214 | ||
![]() |
60c7b4ec67 | ||
![]() |
059f4a821a | ||
![]() |
5ac98c6193 | ||
![]() |
63e097eb78 | ||
![]() |
dc72cca569 | ||
![]() |
0e538c5c25 | ||
![]() |
4125bda687 | ||
![]() |
aedc15d4dc | ||
![]() |
9b8b479e6c | ||
![]() |
8709b8a0e4 | ||
![]() |
35af1e8401 | ||
![]() |
6372226a9c | ||
![]() |
90a415d669 | ||
![]() |
5f9a966b82 | ||
![]() |
9a58abb396 | ||
![]() |
e1593936df | ||
![]() |
e12a4f9467 | ||
![]() |
678a9c0f06 | ||
![]() |
9b674a4433 | ||
![]() |
c6e3cfd1c5 | ||
![]() |
0cc297fe3b | ||
![]() |
7381cd456a | ||
![]() |
b91f4f167b | ||
![]() |
98c5b8e158 | ||
![]() |
11ae293176 | ||
![]() |
627b106e18 | ||
![]() |
d87f076071 | ||
![]() |
e6bf67b6fd | ||
![]() |
d0efbac292 | ||
![]() |
c90e17a5ff | ||
![]() |
95b8efa500 | ||
![]() |
22c0a1a1cb | ||
![]() |
8a7176b93f | ||
![]() |
f34fab1c0f | ||
![]() |
ec2a6e7206 | ||
![]() |
083c43877a | ||
![]() |
b46d3be794 | ||
![]() |
8a2bcd28b1 | ||
![]() |
69d5134f91 | ||
![]() |
bd5582619b | ||
![]() |
d09e0340e4 | ||
![]() |
473a9f09b7 | ||
![]() |
38128d2eb0 | ||
![]() |
a06a9ddf64 | ||
![]() |
137cae89cb | ||
![]() |
e9a90e6082 | ||
![]() |
d42bae9f23 | ||
![]() |
7c7618c372 | ||
![]() |
ecf2cf700e | ||
![]() |
5639d2a806 | ||
![]() |
b92ab703ea | ||
![]() |
92b18e3487 | ||
![]() |
1571c31aa4 | ||
![]() |
b5f82ff461 | ||
![]() |
0fc225990e | ||
![]() |
f5bde3d02c | ||
![]() |
9eca2c2c2c | ||
![]() |
edd3b9e133 | ||
![]() |
3448bc17ab | ||
![]() |
18c2053995 | ||
![]() |
176c228f86 | ||
![]() |
4076d6f3a0 | ||
![]() |
7e36700b82 | ||
![]() |
d5ed1ee63c | ||
![]() |
28ab2f91bf | ||
![]() |
13cf6960fc | ||
![]() |
0c0bfa1e25 | ||
![]() |
4912ba3e60 | ||
![]() |
58f4e08770 | ||
![]() |
a7cd43840f | ||
![]() |
7bfe33c3a8 | ||
![]() |
60680ee2b6 | ||
![]() |
f96f7460dd | ||
![]() |
79f484d772 | ||
![]() |
e33a975147 | ||
![]() |
27379335b8 | ||
![]() |
7814e30b94 | ||
![]() |
eb53aa095e | ||
![]() |
8acb9633fa | ||
![]() |
3e01ff955b | ||
![]() |
99606d75fe | ||
![]() |
f1fdfd5e25 | ||
![]() |
da840075df | ||
![]() |
f2bccb67ba | ||
![]() |
54a6887089 | ||
![]() |
921a17dd32 | ||
![]() |
7903e788f0 | ||
![]() |
0c871f863e | ||
![]() |
4005d4e846 | ||
![]() |
fe329ee0ec | ||
![]() |
59bdf42d31 | ||
![]() |
67be8d0255 | ||
![]() |
c972e19e6b | ||
![]() |
471395d692 | ||
![]() |
e7fbbd0a19 | ||
![]() |
d8e10ff8c9 | ||
![]() |
4f6724a74f | ||
![]() |
64e82d2dff | ||
![]() |
a022af5502 | ||
![]() |
d784aa2bd1 | ||
![]() |
3f60b2ed43 | ||
![]() |
4d9f07da9f | ||
![]() |
3ddcacd0ce | ||
![]() |
014df385ad | ||
![]() |
7e96b75c88 | ||
![]() |
ebef0688c8 | ||
![]() |
58c76e98bc | ||
![]() |
d81096c472 | ||
![]() |
0266057a38 | ||
![]() |
c4daee18bc | ||
![]() |
10816b5166 | ||
![]() |
a5217eb799 | ||
![]() |
cfd56b789f | ||
![]() |
78cb70de53 | ||
![]() |
3a49857db3 | ||
![]() |
f2008627ce | ||
![]() |
c184b5456b | ||
![]() |
e0a8d753d4 | ||
![]() |
25eb15e1a2 | ||
![]() |
a8b75c89eb | ||
![]() |
0a12525fb4 | ||
![]() |
e4f14746c9 | ||
![]() |
f3cb744b4f | ||
![]() |
91169c1dfa | ||
![]() |
6f2bd27d16 | ||
![]() |
db78540213 | ||
![]() |
b2cd8eaf4e | ||
![]() |
a29b0c82da | ||
![]() |
34d73756b2 | ||
![]() |
27961ccbd4 | ||
![]() |
91dbdf86c1 | ||
![]() |
56b6cf1db7 | ||
![]() |
c0d52a4c2f | ||
![]() |
89a3a4c54a | ||
![]() |
ddbe525c99 | ||
![]() |
a8dc857f26 | ||
![]() |
66540245c9 | ||
![]() |
c33084d48a | ||
![]() |
5e30f3bcd3 | ||
![]() |
4f2fa22a96 | ||
![]() |
b81c1daf1e | ||
![]() |
f4a6d68837 | ||
![]() |
4c58f97981 | ||
![]() |
87c95de8f0 | ||
![]() |
2155ad6439 | ||
![]() |
1a39400cfc | ||
![]() |
3a90a3f3b6 | ||
![]() |
6e2976c229 | ||
![]() |
1a18108a55 | ||
![]() |
c0813275b0 | ||
![]() |
65b0fab0c5 | ||
![]() |
eb4ebfa17b |
@ -4,9 +4,9 @@
|
||||
"name": "Node.js & TypeScript",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
// Update 'VARIANT' to pick a Node version: 12, 14, 16
|
||||
// Update 'VARIANT' to pick a Node version: 12, 14, 16, 18, 20
|
||||
"args": {
|
||||
"VARIANT": "16"
|
||||
"VARIANT": "20"
|
||||
}
|
||||
},
|
||||
|
||||
|
8
.github/release-drafter.yml
vendored
@ -1,3 +1,11 @@
|
||||
categories:
|
||||
- title: "Breaking Changes"
|
||||
labels:
|
||||
- "breaking change"
|
||||
- title: "Dependencies"
|
||||
collapse-after: 1
|
||||
labels:
|
||||
- "dependencies"
|
||||
template: |
|
||||
## What's Changed
|
||||
|
||||
|
8
.github/workflows/ci.yml
vendored
@ -14,9 +14,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install jq tool
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install jq
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2.4.1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm ci
|
||||
|
8
.github/workflows/npmpublish.yml
vendored
@ -11,8 +11,12 @@ jobs:
|
||||
publish-npm:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2.4.1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install jq tool
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install jq
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
2
.github/workflows/release-drafter.yml
vendored
@ -9,6 +9,6 @@ jobs:
|
||||
update_release_draft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
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.
|
63
README.md
@ -1,6 +1,6 @@
|
||||
# ESP Web Tools
|
||||
|
||||
Allow flashing ESPHome or other ESP-based firmwares via the browser. Will automatically detect the board type and select a supported firmware.
|
||||
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
|
||||
@ -8,72 +8,63 @@ Allow flashing ESPHome or other ESP-based firmwares via the browser. Will automa
|
||||
></esp-web-install-button>
|
||||
```
|
||||
|
||||
Manifest definition:
|
||||
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.bin", "offset": 4096 },
|
||||
{ "path": "bootloader_dout_40m.bin", "offset": 4096 },
|
||||
{ "path": "partitions.bin", "offset": 32768 },
|
||||
{ "path": "ota.bin", "offset": 57344 },
|
||||
{ "path": "firmware.bin", "offset": 65536 }
|
||||
{ "path": "boot_app0.bin", "offset": 57344 },
|
||||
{ "path": "esp32.bin", "offset": 65536 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"chipFamily": "ESP32-C3",
|
||||
"parts": [
|
||||
{ "path": "esp32-c3.bin", "offset": 0 },
|
||||
{ "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": "esp32-s2.bin", "offset": 0 },
|
||||
{ "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 },
|
||||
{ "path": "esp8266.bin", "offset": 0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
### Attributes
|
||||
|
||||
The following attributes are automatically added to `<esp-web-install-button>` and can be used for styling:
|
||||
|
||||
| Attribute | Description |
|
||||
| -- | -- |
|
||||
| `install-supported` | Added if installing firmware is supported
|
||||
| `install-unsupported` | Added if installing firmware is not supported
|
||||
|
||||
### CSS custom properties (variables)
|
||||
|
||||
The following variables can be used to change the colors of the default UI elements:
|
||||
|
||||
- `--esp-tools-button-color`
|
||||
- `--esp-tools-button-text-color`
|
||||
|
||||
### Slots
|
||||
|
||||
The following slots are available:
|
||||
|
||||
| Slot name | Description |
|
||||
| -- | -- |
|
||||
| `activate` | Button to start the flash progress
|
||||
| `unsupported` | Message to show when the browser is not supported
|
||||
| `not-allowed` | Message to show when not a secure context
|
||||
|
||||
## Development
|
||||
|
||||
Run `script/develop`. This starts a server. Open it on http://localhost:5001.
|
||||
|
||||
[](https://www.openhomefoundation.org/)
|
||||
|
579
index.html
@ -1,3 +1,4 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
@ -33,6 +34,7 @@
|
||||
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",
|
||||
@ -46,31 +48,60 @@
|
||||
margin: 0 auto;
|
||||
padding: 12px;
|
||||
}
|
||||
.project .logo {
|
||||
float: right;
|
||||
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: #000;
|
||||
background: #ccc;
|
||||
}
|
||||
.videoWrapper iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.content pre {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
padding-left: 8px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.footer {
|
||||
@ -100,12 +131,16 @@
|
||||
}
|
||||
}
|
||||
</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@4.0.2/dist/web/install-button.js?module"
|
||||
: "https://unpkg.com/esp-web-tools/dist/web/install-button.js?module"
|
||||
);
|
||||
</script>
|
||||
</head>
|
||||
@ -113,136 +148,267 @@
|
||||
<div class="content">
|
||||
<h1>ESP Web Tools</h1>
|
||||
<p>
|
||||
ESP Web Tools is a set of open source tools to allow working with ESP
|
||||
devices in the browser.
|
||||
<a href="https://github.com/esphome/esp-web-tools"
|
||||
>The code is available on GitHub.</a
|
||||
>
|
||||
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>
|
||||
To try it out and install
|
||||
<a href="https://esphome.io">ESPHome</a> on an ESP, connect it to your
|
||||
computer and hit the button:
|
||||
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="static/firmware_build/manifest.json"
|
||||
></esp-web-install-button>
|
||||
<p>
|
||||
<i>
|
||||
Note, this only works in desktop Chrome and Edge. Android support
|
||||
should be possible but has not been implemented yet. If you don't see
|
||||
your ESP device, you might miss <a href="#drivers">drivers</a>.
|
||||
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>
|
||||
</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>
|
||||
This works by combining
|
||||
<a
|
||||
href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API"
|
||||
>Web Serial</a
|
||||
>
|
||||
with a manifest which describes the firmware. It will automatically
|
||||
detect the type of the connected ESP device and find the right firmware
|
||||
files in the manifest.
|
||||
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>
|
||||
<div class="videoWrapper">
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube-nocookie.com/embed/k88BS8zgWq0"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
<h2 id="used-by">Products using ESP Web Tools</h2>
|
||||
<div class="project">
|
||||
<a href="https://wled.me" class="logo"
|
||||
><img src="static/logos/wled.png" alt="WLED logo"
|
||||
/></a>
|
||||
<h3>WLED</h3>
|
||||
<p>
|
||||
Fast and feature-rich firmware to control NeoPixel (WS2812B, WS2811,
|
||||
SK6812) LEDs and SPI based chipsets like the WS2801 and APA102.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://install.wled.me" target="_blank"
|
||||
>Installation Website</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="project">
|
||||
<a href="https://tasmota.github.io" class="logo"
|
||||
><img src="static/logos/tasmota.svg" alt="Tasmota logo"
|
||||
/></a>
|
||||
<h3>Tasmota</h3>
|
||||
<p>
|
||||
Firmware with easy configuration using webUI, OTA updates, automation
|
||||
using timers or rules, expandability and entirely local control over
|
||||
MQTT, HTTP, Serial or KNX.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://arendst.github.io/Tasmota-firmware/" target="_blank"
|
||||
>Installation Website</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="project">
|
||||
<a href="http://www.espeasy.com/" class="logo"
|
||||
><img src="static/logos/espeasy.png" alt="ESPEasy logo"
|
||||
/></a>
|
||||
<h3>ESPEasy</h3>
|
||||
<p>Easy MultiSensor device based on ESP8266/ESP32.</p>
|
||||
<p>
|
||||
<a href="https://td-er.nl/ESPEasy/" target="_blank"
|
||||
>Installation Website</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="project">
|
||||
<a href="https://esphome.io" class="logo"
|
||||
><img src="static/logos/esphome.svg" alt="ESPHome logo"
|
||||
/></a>
|
||||
<h3>ESPHome</h3>
|
||||
<p>
|
||||
No-code platform for ESP devices. Uses ESP Web Tools in their
|
||||
dashboard to install ESPHome on devices.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://esphome.io" target="_blank">Website</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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, create a manifest and add the button to
|
||||
your website. Make sure you update the manifest attribute to point at
|
||||
your manifest.
|
||||
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>
|
||||
You can import ESP Web Tools directly from the unpkg CDN or
|
||||
<a href="https://unpkg.com/browse/esp-web-tools/dist/web/"
|
||||
>download the files</a
|
||||
<a href="https://github.com/balloob/squeezelite-esp32-install"
|
||||
>Click here to see a full example.</a
|
||||
>
|
||||
and put them on your website.
|
||||
</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@4.0.2/dist/web/install-button.js?module"
|
||||
></script>
|
||||
|
||||
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="static/firmware_build/manifest.json"
|
||||
manifest="https://firmware.esphome.io/esp-web-tools/manifest.json"
|
||||
></esp-web-install-button></pre
|
||||
>
|
||||
<p>
|
||||
Your website needs to be served over <code>https://</code>. If your
|
||||
manifest is hosted on another server, make sure you configure
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS"
|
||||
<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
|
||||
>
|
||||
for your manifest and firmware files such that your website is allowed
|
||||
to fetch those files by adding the header
|
||||
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
|
||||
>.
|
||||
@ -250,30 +416,66 @@
|
||||
|
||||
<p>
|
||||
ESP Web Tools can also be integrated in your projects by installing it
|
||||
via NPM:<br />
|
||||
<code>npm install --save esp-web-tools</code>
|
||||
<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>
|
||||
ESP Web Tools manifest describe the firmware that you want to install.
|
||||
It allows specifying different builds for the different types of ESP
|
||||
devices. Current supported chip families are <code>ESP8266</code>,
|
||||
<code>ESP32</code>, <code>ESP32C3</code> and <code>ESP32S2</code>. The
|
||||
correct build will be automatically selected based on the type of the
|
||||
ESP device we detect via the serial port.
|
||||
Manifests describe the firmware that you want to offer the user to
|
||||
install. It allows specifying different builds for the different types
|
||||
of ESP devices. Current supported chip families are
|
||||
<code>ESP8266</code>, <code>ESP32</code>, <code>ESP32-C2</code>,
|
||||
<code>ESP32-C3</code>, <code>ESP32-C6</code>, <code>ESP32-H2</code>,
|
||||
<code>ESP32-S2</code> and <code>ESP32-S3</code>. The correct build will
|
||||
be automatically selected based on the type of the connected ESP device.
|
||||
</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": "bootloader.bin", "offset": 4096 },
|
||||
{ "path": "partitions.bin", "offset": 32768 },
|
||||
{ "path": "ota.bin", "offset": 57344 },
|
||||
{ "path": "firmware.bin", "offset": 65536 }
|
||||
{ "path": "merged-firmware.bin", "offset": 0 },
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -286,32 +488,65 @@
|
||||
}</pre
|
||||
>
|
||||
<p>
|
||||
Each build contains a list of parts to be flashed to the ESP device.
|
||||
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>
|
||||
<h3 id="improv">Wi-Fi provisioning</h3>
|
||||
<p>
|
||||
ESP Web Tools has support for the
|
||||
<a href="https://www.improv-wifi.com/serial"
|
||||
>Improv Wi-Fi serial standard</a
|
||||
>. This is an open standard to allow configuring Wi-Fi via the serial
|
||||
port.
|
||||
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>
|
||||
If Improv is supported, a user will be guided to connect the device to
|
||||
the network after installation. It also allows the user to connect
|
||||
already installed devices and re-configure the wireless network
|
||||
settings.
|
||||
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
|
||||
>
|
||||
|
||||
<h3 id="customize">Customizing the look and feel</h3>
|
||||
<p>
|
||||
There are multiple options to change the look and feel of the button and
|
||||
other elements.
|
||||
</p>
|
||||
<h4>Change colors</h4>
|
||||
<p>
|
||||
You can change the colors of the default UI elements with CSS custom
|
||||
properties (variables), the following variables are available:
|
||||
@ -319,6 +554,7 @@
|
||||
<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>
|
||||
@ -342,7 +578,7 @@
|
||||
</p>
|
||||
<pre>
|
||||
<esp-web-install-button
|
||||
manifest="static/firmware_build/manifest.json"
|
||||
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>
|
||||
@ -350,40 +586,35 @@
|
||||
</esp-web-install-button>
|
||||
</pre
|
||||
>
|
||||
<h3 id="drivers">USB Serial Drivers</h3>
|
||||
<p>
|
||||
If the serial port is not showing up, your computer might be missing the
|
||||
drivers for the USB serial chip used in your ESP device. These drivers
|
||||
work for most ESP devices:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
CP2102 (square chip):
|
||||
<a
|
||||
href="https://www.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers"
|
||||
>driver</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
CH341:
|
||||
<a
|
||||
href="https://github.com/nodemcu/nodemcu-devkit/tree/master/Drivers"
|
||||
>driver</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<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>
|
||||
ESP Web Tools –
|
||||
<a href="https://github.com/esphome/esp-web-tools">GitHub</a>
|
||||
</div>
|
||||
<div class="initiative">
|
||||
ESP Web Tools is a project by
|
||||
<a href="https://esphome.io">ESPHome</a>.<br />
|
||||
<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");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
9176
package-lock.json
generated
42
package.json
@ -1,36 +1,34 @@
|
||||
{
|
||||
"name": "esp-web-tools",
|
||||
"version": "4.0.2",
|
||||
"version": "10.1.0",
|
||||
"description": "Web tools for ESP devices",
|
||||
"main": "dist/install-button.js",
|
||||
"repository": "https://github.com/esphome/web",
|
||||
"repository": "https://github.com/esphome/esp-web-tools",
|
||||
"author": "ESPHome maintainers",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"prepublishOnly": "script/build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
||||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@types/w3c-web-serial": "^1.0.1",
|
||||
"prettier": "^2.3.0",
|
||||
"rollup": "^2.50.2",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"serve": "^13.0.2",
|
||||
"typescript": "^4.3.2"
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-commonjs": "^28.0.2",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/w3c-web-serial": "^1.0.7",
|
||||
"prettier": "^3.4.2",
|
||||
"rollup": "^4.29.1",
|
||||
"serve": "^14.2.4",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@material/mwc-button": "^0.25.3",
|
||||
"@material/mwc-checkbox": "^0.25.3",
|
||||
"@material/mwc-circular-progress": "^0.25.3",
|
||||
"@material/mwc-dialog": "^0.25.3",
|
||||
"@material/mwc-icon-button": "^0.25.3",
|
||||
"@material/mwc-linear-progress": "^0.25.1",
|
||||
"@material/mwc-textfield": "^0.25.3",
|
||||
"esp-web-flasher": "^4.0.0",
|
||||
"improv-wifi-serial-sdk": "^1.0.0",
|
||||
"lit": "^2.0.0",
|
||||
"tslib": "^2.3.1"
|
||||
"@material/web": "^2.2.0",
|
||||
"esptool-js": "^0.5.3",
|
||||
"improv-wifi-serial-sdk": "^2.5.0",
|
||||
"lit": "^3.2.1",
|
||||
"pako": "^2.1.0",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
||||
import json from "@rollup/plugin-json";
|
||||
import { terser } from "rollup-plugin-terser";
|
||||
|
||||
const config = {
|
||||
input: "dist/install-button.js",
|
||||
output: {
|
||||
dir: "dist/web",
|
||||
format: "module",
|
||||
},
|
||||
external: ["https://www.improv-wifi.com/sdk-js/launch-button.js"],
|
||||
preserveEntrySignatures: false,
|
||||
plugins: [nodeResolve(), json()],
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
config.plugins.push(
|
||||
terser({
|
||||
ecma: 2019,
|
||||
toplevel: true,
|
||||
output: {
|
||||
comments: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default config;
|
51
rollup.config.mjs
Normal file
@ -0,0 +1,51 @@
|
||||
import nodeResolve from "@rollup/plugin-node-resolve";
|
||||
import json from "@rollup/plugin-json";
|
||||
import terser from "@rollup/plugin-terser";
|
||||
import babel from "@rollup/plugin-babel";
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
|
||||
const config = {
|
||||
input: "dist/install-button.js",
|
||||
output: {
|
||||
dir: "dist/web",
|
||||
format: "module",
|
||||
},
|
||||
external: ["https://www.improv-wifi.com/sdk-js/launch-button.js"],
|
||||
preserveEntrySignatures: false,
|
||||
plugins: [
|
||||
commonjs(),
|
||||
nodeResolve({
|
||||
browser: true,
|
||||
preferBuiltins: false,
|
||||
}),
|
||||
babel({
|
||||
babelHelpers: "bundled",
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
targets: {
|
||||
// We use unpkg as CDN and it doesn't bundle modern syntax
|
||||
chrome: "84",
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
json(),
|
||||
],
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
config.plugins.push(
|
||||
terser({
|
||||
ecma: 2019,
|
||||
toplevel: true,
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default config;
|
@ -2,6 +2,7 @@
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
echo 'export const version =' `jq .version package.json`";" > src/version.ts
|
||||
|
||||
rm -rf dist
|
||||
NODE_ENV=production npm exec -- tsc
|
||||
|
@ -1,6 +1,10 @@
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
if [ -z "$PORT" ]; then
|
||||
PORT=5001
|
||||
fi
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
rm -rf dist
|
||||
@ -9,9 +13,8 @@ rm -rf dist
|
||||
trap "kill 0" EXIT
|
||||
|
||||
# Run tsc once as rollup expects those files
|
||||
tsc || true
|
||||
npm exec -- tsc || true
|
||||
|
||||
npm exec -- serve -p 5001 &
|
||||
npm exec -- serve -p "$PORT" &
|
||||
npm exec -- tsc --watch &
|
||||
npm exec -- rollup -c --watch &
|
||||
wait
|
||||
npm exec -- rollup -c --watch
|
||||
|
14
src/components/ew-checkbox.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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);
|
14
src/components/ew-circular-progress.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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);
|
14
src/components/ew-dialog.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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);
|
14
src/components/ew-divider.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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);
|
15
src/components/ew-filled-select.ts
Normal file
@ -0,0 +1,15 @@
|
||||
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);
|
17
src/components/ew-filled-text-field.ts
Normal file
@ -0,0 +1,17 @@
|
||||
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);
|
15
src/components/ew-icon-button.ts
Normal file
@ -0,0 +1,15 @@
|
||||
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);
|
14
src/components/ew-list-item.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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);
|
14
src/components/ew-list.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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);
|
14
src/components/ew-select-option.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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);
|
15
src/components/ew-text-button.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { styles as sharedStyles } from "@material/web/button/internal/shared-styles.js";
|
||||
import { TextButton } from "@material/web/button/internal/text-button.js";
|
||||
import { styles as textStyles } from "@material/web/button/internal/text-styles.js";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ew-text-button": EwTextButton;
|
||||
}
|
||||
}
|
||||
|
||||
export class EwTextButton extends TextButton {
|
||||
static override styles = [sharedStyles, textStyles];
|
||||
}
|
||||
|
||||
customElements.define("ew-text-button", EwTextButton);
|
@ -1,14 +0,0 @@
|
||||
import { ButtonBase } from "@material/mwc-button/mwc-button-base";
|
||||
import { styles } from "@material/mwc-button/styles.css";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ewt-button": EwtButton;
|
||||
}
|
||||
}
|
||||
|
||||
export class EwtButton extends ButtonBase {
|
||||
static override styles = [styles];
|
||||
}
|
||||
|
||||
customElements.define("ewt-button", EwtButton);
|
@ -1,14 +0,0 @@
|
||||
import { CircularProgressBase } from "@material/mwc-circular-progress/mwc-circular-progress-base";
|
||||
import { styles } from "@material/mwc-circular-progress/mwc-circular-progress.css";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ewt-circular-progress": EwtCircularProgress;
|
||||
}
|
||||
}
|
||||
|
||||
export class EwtCircularProgress extends CircularProgressBase {
|
||||
static override styles = [styles];
|
||||
}
|
||||
|
||||
customElements.define("ewt-circular-progress", EwtCircularProgress);
|
@ -1,15 +1,21 @@
|
||||
import { ColoredConsole } from "../util/console-color";
|
||||
import { ColoredConsole, coloredConsoleStyles } from "../util/console-color";
|
||||
import { sleep } from "../util/sleep";
|
||||
import { LineBreakTransformer } from "../util/line-break-transformer";
|
||||
import { TimestampTransformer } from "../util/timestamp-transformer";
|
||||
import { Logger } from "../const";
|
||||
|
||||
export class EwtConsole extends HTMLElement {
|
||||
public port!: SerialPort;
|
||||
public logger!: Logger;
|
||||
public allowInput = true;
|
||||
|
||||
private _console?: ColoredConsole;
|
||||
private _cancelConnection?: () => Promise<void>;
|
||||
|
||||
public logs(): string {
|
||||
return this._console?.logs() || "";
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
if (this._console) {
|
||||
return;
|
||||
@ -24,90 +30,8 @@ export class EwtConsole extends HTMLElement {
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
|
||||
monospace;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.log {
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 28px);
|
||||
font-size: 12px;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.log-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.log-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.log-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.log-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.log-underline.log-strikethrough {
|
||||
text-decoration: underline line-through;
|
||||
}
|
||||
.log-secret {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.log-secret-redacted {
|
||||
opacity: 0;
|
||||
width: 1px;
|
||||
font-size: 1px;
|
||||
}
|
||||
.log-fg-black {
|
||||
color: rgb(128, 128, 128);
|
||||
}
|
||||
.log-fg-red {
|
||||
color: rgb(255, 0, 0);
|
||||
}
|
||||
.log-fg-green {
|
||||
color: rgb(0, 255, 0);
|
||||
}
|
||||
.log-fg-yellow {
|
||||
color: rgb(255, 255, 0);
|
||||
}
|
||||
.log-fg-blue {
|
||||
color: rgb(0, 0, 255);
|
||||
}
|
||||
.log-fg-magenta {
|
||||
color: rgb(255, 0, 255);
|
||||
}
|
||||
.log-fg-cyan {
|
||||
color: rgb(0, 255, 255);
|
||||
}
|
||||
.log-fg-white {
|
||||
color: rgb(187, 187, 187);
|
||||
}
|
||||
.log-bg-black {
|
||||
background-color: rgb(0, 0, 0);
|
||||
}
|
||||
.log-bg-red {
|
||||
background-color: rgb(255, 0, 0);
|
||||
}
|
||||
.log-bg-green {
|
||||
background-color: rgb(0, 255, 0);
|
||||
}
|
||||
.log-bg-yellow {
|
||||
background-color: rgb(255, 255, 0);
|
||||
}
|
||||
.log-bg-blue {
|
||||
background-color: rgb(0, 0, 255);
|
||||
}
|
||||
.log-bg-magenta {
|
||||
background-color: rgb(255, 0, 255);
|
||||
}
|
||||
.log-bg-cyan {
|
||||
background-color: rgb(0, 255, 255);
|
||||
}
|
||||
.log-bg-white {
|
||||
background-color: rgb(255, 255, 255);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
@ -121,27 +45,40 @@ export class EwtConsole extends HTMLElement {
|
||||
border: 0;
|
||||
outline: none;
|
||||
}
|
||||
${coloredConsoleStyles}
|
||||
</style>
|
||||
<div class="log"></div>
|
||||
<form>
|
||||
>
|
||||
<input autofocus>
|
||||
<button type="button">Send</button>
|
||||
</form>
|
||||
${
|
||||
this.allowInput
|
||||
? `<form>
|
||||
>
|
||||
<input autofocus>
|
||||
</form>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
|
||||
this._console = new ColoredConsole(this.shadowRoot!.querySelector("div")!);
|
||||
const input = this.shadowRoot!.querySelector("input")!;
|
||||
|
||||
this.addEventListener("click", () => input.focus());
|
||||
if (this.allowInput) {
|
||||
const input = this.shadowRoot!.querySelector("input")!;
|
||||
|
||||
input.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "Enter") {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._sendCommand();
|
||||
}
|
||||
});
|
||||
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);
|
||||
@ -159,12 +96,13 @@ export class EwtConsole extends HTMLElement {
|
||||
signal: abortSignal,
|
||||
})
|
||||
.pipeThrough(new TransformStream(new LineBreakTransformer()))
|
||||
.pipeThrough(new TransformStream(new TimestampTransformer()))
|
||||
.pipeTo(
|
||||
new WritableStream({
|
||||
write: (chunk) => {
|
||||
this._console!.addLine(chunk);
|
||||
this._console!.addLine(chunk.replace("\r", ""));
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
if (!abortSignal.aborted) {
|
||||
this._console!.addLine("");
|
||||
@ -205,15 +143,17 @@ export class EwtConsole extends HTMLElement {
|
||||
}
|
||||
|
||||
public async reset() {
|
||||
this.logger.debug("Triggering reset.");
|
||||
this.logger.debug("Triggering reset");
|
||||
await this.port.setSignals({
|
||||
dataTerminalReady: false,
|
||||
requestToSend: true,
|
||||
});
|
||||
await sleep(250);
|
||||
await this.port.setSignals({
|
||||
dataTerminalReady: false,
|
||||
requestToSend: false,
|
||||
});
|
||||
await sleep(250);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
import { IconButtonBase } from "@material/mwc-icon-button/mwc-icon-button-base";
|
||||
import { styles } from "@material/mwc-icon-button/mwc-icon-button.css";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ewt-icon-button": EwtIconButton;
|
||||
}
|
||||
}
|
||||
|
||||
export class EwtIconButton extends IconButtonBase {
|
||||
static override styles = [styles];
|
||||
}
|
||||
|
||||
customElements.define("ewt-icon-button", EwtIconButton);
|
@ -1,14 +0,0 @@
|
||||
import { TextFieldBase } from "@material/mwc-textfield/mwc-textfield-base";
|
||||
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ewt-textfield": EwtTextfield;
|
||||
}
|
||||
}
|
||||
|
||||
export class EwtTextfield extends TextFieldBase {
|
||||
static override styles = [styles];
|
||||
}
|
||||
|
||||
customElements.define("ewt-textfield", EwtTextfield);
|
61
src/components/svg.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { svg } from "lit";
|
||||
|
||||
export const closeIcon = svg`
|
||||
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export const refreshIcon = svg`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export const listItemInstallIcon = svg`
|
||||
<svg slot="start" viewBox="0 0 24 24">
|
||||
<path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export const listItemWifi = svg`
|
||||
<svg slot="start" viewBox="0 0 24 24">
|
||||
<path d="M12,21L15.6,16.2C14.6,15.45 13.35,15 12,15C10.65,15 9.4,15.45 8.4,16.2L12,21M12,3C7.95,3 4.21,4.34 1.2,6.6L3,9C5.5,7.12 8.62,6 12,6C15.38,6 18.5,7.12 21,9L22.8,6.6C19.79,4.34 16.05,3 12,3M12,9C9.3,9 6.81,9.89 4.8,11.4L6.6,13.8C8.1,12.67 9.97,12 12,12C14.03,12 15.9,12.67 17.4,13.8L19.2,11.4C17.19,9.89 14.7,9 12,9Z" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export const listItemConsole = svg`
|
||||
<svg slot="start" viewBox="0 0 24 24">
|
||||
<path d="M20,19V7H4V19H20M20,3A2,2 0 0,1 22,5V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V5C2,3.89 2.9,3 4,3H20M13,17V15H18V17H13M9.58,13L5.57,9H8.4L11.7,12.3C12.09,12.69 12.09,13.33 11.7,13.72L8.42,17H5.59L9.58,13Z" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export const listItemVisitDevice = svg`
|
||||
<svg slot="start" viewBox="0 0 24 24">
|
||||
<path d="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export const listItemHomeAssistant = svg`
|
||||
<svg slot="start" viewBox="0 0 24 24">
|
||||
<path d="m12.151 1.5882c-.3262 0-.6523.1291-.8996.3867l-8.3848 8.7354c-.0619.0644-.1223.1368-.1807.2154-.0588.0789-.1151.1638-.1688.2534-.2593.4325-.4552.9749-.5232 1.4555-.0026.018-.0076.0369-.0094.0548-.0121.0987-.0184.1944-.0184.2857v8.0124a1.2731 1.2731 0 001.2731 1.2731h7.8313l-3.4484-3.593a1.7399 1.7399 0 111.0803-1.125l2.6847 2.7972v-10.248a1.7399 1.7399 0 111.5276-0v7.187l2.6702-2.782a1.7399 1.7399 0 111.0566 1.1505l-3.7269 3.8831v2.7299h8.174a1.2471 1.2471 0 001.2471-1.2471v-8.0375c0-.0912-.0059-.1868-.0184-.2855-.0603-.4935-.2636-1.0617-.5326-1.5105-.0537-.0896-.1101-.1745-.1684-.253-.0588-.079-.1191-.1513-.181-.2158l-8.3848-8.7363c-.2473-.2577-.5735-.3866-.8995-.3864" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export const listItemEraseUserData = svg`
|
||||
<svg slot="start" viewBox="0 0 24 24">
|
||||
<path d="M15,14C17.67,14 23,15.33 23,18V20H7V18C7,15.33 12.33,14 15,14M15,12A4,4 0 0,1 11,8A4,4 0 0,1 15,4A4,4 0 0,1 19,8A4,4 0 0,1 15,12M5,9.59L7.12,7.46L8.54,8.88L6.41,11L8.54,13.12L7.12,14.54L5,12.41L2.88,14.54L1.46,13.12L3.59,11L1.46,8.88L2.88,7.46L5,9.59Z" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export const listItemFundDevelopment = svg`
|
||||
<svg slot="start" viewBox="0 0 24 24">
|
||||
<path d="M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z" />
|
||||
</svg>
|
||||
`;
|
@ -1,12 +1,18 @@
|
||||
import type { InstallButton } from "./install-button.js";
|
||||
import "./install-dialog.js";
|
||||
|
||||
export const connect = async (button: InstallButton) => {
|
||||
import("./install-dialog.js");
|
||||
let port: SerialPort | undefined;
|
||||
try {
|
||||
port = await navigator.serial.requestPort();
|
||||
} catch (err) {
|
||||
console.error("User cancelled request", err);
|
||||
} 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;
|
||||
}
|
||||
|
||||
@ -14,17 +20,23 @@ export const connect = async (button: InstallButton) => {
|
||||
return;
|
||||
}
|
||||
|
||||
await port.open({ baudRate: 115200 });
|
||||
try {
|
||||
await port.open({ baudRate: 115200 });
|
||||
} catch (err: any) {
|
||||
alert(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const el = document.createElement("ewt-install-dialog");
|
||||
el.port = port;
|
||||
el.manifestPath = button.manifest || button.getAttribute("manifest")!;
|
||||
el.overrides = button.overrides;
|
||||
el.addEventListener(
|
||||
"closed",
|
||||
() => {
|
||||
port!.close();
|
||||
},
|
||||
{ once: true }
|
||||
{ once: true },
|
||||
);
|
||||
document.body.appendChild(el);
|
||||
};
|
||||
|
24
src/const.ts
@ -5,7 +5,15 @@ export interface Logger {
|
||||
}
|
||||
|
||||
export interface Build {
|
||||
chipFamily: "ESP32" | "ESP8266" | "ESP32-S2" | "ESP32-C3";
|
||||
chipFamily:
|
||||
| "ESP32"
|
||||
| "ESP32-C2"
|
||||
| "ESP32-C3"
|
||||
| "ESP32-C6"
|
||||
| "ESP32-H2"
|
||||
| "ESP32-S2"
|
||||
| "ESP32-S3"
|
||||
| "ESP8266";
|
||||
parts: {
|
||||
path: string;
|
||||
offset: number;
|
||||
@ -15,6 +23,13 @@ export interface Build {
|
||||
export interface Manifest {
|
||||
name: string;
|
||||
version: string;
|
||||
home_assistant_domain?: string;
|
||||
funding_url?: string;
|
||||
/** @deprecated use `new_install_prompt_erase` instead */
|
||||
new_install_skip_erase?: boolean;
|
||||
new_install_prompt_erase?: boolean;
|
||||
/* Time to wait to detect Improv Wi-Fi. Set to 0 to disable. */
|
||||
new_install_improv_wait_time?: number;
|
||||
builds: Build[];
|
||||
}
|
||||
|
||||
@ -31,11 +46,6 @@ export interface InitializingState extends BaseFlashState {
|
||||
details: { done: boolean };
|
||||
}
|
||||
|
||||
export interface ManifestState extends BaseFlashState {
|
||||
state: FlashStateType.MANIFEST;
|
||||
details: { done: boolean };
|
||||
}
|
||||
|
||||
export interface PreparingState extends BaseFlashState {
|
||||
state: FlashStateType.PREPARING;
|
||||
details: { done: boolean };
|
||||
@ -62,7 +72,6 @@ export interface ErrorState extends BaseFlashState {
|
||||
|
||||
export type FlashState =
|
||||
| InitializingState
|
||||
| ManifestState
|
||||
| PreparingState
|
||||
| ErasingState
|
||||
| WritingState
|
||||
@ -71,7 +80,6 @@ export type FlashState =
|
||||
|
||||
export const enum FlashStateType {
|
||||
INITIALIZING = "initializing",
|
||||
MANIFEST = "manifest",
|
||||
PREPARING = "preparing",
|
||||
ERASING = "erasing",
|
||||
WRITING = "writing",
|
||||
|
198
src/flash.ts
@ -1,4 +1,4 @@
|
||||
import { ESPLoader, Logger } from "esp-web-flasher";
|
||||
import { Transport, ESPLoader } from "esptool-js";
|
||||
import {
|
||||
Build,
|
||||
FlashError,
|
||||
@ -6,19 +6,17 @@ import {
|
||||
Manifest,
|
||||
FlashStateType,
|
||||
} from "./const";
|
||||
import { getChipFamilyName } from "./util/chip-family-name";
|
||||
import { sleep } from "./util/sleep";
|
||||
import { hardReset } from "./util/reset";
|
||||
|
||||
export const flash = async (
|
||||
onEvent: (state: FlashState) => void,
|
||||
port: SerialPort,
|
||||
logger: Logger,
|
||||
manifestPath: string,
|
||||
eraseFirst: boolean
|
||||
manifest: Manifest,
|
||||
eraseFirst: boolean,
|
||||
) => {
|
||||
let manifest: Manifest;
|
||||
let build: Build | undefined;
|
||||
let chipFamily: ReturnType<typeof getChipFamilyName>;
|
||||
let chipFamily: Build["chipFamily"];
|
||||
|
||||
const fireStateEvent = (stateUpdate: FlashState) =>
|
||||
onEvent({
|
||||
@ -28,12 +26,13 @@ export const flash = async (
|
||||
chipFamily,
|
||||
});
|
||||
|
||||
const manifestURL = new URL(manifestPath, location.toString()).toString();
|
||||
const manifestProm = fetch(manifestURL).then(
|
||||
(resp): Promise<Manifest> => resp.json()
|
||||
);
|
||||
|
||||
const esploader = new ESPLoader(port, logger);
|
||||
const transport = new Transport(port);
|
||||
const esploader = new ESPLoader({
|
||||
transport,
|
||||
baudrate: 115200,
|
||||
romBaudrate: 115200,
|
||||
enableTracing: false,
|
||||
});
|
||||
|
||||
// For debugging
|
||||
(window as any).esploader = esploader;
|
||||
@ -45,61 +44,40 @@ export const flash = async (
|
||||
});
|
||||
|
||||
try {
|
||||
await esploader.initialize();
|
||||
await esploader.main();
|
||||
await esploader.flashId();
|
||||
} catch (err: any) {
|
||||
logger.error(err);
|
||||
if (esploader.connected) {
|
||||
fireStateEvent({
|
||||
state: FlashStateType.ERROR,
|
||||
message:
|
||||
"Failed to initialize. Try resetting your device or holding the BOOT button while clicking INSTALL.",
|
||||
details: { error: FlashError.FAILED_INITIALIZING, details: err },
|
||||
});
|
||||
await esploader.disconnect();
|
||||
}
|
||||
console.error(err);
|
||||
fireStateEvent({
|
||||
state: FlashStateType.ERROR,
|
||||
message:
|
||||
"Failed to initialize. Try resetting your device or holding the BOOT button while clicking INSTALL.",
|
||||
details: { error: FlashError.FAILED_INITIALIZING, details: err },
|
||||
});
|
||||
|
||||
await hardReset(transport);
|
||||
await transport.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
chipFamily = getChipFamilyName(esploader);
|
||||
chipFamily = esploader.chip.CHIP_NAME as any;
|
||||
|
||||
fireStateEvent({
|
||||
state: FlashStateType.INITIALIZING,
|
||||
message: `Initialized. Found ${chipFamily}`,
|
||||
details: { done: true },
|
||||
});
|
||||
fireStateEvent({
|
||||
state: FlashStateType.MANIFEST,
|
||||
message: "Fetching manifest...",
|
||||
details: { done: false },
|
||||
});
|
||||
|
||||
try {
|
||||
manifest = await manifestProm;
|
||||
} catch (err: any) {
|
||||
fireStateEvent({
|
||||
state: FlashStateType.ERROR,
|
||||
message: `Unable to fetch manifest: ${err}`,
|
||||
details: { error: FlashError.FAILED_MANIFEST_FETCH, details: err },
|
||||
});
|
||||
await esploader.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
build = manifest.builds.find((b) => b.chipFamily === chipFamily);
|
||||
|
||||
fireStateEvent({
|
||||
state: FlashStateType.MANIFEST,
|
||||
message: `Found manifest for ${manifest.name}`,
|
||||
details: { done: true },
|
||||
});
|
||||
|
||||
if (!build) {
|
||||
fireStateEvent({
|
||||
state: FlashStateType.ERROR,
|
||||
message: `Your ${chipFamily} board is not supported.`,
|
||||
details: { error: FlashError.NOT_SUPPORTED, details: chipFamily },
|
||||
});
|
||||
await esploader.disconnect();
|
||||
await hardReset(transport);
|
||||
await transport.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -109,28 +87,33 @@ export const flash = async (
|
||||
details: { done: false },
|
||||
});
|
||||
|
||||
const manifestURL = new URL(manifestPath, location.toString()).toString();
|
||||
const filePromises = build.parts.map(async (part) => {
|
||||
const url = new URL(part.path, manifestURL).toString();
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) {
|
||||
throw new Error(
|
||||
`Downlading firmware ${part.path} failed: ${resp.status}`
|
||||
`Downlading firmware ${part.path} failed: ${resp.status}`,
|
||||
);
|
||||
}
|
||||
return resp.arrayBuffer();
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// Run the stub while we wait for files to download
|
||||
const espStub = await esploader.runStub();
|
||||
|
||||
const files: ArrayBuffer[] = [];
|
||||
const fileArray: Array<{ data: string; address: number }> = [];
|
||||
let totalSize = 0;
|
||||
|
||||
for (const prom of filePromises) {
|
||||
for (let part = 0; part < filePromises.length; part++) {
|
||||
try {
|
||||
const data = await prom;
|
||||
files.push(data);
|
||||
totalSize += data.byteLength;
|
||||
const data = await filePromises[part];
|
||||
fileArray.push({ data, address: build.parts[part].offset });
|
||||
totalSize += data.length;
|
||||
} catch (err: any) {
|
||||
fireStateEvent({
|
||||
state: FlashStateType.ERROR,
|
||||
@ -140,7 +123,8 @@ export const flash = async (
|
||||
details: err.message,
|
||||
},
|
||||
});
|
||||
await esploader.disconnect();
|
||||
await hardReset(transport);
|
||||
await transport.disconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -157,7 +141,7 @@ export const flash = async (
|
||||
message: "Erasing device...",
|
||||
details: { done: false },
|
||||
});
|
||||
await espStub.eraseFlash();
|
||||
await esploader.eraseFlash();
|
||||
fireStateEvent({
|
||||
state: FlashStateType.ERASING,
|
||||
message: "Device erased",
|
||||
@ -165,56 +149,61 @@ export const flash = async (
|
||||
});
|
||||
}
|
||||
|
||||
let lastPct = 0;
|
||||
|
||||
fireStateEvent({
|
||||
state: FlashStateType.WRITING,
|
||||
message: `Writing progress: ${lastPct}%`,
|
||||
message: `Writing progress: 0%`,
|
||||
details: {
|
||||
bytesTotal: totalSize,
|
||||
bytesWritten: 0,
|
||||
percentage: lastPct,
|
||||
percentage: 0,
|
||||
},
|
||||
});
|
||||
|
||||
let totalWritten = 0;
|
||||
|
||||
for (const part of build.parts) {
|
||||
const file = files.shift()!;
|
||||
try {
|
||||
await espStub.flashData(
|
||||
file,
|
||||
(bytesWritten: number) => {
|
||||
const newPct = Math.floor(
|
||||
((totalWritten + bytesWritten) / totalSize) * 100
|
||||
);
|
||||
if (newPct === lastPct) {
|
||||
return;
|
||||
}
|
||||
lastPct = newPct;
|
||||
fireStateEvent({
|
||||
state: FlashStateType.WRITING,
|
||||
message: `Writing progress: ${newPct}%`,
|
||||
details: {
|
||||
bytesTotal: totalSize,
|
||||
bytesWritten: totalWritten + bytesWritten,
|
||||
percentage: newPct,
|
||||
},
|
||||
});
|
||||
},
|
||||
part.offset,
|
||||
true
|
||||
);
|
||||
} catch (err: any) {
|
||||
fireStateEvent({
|
||||
state: FlashStateType.ERROR,
|
||||
message: err.message,
|
||||
details: { error: FlashError.WRITE_FAILED, details: err },
|
||||
});
|
||||
await esploader.disconnect();
|
||||
return;
|
||||
}
|
||||
totalWritten += file.byteLength;
|
||||
try {
|
||||
await esploader.writeFlash({
|
||||
fileArray,
|
||||
flashSize: "keep",
|
||||
flashMode: "keep",
|
||||
flashFreq: "keep",
|
||||
eraseAll: false,
|
||||
compress: true,
|
||||
// report progress
|
||||
reportProgress: (fileIndex: number, written: number, total: number) => {
|
||||
const uncompressedWritten =
|
||||
(written / total) * fileArray[fileIndex].data.length;
|
||||
|
||||
const newPct = Math.floor(
|
||||
((totalWritten + uncompressedWritten) / totalSize) * 100,
|
||||
);
|
||||
|
||||
// we're done with this file
|
||||
if (written === total) {
|
||||
totalWritten += uncompressedWritten;
|
||||
return;
|
||||
}
|
||||
|
||||
fireStateEvent({
|
||||
state: FlashStateType.WRITING,
|
||||
message: `Writing progress: ${newPct}%`,
|
||||
details: {
|
||||
bytesTotal: totalSize,
|
||||
bytesWritten: totalWritten + written,
|
||||
percentage: newPct,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
fireStateEvent({
|
||||
state: FlashStateType.ERROR,
|
||||
message: err.message,
|
||||
details: { error: FlashError.WRITE_FAILED, details: err },
|
||||
});
|
||||
await hardReset(transport);
|
||||
await transport.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
fireStateEvent({
|
||||
@ -227,11 +216,10 @@ export const flash = async (
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(100);
|
||||
await hardReset(transport);
|
||||
|
||||
console.log("DISCONNECT");
|
||||
await esploader.disconnect();
|
||||
console.log("HARD RESET");
|
||||
await esploader.hardReset();
|
||||
await transport.disconnect();
|
||||
|
||||
fireStateEvent({
|
||||
state: FlashStateType.FINISHED,
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { FlashState } from "./const";
|
||||
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;
|
||||
@ -10,12 +12,12 @@ export class InstallButton extends HTMLElement {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 8px 28px;
|
||||
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: 4px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.12), 0 1px 5px 0 rgba(0,0,0,.2);
|
||||
border-radius: var(--esp-tools-button-border-radius, 9999px);
|
||||
}
|
||||
button::before {
|
||||
content: " ";
|
||||
@ -25,10 +27,7 @@ export class InstallButton extends HTMLElement {
|
||||
left: 0;
|
||||
right: 0;
|
||||
opacity: 0.2;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button:hover {
|
||||
box-shadow: 0 4px 8px 0 rgba(0,0,0,.14), 0 1px 7px 0 rgba(0,0,0,.12), 0 3px 1px -1px rgba(0,0,0,.2);
|
||||
border-radius: var(--esp-tools-button-border-radius, 9999px);
|
||||
}
|
||||
button:hover::before {
|
||||
background-color: rgba(255,255,255,.8);
|
||||
@ -49,10 +48,6 @@ export class InstallButton extends HTMLElement {
|
||||
cursor: unset;
|
||||
pointer-events: none;
|
||||
}
|
||||
improv-wifi-launch-button {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}`;
|
||||
@ -71,9 +66,7 @@ export class InstallButton extends HTMLElement {
|
||||
|
||||
public renderRoot?: ShadowRoot;
|
||||
|
||||
public static preload() {
|
||||
import("./connect");
|
||||
}
|
||||
public overrides: EwtInstallDialog["overrides"];
|
||||
|
||||
public connectedCallback() {
|
||||
if (this.renderRoot) {
|
||||
@ -92,28 +85,23 @@ export class InstallButton extends HTMLElement {
|
||||
|
||||
this.toggleAttribute("install-supported", true);
|
||||
|
||||
this.addEventListener("mouseover", InstallButton.preload);
|
||||
|
||||
const slot = document.createElement("slot");
|
||||
|
||||
slot.addEventListener("click", async (ev) => {
|
||||
ev.preventDefault();
|
||||
const mod = await import("./connect");
|
||||
mod.connect(this);
|
||||
connect(this);
|
||||
});
|
||||
|
||||
slot.name = "activate";
|
||||
const button = document.createElement("button");
|
||||
button.innerText = "INSTALL";
|
||||
button.innerText = "Connect";
|
||||
slot.append(button);
|
||||
if (
|
||||
"adoptedStyleSheets" in Document.prototype &&
|
||||
"replaceSync" in CSSStyleSheet.prototype
|
||||
) {
|
||||
const sheet = new CSSStyleSheet();
|
||||
// @ts-expect-error
|
||||
sheet.replaceSync(InstallButton.style);
|
||||
// @ts-expect-error
|
||||
this.renderRoot.adoptedStyleSheets = [sheet];
|
||||
} else {
|
||||
const styleSheet = document.createElement("style");
|
||||
|
10
src/no-port-picked/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
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;
|
||||
};
|
176
src/no-port-picked/no-port-picked-dialog.ts
Normal file
@ -0,0 +1,176 @@
|
||||
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;
|
||||
}
|
||||
}
|
35
src/pages/ewt-page-message.ts
Normal file
@ -0,0 +1,35 @@
|
||||
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;
|
||||
}
|
||||
}
|
43
src/pages/ewt-page-progress.ts
Normal file
@ -0,0 +1,43 @@
|
||||
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;
|
||||
}
|
||||
}
|
31
src/styles.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { css } from "lit";
|
||||
|
||||
// We set font-size to 16px and all the mdc typography styles
|
||||
// because it defaults to rem, which means that the font-size
|
||||
// of the host website would influence the ESP Web Tools dialog.
|
||||
|
||||
export const dialogStyles = css`
|
||||
:host {
|
||||
--roboto-font: Roboto, system-ui;
|
||||
--text-color: rgba(0, 0, 0, 0.6);
|
||||
--danger-color: #db4437;
|
||||
|
||||
--md-sys-color-primary: #03a9f4;
|
||||
--md-sys-color-on-primary: #fff;
|
||||
--md-ref-typeface-brand: var(--roboto-font);
|
||||
--md-ref-typeface-plain: var(--roboto-font);
|
||||
|
||||
--md-sys-color-surface: #fff;
|
||||
--md-sys-color-surface-container: #fff;
|
||||
--md-sys-color-surface-container-high: #fff;
|
||||
--md-sys-color-surface-container-highest: #f5f5f5;
|
||||
--md-sys-color-secondary-container: #e0e0e0;
|
||||
|
||||
--md-sys-typescale-headline-font: var(--roboto-font);
|
||||
--md-sys-typescale-title-font: var(--roboto-font);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
`;
|
@ -1,25 +0,0 @@
|
||||
import {
|
||||
CHIP_FAMILY_ESP32,
|
||||
CHIP_FAMILY_ESP32S2,
|
||||
CHIP_FAMILY_ESP8266,
|
||||
CHIP_FAMILY_ESP32C3,
|
||||
ESPLoader,
|
||||
} from "esp-web-flasher";
|
||||
import type { BaseFlashState } from "../const";
|
||||
|
||||
export const getChipFamilyName = (
|
||||
esploader: ESPLoader
|
||||
): NonNullable<BaseFlashState["chipFamily"]> => {
|
||||
switch (esploader.chipFamily) {
|
||||
case CHIP_FAMILY_ESP32:
|
||||
return "ESP32";
|
||||
case CHIP_FAMILY_ESP8266:
|
||||
return "ESP8266";
|
||||
case CHIP_FAMILY_ESP32S2:
|
||||
return "ESP32-S2";
|
||||
case CHIP_FAMILY_ESP32C3:
|
||||
return "ESP32-C3";
|
||||
default:
|
||||
return "Unknown Chip";
|
||||
}
|
||||
};
|
@ -5,7 +5,7 @@ interface ConsoleState {
|
||||
strikethrough: boolean;
|
||||
foregroundColor: string | null;
|
||||
backgroundColor: string | null;
|
||||
// carriageReturn: boolean;
|
||||
carriageReturn: boolean;
|
||||
secret: boolean;
|
||||
}
|
||||
|
||||
@ -17,28 +17,32 @@ export class ColoredConsole {
|
||||
strikethrough: false,
|
||||
foregroundColor: null,
|
||||
backgroundColor: null,
|
||||
// carriageReturn: false,
|
||||
carriageReturn: false,
|
||||
secret: false,
|
||||
};
|
||||
|
||||
constructor(public targetElement: HTMLElement) {}
|
||||
|
||||
logs(): string {
|
||||
return this.targetElement.innerText;
|
||||
}
|
||||
|
||||
addLine(line: string) {
|
||||
// @ts-expect-error
|
||||
const re = /(?:\033|\\033)(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
|
||||
let i = 0;
|
||||
|
||||
// This doesn't work for some reason
|
||||
// if (this.state.carriageReturn) {
|
||||
// if (line !== "\n") {
|
||||
// // don't remove if \r\n
|
||||
// this.targetElement.removeChild(this.targetElement.lastChild!);
|
||||
// }
|
||||
// this.state.carriageReturn = false;
|
||||
// }
|
||||
if (this.state.carriageReturn) {
|
||||
if (line !== "\n") {
|
||||
// don't remove if \r\n
|
||||
this.targetElement.removeChild(this.targetElement.lastChild!);
|
||||
}
|
||||
this.state.carriageReturn = false;
|
||||
}
|
||||
|
||||
// if (line.includes("\r")) {
|
||||
// this.state.carriageReturn = true;
|
||||
// }
|
||||
if (line.includes("\r")) {
|
||||
this.state.carriageReturn = true;
|
||||
}
|
||||
|
||||
const lineSpan = document.createElement("span");
|
||||
lineSpan.classList.add("line");
|
||||
@ -175,14 +179,107 @@ export class ColoredConsole {
|
||||
}
|
||||
}
|
||||
}
|
||||
const atBottom =
|
||||
this.targetElement.scrollTop >
|
||||
this.targetElement.scrollHeight - this.targetElement.offsetHeight - 50;
|
||||
|
||||
addSpan(line.substring(i));
|
||||
|
||||
if (
|
||||
this.targetElement.scrollTop + 56 >=
|
||||
this.targetElement.scrollHeight - this.targetElement.offsetHeight
|
||||
) {
|
||||
// at bottom
|
||||
// Keep scroll at bottom
|
||||
if (atBottom) {
|
||||
this.targetElement.scrollTop = this.targetElement.scrollHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const coloredConsoleStyles = `
|
||||
.log {
|
||||
flex: 1;
|
||||
background-color: #1c1c1c;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
|
||||
monospace;
|
||||
font-size: 12px;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
line-height: 1.45;
|
||||
border-radius: 3px;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.log-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.log-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.log-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.log-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.log-underline.log-strikethrough {
|
||||
text-decoration: underline line-through;
|
||||
}
|
||||
.log-secret {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.log-secret-redacted {
|
||||
opacity: 0;
|
||||
width: 1px;
|
||||
font-size: 1px;
|
||||
}
|
||||
.log-fg-black {
|
||||
color: rgb(128, 128, 128);
|
||||
}
|
||||
.log-fg-red {
|
||||
color: rgb(255, 0, 0);
|
||||
}
|
||||
.log-fg-green {
|
||||
color: rgb(0, 255, 0);
|
||||
}
|
||||
.log-fg-yellow {
|
||||
color: rgb(255, 255, 0);
|
||||
}
|
||||
.log-fg-blue {
|
||||
color: rgb(0, 0, 255);
|
||||
}
|
||||
.log-fg-magenta {
|
||||
color: rgb(255, 0, 255);
|
||||
}
|
||||
.log-fg-cyan {
|
||||
color: rgb(0, 255, 255);
|
||||
}
|
||||
.log-fg-white {
|
||||
color: rgb(187, 187, 187);
|
||||
}
|
||||
.log-bg-black {
|
||||
background-color: rgb(0, 0, 0);
|
||||
}
|
||||
.log-bg-red {
|
||||
background-color: rgb(255, 0, 0);
|
||||
}
|
||||
.log-bg-green {
|
||||
background-color: rgb(0, 255, 0);
|
||||
}
|
||||
.log-bg-yellow {
|
||||
background-color: rgb(255, 255, 0);
|
||||
}
|
||||
.log-bg-blue {
|
||||
background-color: rgb(0, 0, 255);
|
||||
}
|
||||
.log-bg-magenta {
|
||||
background-color: rgb(255, 0, 255);
|
||||
}
|
||||
.log-bg-cyan {
|
||||
background-color: rgb(0, 255, 255);
|
||||
}
|
||||
.log-bg-white {
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
`;
|
||||
|
17
src/util/file-download.ts
Normal file
@ -0,0 +1,17 @@
|
||||
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);
|
||||
};
|
@ -7,7 +7,7 @@ export const fireEvent = <Event extends keyof HTMLElementEventMap>(
|
||||
bubbles?: boolean;
|
||||
cancelable?: boolean;
|
||||
composed?: boolean;
|
||||
}
|
||||
},
|
||||
): void => {
|
||||
options = options || {};
|
||||
const event = new CustomEvent(type, {
|
||||
|
24
src/util/get-operating-system.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// 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;
|
||||
};
|
@ -3,7 +3,7 @@ export class LineBreakTransformer implements Transformer<string, string> {
|
||||
|
||||
transform(
|
||||
chunk: string,
|
||||
controller: TransformStreamDefaultController<string>
|
||||
controller: TransformStreamDefaultController<string>,
|
||||
) {
|
||||
// Append new chunks to existing chunks.
|
||||
this.chunks += chunk;
|
||||
|
18
src/util/manifest.ts
Normal file
@ -0,0 +1,18 @@
|
||||
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;
|
||||
};
|
17
src/util/reset.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Transport } from "esptool-js";
|
||||
import { sleep } from "./sleep";
|
||||
|
||||
export const hardReset = async (transport: Transport) => {
|
||||
console.log("Triggering reset");
|
||||
await transport.device.setSignals({
|
||||
dataTerminalReady: false,
|
||||
requestToSend: true,
|
||||
});
|
||||
await sleep(250);
|
||||
await transport.device.setSignals({
|
||||
dataTerminalReady: false,
|
||||
requestToSend: false,
|
||||
});
|
||||
await sleep(250);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
};
|
12
src/util/timestamp-transformer.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export class TimestampTransformer implements Transformer<string, string> {
|
||||
transform(
|
||||
chunk: string,
|
||||
controller: TransformStreamDefaultController<string>,
|
||||
) {
|
||||
const date = new Date();
|
||||
const h = date.getHours().toString().padStart(2, "0");
|
||||
const m = date.getMinutes().toString().padStart(2, "0");
|
||||
const s = date.getSeconds().toString().padStart(2, "0");
|
||||
controller.enqueue(`[${h}:${m}:${s}]${chunk}`);
|
||||
}
|
||||
}
|
1
src/version.ts
Normal file
@ -0,0 +1 @@
|
||||
export const version = "dev";
|
@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "ESPHome",
|
||||
"version": "2021.11.0-dev",
|
||||
"builds": [
|
||||
{
|
||||
"chipFamily": "ESP32",
|
||||
"parts": [
|
||||
{ "path": "bootloader.bin", "offset": 4096 },
|
||||
{ "path": "partitions.bin", "offset": 32768 },
|
||||
{ "path": "ota.bin", "offset": 57344 },
|
||||
{ "path": "firmware.bin", "offset": 65536 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"chipFamily": "ESP8266",
|
||||
"parts": [{ "path": "esp8266.bin", "offset": 0 }]
|
||||
}
|
||||
]
|
||||
}
|
BIN
static/logos/2smart.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
static/logos/canairio.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
static/logos/clockwise.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
static/logos/luciferin_logo.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
156
static/logos/nspanelmanager.svg
Normal file
@ -0,0 +1,156 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="95.541344mm"
|
||||
height="29.999447mm"
|
||||
viewBox="0 0 95.54134 29.999447"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:export-filename="/home/erik/Documents/Projekt/NSPanel Manager/Logos/logo250.png"
|
||||
inkscape:export-xdpi="76.010269"
|
||||
inkscape:export-ydpi="76.010269"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#000000"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="1"
|
||||
inkscape:pagecheckerboard="false"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.3404872"
|
||||
inkscape:cx="174.5634"
|
||||
inkscape:cy="71.988753"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="704"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer2"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
fit-margin-top="5"
|
||||
fit-margin-left="7"
|
||||
fit-margin-right="7"
|
||||
fit-margin-bottom="5" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
style="display:inline"
|
||||
transform="translate(-23.806324,-37.155967)">
|
||||
<rect
|
||||
style="fill:#000000;stroke:#000000;stroke-width:0.264583"
|
||||
id="rect91625"
|
||||
width="16.863066"
|
||||
height="1.0996726"
|
||||
x="30.938616"
|
||||
y="59.758064" />
|
||||
<rect
|
||||
style="fill:#000000;stroke:#000000;stroke-width:0.473039"
|
||||
id="rect91761"
|
||||
width="66.510048"
|
||||
height="0.89121634"
|
||||
x="31.042845"
|
||||
y="42.392487" />
|
||||
<rect
|
||||
style="fill:#000000;stroke:#000000;stroke-width:0.34752"
|
||||
id="rect91763"
|
||||
width="31.46484"
|
||||
height="1.0167363"
|
||||
x="80.709068"
|
||||
y="59.79953" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Layer 2"
|
||||
style="display:inline"
|
||||
transform="translate(-23.806324,-37.155967)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:0;font-family:sans-serif;letter-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
|
||||
x="64.189873"
|
||||
y="56.76144"
|
||||
id="text39249"><tspan
|
||||
sodipodi:role="line"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.4056px;line-height:0.55;font-family:Hamlin;-inkscape-font-specification:'Hamlin, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#000000;stroke-width:0.264583"
|
||||
x="64.189873"
|
||||
y="56.76144"
|
||||
id="tspan62001">NSPANEL</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93889px;line-height:1.55;font-family:Hamlin;-inkscape-font-specification:'Hamlin Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:center;text-anchor:middle;fill:#000000;stroke-width:0.264583"
|
||||
x="64.189873"
|
||||
y="62.091209"
|
||||
id="tspan73527">MANAGER</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Layer 3"
|
||||
transform="translate(-23.806324,-37.155967)">
|
||||
<g
|
||||
id="g91894"
|
||||
transform="matrix(0.15910033,0,0,0.15910033,102.25849,44.830316)">
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
id="layer1-1"
|
||||
transform="translate(-30.269979,-9.7200098)"
|
||||
style="display:inline;fill:#ffc101;fill-opacity:1">
|
||||
<path
|
||||
style="fill:#ffc101;fill-opacity:1;stroke-width:0.103892"
|
||||
d="m 55.789326,41.355207 -7.50621,-0.0036 c -8.143469,-0.0039 -15.508914,-0.135689 -16.902471,-0.302478 -0.482408,-0.05774 -0.940146,-0.168019 -1.017198,-0.245069 -0.115522,-0.115523 -0.121188,-0.512246 -0.03231,-2.261925 0.23677,-4.661121 1.011239,-7.815267 2.954845,-12.034057 2.163281,-4.695621 5.19604,-8.340625 9.101253,-10.938592 2.710535,-1.8032 6.982591,-3.536303 9.582241,-3.887359 0.270422,-0.03652 0.543941,-0.129371 0.60782,-0.20634 0.06388,-0.07697 0.286764,-0.168201 0.495299,-0.202735 0.208537,-0.03453 0.449284,-0.09017 0.534995,-0.123628 0.168484,-0.06577 0.381034,-0.08553 1.215776,-0.112981 l 0.540477,-0.01778 0.03093,-0.649326 0.03093,-0.6493272 h 1.610329 1.610329 l 0.03093,0.6493272 0.03093,0.649326 0.540477,0.01778 c 0.834742,0.02745 1.047292,0.04721 1.215776,0.112981 0.08571,0.03346 0.326458,0.08909 0.534995,0.123628 0.208535,0.03453 0.43142,0.125765 0.495299,0.202735 0.06388,0.07697 0.337398,0.169823 0.60782,0.20634 1.199505,0.161981 3.423686,0.820696 4.997483,1.480058 3.507719,1.469604 6.221358,3.303143 8.667596,5.856479 2.140092,2.233788 3.602852,4.416791 5.018415,7.489412 1.943606,4.21879 2.718075,7.372936 2.954845,12.034057 0.08888,1.749679 0.08321,2.146402 -0.03231,2.261925 -0.07705,0.07705 -0.53479,0.187332 -1.017198,0.245069 -1.393557,0.166789 -8.759002,0.298597 -16.902471,0.302478 l -7.50621,0.0036 z"
|
||||
id="path1465"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:export-xdpi="15.183412"
|
||||
inkscape:export-ydpi="15.183412"
|
||||
sodipodi:nodetypes="csssssssssscccccccccscssssssssccc" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="Layer 1 copy"
|
||||
id="g2878"
|
||||
transform="translate(-30.269979,-9.7200098)"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.103892"
|
||||
d="m 54.334834,101.13608 c -3.956539,-0.21732 -5.98252,-0.58093 -6.463066,-1.159955 -0.09761,-0.11762 -0.123549,-0.57148 -0.09916,-1.73541 0.02799,-1.33577 0.008,-1.59689 -0.130816,-1.71212 -0.09011,-0.0748 -0.16384,-0.29294 -0.16384,-0.48477 0,-0.30436 0.05486,-0.37678 0.430667,-0.5685 0.694102,-0.3541 2.301256,-0.71174 4.060659,-0.9036 l 1.638312,-0.17866 V 70.783119 c 0,-15.670705 -0.03494,-23.609951 -0.103892,-23.609951 -0.05714,0 -0.103892,-0.0935 -0.103892,-0.207784 0,-0.194592 0.06926,-0.207784 1.090868,-0.207784 0.900399,0 1.090868,-0.02721 1.090868,-0.155839 0,-0.08571 0.04675,-0.155838 0.103892,-0.155838 0.06644,0 0.103892,-3.463072 0.103892,-5.090716 l 2.493412,-2e-6 c 0,1.627644 0.03746,5.090716 0.103892,5.090716 0.05714,0 0.103892,0.07013 0.103892,0.155838 0,0.128629 0.190469,0.155839 1.090868,0.155839 1.021606,0 1.090868,0.01319 1.090868,0.207784 0,0.114281 -0.04675,0.207784 -0.103892,0.207784 -0.06896,0 -0.103892,7.939246 -0.103892,23.609951 v 23.609948 l 1.638312,0.17866 c 1.759403,0.19186 3.366557,0.54949 4.060659,0.9036 0.375812,0.19172 0.430667,0.26413 0.430667,0.5685 0,0.19183 -0.07373,0.40998 -0.16384,0.48476 -0.138668,0.11509 -0.158971,0.37643 -0.132144,1.70097 0.01743,0.86075 -0.0055,1.62411 -0.05099,1.69635 -0.27864,0.442605 -1.632284,0.804945 -3.803075,1.017985 -1.460176,0.14331 -6.726732,0.26812 -8.109225,0.19219 z"
|
||||
id="path2876"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:export-xdpi="15.183412"
|
||||
inkscape:export-ydpi="15.183412"
|
||||
sodipodi:nodetypes="ssssssscssssssccsssssscssssscsss" />
|
||||
</g>
|
||||
<g
|
||||
id="layer3-5"
|
||||
inkscape:label="Layer 3">
|
||||
<circle
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:17.1351;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="path1510"
|
||||
cx="34.837666"
|
||||
cy="55.072739"
|
||||
r="1.2872558" />
|
||||
</g>
|
||||
<g
|
||||
id="layer2-9"
|
||||
inkscape:label="Layer 2">
|
||||
<rect
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:12.8043;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect1508"
|
||||
width="1.3904994"
|
||||
height="22.681686"
|
||||
x="34.147842"
|
||||
y="31.601988" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.3 KiB |
BIN
static/logos/openepaperlink.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
static/logos/openspool.png
Normal file
After Width: | Height: | Size: 213 KiB |
BIN
static/logos/squeezelite-esp32.png
Normal file
After Width: | Height: | Size: 691 B |
BIN
static/logos/trmnl.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
static/screenshots/dashboard.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
static/screenshots/logs.png
Normal file
After Width: | Height: | Size: 15 KiB |
@ -13,8 +13,8 @@
|
||||
"noUnusedLocals": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"importHelpers": true
|
||||
},
|
||||
"include": ["src/*"]
|
||||
}
|
||||
|