tasmota-esp-web-tools 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/.devcontainer/Dockerfile +16 -0
  2. package/.devcontainer/devcontainer.json +44 -0
  3. package/.github/dependabot.yml +10 -0
  4. package/.github/release-drafter.yml +12 -0
  5. package/.github/workflows/ci.yml +22 -0
  6. package/.github/workflows/npmpublish.yml +22 -0
  7. package/.github/workflows/release-drafter.yml +14 -0
  8. package/.prettierignore +1 -0
  9. package/README.md +68 -0
  10. package/dist/components/ewt-button.d.ts +9 -0
  11. package/dist/components/ewt-button.js +17 -0
  12. package/dist/components/ewt-checkbox.d.ts +9 -0
  13. package/dist/components/ewt-checkbox.js +6 -0
  14. package/dist/components/ewt-circular-progress.d.ts +9 -0
  15. package/dist/components/ewt-circular-progress.js +6 -0
  16. package/dist/components/ewt-console.d.ts +20 -0
  17. package/dist/components/ewt-console.js +141 -0
  18. package/dist/components/ewt-dialog.d.ts +9 -0
  19. package/dist/components/ewt-dialog.js +14 -0
  20. package/dist/components/ewt-formfield.d.ts +9 -0
  21. package/dist/components/ewt-formfield.js +6 -0
  22. package/dist/components/ewt-icon-button.d.ts +9 -0
  23. package/dist/components/ewt-icon-button.js +6 -0
  24. package/dist/components/ewt-list-item.d.ts +9 -0
  25. package/dist/components/ewt-list-item.js +6 -0
  26. package/dist/components/ewt-select.d.ts +9 -0
  27. package/dist/components/ewt-select.js +15 -0
  28. package/dist/components/ewt-textfield.d.ts +9 -0
  29. package/dist/components/ewt-textfield.js +15 -0
  30. package/dist/components/svg.d.ts +3 -0
  31. package/dist/components/svg.js +24 -0
  32. package/dist/connect.d.ts +3 -0
  33. package/dist/connect.js +33 -0
  34. package/dist/const.d.ts +94 -0
  35. package/dist/const.js +1 -0
  36. package/dist/flash.d.ts +4 -0
  37. package/dist/flash.js +191 -0
  38. package/dist/install-button.d.ts +17 -0
  39. package/dist/install-button.js +96 -0
  40. package/dist/install-dialog.d.ts +70 -0
  41. package/dist/install-dialog.js +899 -0
  42. package/dist/no-port-picked/index.d.ts +2 -0
  43. package/dist/no-port-picked/index.js +7 -0
  44. package/dist/no-port-picked/no-port-picked-dialog.d.ts +15 -0
  45. package/dist/no-port-picked/no-port-picked-dialog.js +149 -0
  46. package/dist/pages/ewt-page-message.d.ts +14 -0
  47. package/dist/pages/ewt-page-message.js +34 -0
  48. package/dist/pages/ewt-page-progress.d.ts +14 -0
  49. package/dist/pages/ewt-page-progress.js +39 -0
  50. package/dist/styles.d.ts +1 -0
  51. package/dist/styles.js +32 -0
  52. package/dist/util/chip-family-name.d.ts +3 -0
  53. package/dist/util/chip-family-name.js +17 -0
  54. package/dist/util/console-color.d.ts +19 -0
  55. package/dist/util/console-color.js +265 -0
  56. package/dist/util/file-download.d.ts +2 -0
  57. package/dist/util/file-download.js +15 -0
  58. package/dist/util/fire-event.d.ts +5 -0
  59. package/dist/util/fire-event.js +12 -0
  60. package/dist/util/line-break-transformer.d.ts +5 -0
  61. package/dist/util/line-break-transformer.js +17 -0
  62. package/dist/util/manifest.d.ts +2 -0
  63. package/dist/util/manifest.js +12 -0
  64. package/dist/util/sleep.d.ts +1 -0
  65. package/dist/util/sleep.js +1 -0
  66. package/dist/web/connect-3012e6dd.js +886 -0
  67. package/dist/web/esp32-5f88817f.js +1 -0
  68. package/dist/web/esp32c3-596796ad.js +1 -0
  69. package/dist/web/esp32s2-f7a69530.js +1 -0
  70. package/dist/web/esp32s3-314fbacd.js +1 -0
  71. package/dist/web/esp8266-c68f89af.js +1 -0
  72. package/dist/web/index-f110c132.js +126 -0
  73. package/dist/web/install-button.js +1 -0
  74. package/package.json +36 -0
  75. package/rollup.config.js +28 -0
  76. package/script/build +8 -0
  77. package/script/develop +17 -0
  78. package/script/stubgen.py +161 -0
  79. package/src/components/ewt-button.ts +25 -0
  80. package/src/components/ewt-checkbox.ts +14 -0
  81. package/src/components/ewt-circular-progress.ts +14 -0
  82. package/src/components/ewt-console.ts +163 -0
  83. package/src/components/ewt-dialog.ts +22 -0
  84. package/src/components/ewt-formfield.ts +14 -0
  85. package/src/components/ewt-icon-button.ts +14 -0
  86. package/src/components/ewt-list-item.ts +14 -0
  87. package/src/components/ewt-select.ts +23 -0
  88. package/src/components/ewt-textfield.ts +23 -0
  89. package/src/components/svg.ts +27 -0
  90. package/src/connect.ts +42 -0
  91. package/src/const.ts +101 -0
  92. package/src/flash.ts +240 -0
  93. package/src/install-button.ts +128 -0
  94. package/src/install-dialog.ts +981 -0
  95. package/src/no-port-picked/index.ts +10 -0
  96. package/src/no-port-picked/no-port-picked-dialog.ts +158 -0
  97. package/src/pages/ewt-page-message.ts +39 -0
  98. package/src/pages/ewt-page-progress.ts +44 -0
  99. package/src/styles.ts +34 -0
  100. package/src/util/chip-family-name.ts +28 -0
  101. package/src/util/console-color.ts +283 -0
  102. package/src/util/file-download.ts +17 -0
  103. package/src/util/fire-event.ts +20 -0
  104. package/src/util/line-break-transformer.ts +20 -0
  105. package/src/util/manifest.ts +18 -0
  106. package/src/util/sleep.ts +2 -0
  107. package/static/logos/canairio.png +0 -0
  108. package/static/logos/espeasy.png +0 -0
  109. package/static/logos/esphome.svg +1 -0
  110. package/static/logos/tasmota.svg +1 -0
  111. package/static/logos/wled.png +0 -0
  112. package/static/screenshots/dashboard.png +0 -0
  113. package/static/screenshots/logs.png +0 -0
  114. package/static/social.png +0 -0
  115. package/tsconfig.json +21 -0
@@ -0,0 +1,981 @@
1
+ import { LitElement, html, PropertyValues, css, TemplateResult } from "lit";
2
+ import { state } from "lit/decorators.js";
3
+ import "./components/ewt-button";
4
+ import "./components/ewt-checkbox";
5
+ import "./components/ewt-console";
6
+ import "./components/ewt-dialog";
7
+ import "./components/ewt-formfield";
8
+ import "./components/ewt-icon-button";
9
+ import "./components/ewt-textfield";
10
+ import type { EwtTextfield } from "./components/ewt-textfield";
11
+ import "./components/ewt-select";
12
+ import "./components/ewt-list-item";
13
+ import "./pages/ewt-page-progress";
14
+ import "./pages/ewt-page-message";
15
+ import { chipIcon, closeIcon, firmwareIcon } from "./components/svg";
16
+ import { Logger, Manifest, FlashStateType, FlashState } from "./const.js";
17
+ import { ImprovSerial, Ssid } from "improv-wifi-serial-sdk/dist/serial";
18
+ import {
19
+ ImprovSerialCurrentState,
20
+ ImprovSerialErrorState,
21
+ PortNotReady,
22
+ } from "improv-wifi-serial-sdk/dist/const";
23
+ import { flash } from "./flash";
24
+ import { textDownload } from "./util/file-download";
25
+ import { fireEvent } from "./util/fire-event";
26
+ import { sleep } from "./util/sleep";
27
+ import { downloadManifest } from "./util/manifest";
28
+ import { dialogStyles } from "./styles";
29
+
30
+ const ERROR_ICON = "⚠️";
31
+ const OK_ICON = "🎉";
32
+
33
+ export class EwtInstallDialog extends LitElement {
34
+ public port!: SerialPort;
35
+
36
+ public manifestPath!: string;
37
+
38
+ public logger: Logger = console;
39
+
40
+ public overrides?: {
41
+ checkSameFirmware?: (
42
+ manifest: Manifest,
43
+ deviceImprov: ImprovSerial["info"]
44
+ ) => boolean;
45
+ };
46
+
47
+ private _manifest!: Manifest;
48
+
49
+ private _info?: ImprovSerial["info"];
50
+
51
+ // null = NOT_SUPPORTED
52
+ @state() private _client?: ImprovSerial | null;
53
+
54
+ @state() private _state:
55
+ | "ERROR"
56
+ | "DASHBOARD"
57
+ | "PROVISION"
58
+ | "INSTALL"
59
+ | "ASK_ERASE"
60
+ | "LOGS" = "DASHBOARD";
61
+
62
+ @state() private _installErase = false;
63
+ @state() private _installConfirmed = false;
64
+ @state() private _installState?: FlashState;
65
+
66
+ @state() private _provisionForce = false;
67
+ private _wasProvisioned = false;
68
+
69
+ @state() private _error?: string;
70
+
71
+ @state() private _busy = false;
72
+
73
+ // undefined = not loaded
74
+ // null = not available
75
+ @state() private _ssids?: Ssid[] | null;
76
+
77
+ // -1 = custom
78
+ @state() private _selectedSsid = -1;
79
+
80
+ protected render() {
81
+ if (!this.port) {
82
+ return html``;
83
+ }
84
+ let heading: string | undefined;
85
+ let content: TemplateResult;
86
+ let hideActions = false;
87
+ let allowClosing = false;
88
+
89
+ // During installation phase we temporarily remove the client
90
+ if (
91
+ this._client === undefined &&
92
+ this._state !== "INSTALL" &&
93
+ this._state !== "LOGS"
94
+ ) {
95
+ if (this._error) {
96
+ [heading, content, hideActions] = this._renderError(this._error);
97
+ } else {
98
+ content = this._renderProgress("Connecting");
99
+ hideActions = true;
100
+ }
101
+ } else if (this._state === "INSTALL") {
102
+ [heading, content, hideActions, allowClosing] = this._renderInstall();
103
+ } else if (this._state === "ASK_ERASE") {
104
+ [heading, content] = this._renderAskErase();
105
+ } else if (this._state === "ERROR") {
106
+ [heading, content, hideActions] = this._renderError(this._error!);
107
+ } else if (this._state === "DASHBOARD") {
108
+ [heading, content, hideActions, allowClosing] = this._client
109
+ ? this._renderDashboard()
110
+ : this._renderDashboardNoImprov();
111
+ } else if (this._state === "PROVISION") {
112
+ [heading, content, hideActions] = this._renderProvision();
113
+ } else if (this._state === "LOGS") {
114
+ [heading, content, hideActions] = this._renderLogs();
115
+ }
116
+
117
+ return html`
118
+ <ewt-dialog
119
+ open
120
+ .heading=${heading!}
121
+ scrimClickAction
122
+ @closed=${this._handleClose}
123
+ .hideActions=${hideActions}
124
+ >
125
+ ${heading && allowClosing
126
+ ? html`
127
+ <ewt-icon-button dialogAction="close">
128
+ ${closeIcon}
129
+ </ewt-icon-button>
130
+ `
131
+ : ""}
132
+ ${content!}
133
+ </ewt-dialog>
134
+ `;
135
+ }
136
+
137
+ _renderProgress(label: string | TemplateResult, progress?: number) {
138
+ return html`
139
+ <ewt-page-progress
140
+ .label=${label}
141
+ .progress=${progress}
142
+ ></ewt-page-progress>
143
+ `;
144
+ }
145
+
146
+ _renderError(label: string): [string, TemplateResult, boolean] {
147
+ const heading = "Error";
148
+ const content = html`
149
+ <ewt-page-message .icon=${ERROR_ICON} .label=${label}></ewt-page-message>
150
+ <ewt-button
151
+ slot="primaryAction"
152
+ dialogAction="ok"
153
+ label="Close"
154
+ ></ewt-button>
155
+ `;
156
+ const hideActions = false;
157
+ return [heading, content, hideActions];
158
+ }
159
+
160
+ _renderDashboard(): [string, TemplateResult, boolean, boolean] {
161
+ const heading = this._info!.name;
162
+ let content: TemplateResult;
163
+ let hideActions = true;
164
+ let allowClosing = true;
165
+
166
+ content = html`
167
+ <div class="table-row">
168
+ ${firmwareIcon}
169
+ <div>${this._info!.firmware}&nbsp;${this._info!.version}</div>
170
+ </div>
171
+ <div class="table-row last">
172
+ ${chipIcon}
173
+ <div>${this._info!.chipFamily}</div>
174
+ </div>
175
+ <div class="dashboard-buttons">
176
+ ${!this._isSameVersion
177
+ ? html`
178
+ <div>
179
+ <ewt-button
180
+ text-left
181
+ .label=${!this._isSameFirmware
182
+ ? `Install ${this._manifest.name}`
183
+ : `Update ${this._manifest.name}`}
184
+ @click=${() => {
185
+ if (this._isSameFirmware) {
186
+ this._startInstall(false);
187
+ } else if (this._manifest.new_install_prompt_erase) {
188
+ this._state = "ASK_ERASE";
189
+ } else {
190
+ this._startInstall(true);
191
+ }
192
+ }}
193
+ ></ewt-button>
194
+ </div>
195
+ `
196
+ : ""}
197
+ ${this._client!.nextUrl === undefined
198
+ ? ""
199
+ : html`
200
+ <div>
201
+ <a
202
+ href=${this._client!.nextUrl}
203
+ class="has-button"
204
+ target="_blank"
205
+ >
206
+ <ewt-button label="Visit Device"></ewt-button>
207
+ </a>
208
+ </div>
209
+ `}
210
+ ${!this._manifest.home_assistant_domain ||
211
+ this._client!.state !== ImprovSerialCurrentState.PROVISIONED
212
+ ? ""
213
+ : html`
214
+ <div>
215
+ <a
216
+ href=${`https://my.home-assistant.io/redirect/config_flow_start/?domain=${this._manifest.home_assistant_domain}`}
217
+ class="has-button"
218
+ target="_blank"
219
+ >
220
+ <ewt-button label="Add to Home Assistant"></ewt-button>
221
+ </a>
222
+ </div>
223
+ `}
224
+ <div>
225
+ <ewt-button
226
+ .label=${this._client!.state === ImprovSerialCurrentState.READY
227
+ ? "Connect to Wi-Fi"
228
+ : "Change Wi-Fi"}
229
+ @click=${() => {
230
+ this._state = "PROVISION";
231
+ if (
232
+ this._client!.state === ImprovSerialCurrentState.PROVISIONED
233
+ ) {
234
+ this._provisionForce = true;
235
+ }
236
+ }}
237
+ ></ewt-button>
238
+ </div>
239
+ <div>
240
+ <ewt-button
241
+ label="Logs & Console"
242
+ @click=${async () => {
243
+ const client = this._client;
244
+ if (client) {
245
+ await this._closeClientWithoutEvents(client);
246
+ await sleep(100);
247
+ }
248
+ // Also set `null` back to undefined.
249
+ this._client = undefined;
250
+ this._state = "LOGS";
251
+ }}
252
+ ></ewt-button>
253
+ </div>
254
+ ${this._isSameFirmware && this._manifest.funding_url
255
+ ? html`
256
+ <div>
257
+ <a
258
+ class="button"
259
+ href=${this._manifest.funding_url}
260
+ target="_blank"
261
+ >
262
+ <ewt-button label="Fund Development"></ewt-button>
263
+ </a>
264
+ </div>
265
+ `
266
+ : ""}
267
+ ${this._isSameVersion
268
+ ? html`
269
+ <div>
270
+ <ewt-button
271
+ class="danger"
272
+ label="Erase User Data"
273
+ @click=${() => this._startInstall(true)}
274
+ ></ewt-button>
275
+ </div>
276
+ `
277
+ : ""}
278
+ </div>
279
+ `;
280
+
281
+ return [heading, content, hideActions, allowClosing];
282
+ }
283
+ _renderDashboardNoImprov(): [string, TemplateResult, boolean, boolean] {
284
+ const heading = "Device Dashboard";
285
+ let content: TemplateResult;
286
+ let hideActions = true;
287
+ let allowClosing = true;
288
+
289
+ content = html`
290
+ <div class="dashboard-buttons">
291
+ <div>
292
+ <ewt-button
293
+ text-left
294
+ .label=${`Install ${this._manifest.name}`}
295
+ @click=${() => {
296
+ if (this._manifest.new_install_prompt_erase) {
297
+ this._state = "ASK_ERASE";
298
+ } else {
299
+ // Default is to erase a device that does not support Improv Serial
300
+ this._startInstall(true);
301
+ }
302
+ }}
303
+ ></ewt-button>
304
+ </div>
305
+
306
+ <div>
307
+ <ewt-button
308
+ label="Logs & Console"
309
+ @click=${async () => {
310
+ // Also set `null` back to undefined.
311
+ this._client = undefined;
312
+ this._state = "LOGS";
313
+ }}
314
+ ></ewt-button>
315
+ </div>
316
+ </div>
317
+ `;
318
+
319
+ return [heading, content, hideActions, allowClosing];
320
+ }
321
+
322
+ _renderProvision(): [string | undefined, TemplateResult, boolean] {
323
+ let heading: string | undefined = "Configure Wi-Fi";
324
+ let content: TemplateResult;
325
+ let hideActions = false;
326
+
327
+ if (this._busy) {
328
+ return [
329
+ heading,
330
+ this._renderProgress(
331
+ this._ssids === undefined
332
+ ? "Scanning for networks"
333
+ : "Trying to connect"
334
+ ),
335
+ true,
336
+ ];
337
+ }
338
+
339
+ if (
340
+ !this._provisionForce &&
341
+ this._client!.state === ImprovSerialCurrentState.PROVISIONED
342
+ ) {
343
+ heading = undefined;
344
+ const showSetupLinks =
345
+ !this._wasProvisioned &&
346
+ (this._client!.nextUrl !== undefined ||
347
+ "home_assistant_domain" in this._manifest);
348
+ hideActions = showSetupLinks;
349
+ content = html`
350
+ <ewt-page-message
351
+ .icon=${OK_ICON}
352
+ label="Device connected to the network!"
353
+ ></ewt-page-message>
354
+ ${showSetupLinks
355
+ ? html`
356
+ <div class="dashboard-buttons">
357
+ ${this._client!.nextUrl === undefined
358
+ ? ""
359
+ : html`
360
+ <div>
361
+ <a
362
+ href=${this._client!.nextUrl}
363
+ class="has-button"
364
+ target="_blank"
365
+ @click=${() => {
366
+ this._state = "DASHBOARD";
367
+ }}
368
+ >
369
+ <ewt-button label="Visit Device"></ewt-button>
370
+ </a>
371
+ </div>
372
+ `}
373
+ ${!this._manifest.home_assistant_domain
374
+ ? ""
375
+ : html`
376
+ <div>
377
+ <a
378
+ href=${`https://my.home-assistant.io/redirect/config_flow_start/?domain=${this._manifest.home_assistant_domain}`}
379
+ class="has-button"
380
+ target="_blank"
381
+ @click=${() => {
382
+ this._state = "DASHBOARD";
383
+ }}
384
+ >
385
+ <ewt-button
386
+ label="Add to Home Assistant"
387
+ ></ewt-button>
388
+ </a>
389
+ </div>
390
+ `}
391
+ <div>
392
+ <ewt-button
393
+ label="Skip"
394
+ @click=${() => {
395
+ this._state = "DASHBOARD";
396
+ }}
397
+ ></ewt-button>
398
+ </div>
399
+ </div>
400
+ `
401
+ : html`
402
+ <ewt-button
403
+ slot="primaryAction"
404
+ label="Continue"
405
+ @click=${() => {
406
+ this._state = "DASHBOARD";
407
+ }}
408
+ ></ewt-button>
409
+ `}
410
+ `;
411
+ } else {
412
+ let error: string | undefined;
413
+
414
+ switch (this._client!.error) {
415
+ case ImprovSerialErrorState.UNABLE_TO_CONNECT:
416
+ error = "Unable to connect";
417
+ break;
418
+
419
+ case ImprovSerialErrorState.NO_ERROR:
420
+ // Happens when list SSIDs not supported.
421
+ case ImprovSerialErrorState.UNKNOWN_RPC_COMMAND:
422
+ break;
423
+
424
+ default:
425
+ error = `Unknown error (${this._client!.error})`;
426
+ }
427
+ content = html`
428
+ <div>
429
+ Enter the credentials of the Wi-Fi network that you want your device
430
+ to connect to.
431
+ </div>
432
+ ${error ? html`<p class="error">${error}</p>` : ""}
433
+ ${this._ssids !== null
434
+ ? html`
435
+ <ewt-select
436
+ fixedMenuPosition
437
+ label="Network"
438
+ @selected=${(ev: { detail: { index: number } }) => {
439
+ const index = ev.detail.index;
440
+ // The "Join Other" item is always the last item.
441
+ this._selectedSsid =
442
+ index === this._ssids!.length ? -1 : index;
443
+ }}
444
+ @closed=${(ev: Event) => ev.stopPropagation()}
445
+ >
446
+ ${this._ssids!.map(
447
+ (info, idx) => html`
448
+ <ewt-list-item
449
+ .selected=${this._selectedSsid === idx}
450
+ value=${idx}
451
+ >
452
+ ${info.name}
453
+ </ewt-list-item>
454
+ `
455
+ )}
456
+ <ewt-list-item
457
+ .selected=${this._selectedSsid === -1}
458
+ value="-1"
459
+ >
460
+ Join other…
461
+ </ewt-list-item>
462
+ </ewt-select>
463
+ `
464
+ : ""}
465
+ ${
466
+ // Show input box if command not supported or "Join Other" selected
467
+ this._selectedSsid === -1
468
+ ? html`
469
+ <ewt-textfield label="Network Name" name="ssid"></ewt-textfield>
470
+ `
471
+ : ""
472
+ }
473
+ <ewt-textfield
474
+ label="Password"
475
+ name="password"
476
+ type="password"
477
+ ></ewt-textfield>
478
+ <ewt-button
479
+ slot="primaryAction"
480
+ label="Connect"
481
+ @click=${this._doProvision}
482
+ ></ewt-button>
483
+ <ewt-button
484
+ slot="secondaryAction"
485
+ .label=${this._installState && this._installErase ? "Skip" : "Back"}
486
+ @click=${() => {
487
+ this._state = "DASHBOARD";
488
+ }}
489
+ ></ewt-button>
490
+ `;
491
+ }
492
+ return [heading, content, hideActions];
493
+ }
494
+
495
+ _renderAskErase(): [string | undefined, TemplateResult] {
496
+ const heading = "Erase device";
497
+ const content = html`
498
+ <div>
499
+ Do you want to erase the device before installing
500
+ ${this._manifest.name}? All data on the device will be lost.
501
+ </div>
502
+ <ewt-formfield label="Erase device" class="danger">
503
+ <ewt-checkbox></ewt-checkbox>
504
+ </ewt-formfield>
505
+ <ewt-button
506
+ slot="primaryAction"
507
+ label="Next"
508
+ @click=${() => {
509
+ const checkbox = this.shadowRoot!.querySelector("ewt-checkbox")!;
510
+ this._startInstall(checkbox.checked);
511
+ }}
512
+ ></ewt-button>
513
+ <ewt-button
514
+ slot="secondaryAction"
515
+ label="Back"
516
+ @click=${() => {
517
+ this._state = "DASHBOARD";
518
+ }}
519
+ ></ewt-button>
520
+ `;
521
+
522
+ return [heading, content];
523
+ }
524
+
525
+ _renderInstall(): [string | undefined, TemplateResult, boolean, boolean] {
526
+ let heading: string | undefined;
527
+ let content: TemplateResult;
528
+ let hideActions = false;
529
+ const allowClosing = false;
530
+
531
+ const isUpdate = !this._installErase && this._isSameFirmware;
532
+
533
+ if (!this._installConfirmed && this._isSameVersion) {
534
+ heading = "Erase User Data";
535
+ content = html`
536
+ Do you want to reset your device and erase all user data from your
537
+ device?
538
+ <ewt-button
539
+ class="danger"
540
+ slot="primaryAction"
541
+ label="Erase User Data"
542
+ @click=${this._confirmInstall}
543
+ ></ewt-button>
544
+ `;
545
+ } else if (!this._installConfirmed) {
546
+ heading = "Confirm Installation";
547
+ const action = isUpdate ? "update to" : "install";
548
+ content = html`
549
+ ${isUpdate
550
+ ? html`Your device is running
551
+ ${this._info!.firmware}&nbsp;${this._info!.version}.<br /><br />`
552
+ : ""}
553
+ Do you want to ${action}
554
+ ${this._manifest.name}&nbsp;${this._manifest.version}?
555
+ ${this._installErase
556
+ ? html`<br /><br />All data on the device will be erased.`
557
+ : ""}
558
+ <ewt-button
559
+ slot="primaryAction"
560
+ label="Install"
561
+ @click=${this._confirmInstall}
562
+ ></ewt-button>
563
+ <ewt-button
564
+ slot="secondaryAction"
565
+ label="Back"
566
+ @click=${() => {
567
+ this._state = "DASHBOARD";
568
+ }}
569
+ ></ewt-button>
570
+ `;
571
+ } else if (
572
+ !this._installState ||
573
+ this._installState.state === FlashStateType.INITIALIZING ||
574
+ this._installState.state === FlashStateType.MANIFEST ||
575
+ this._installState.state === FlashStateType.PREPARING
576
+ ) {
577
+ heading = "Installing";
578
+ content = this._renderProgress("Preparing installation");
579
+ hideActions = true;
580
+ } else if (this._installState.state === FlashStateType.ERASING) {
581
+ heading = "Installing";
582
+ content = this._renderProgress("Erasing");
583
+ hideActions = true;
584
+ } else if (
585
+ this._installState.state === FlashStateType.WRITING ||
586
+ // When we're finished, keep showing this screen with 100% written
587
+ // until Improv is initialized / not detected.
588
+ (this._installState.state === FlashStateType.FINISHED &&
589
+ this._client === undefined)
590
+ ) {
591
+ heading = "Installing";
592
+ let percentage: number | undefined;
593
+ let undeterminateLabel: string | undefined;
594
+ if (this._installState.state === FlashStateType.FINISHED) {
595
+ // We're done writing and detecting improv, show spinner
596
+ undeterminateLabel = "Wrapping up";
597
+ } else if (this._installState.details.percentage < 4) {
598
+ // We're writing the firmware under 4%, show spinner or else we don't show any pixels
599
+ undeterminateLabel = "Installing";
600
+ } else {
601
+ // We're writing the firmware over 4%, show progress bar
602
+ percentage = this._installState.details.percentage;
603
+ }
604
+ content = this._renderProgress(
605
+ html`
606
+ ${undeterminateLabel ? html`${undeterminateLabel}<br />` : ""}
607
+ <br />
608
+ This will take
609
+ ${this._installState.chipFamily === "ESP8266"
610
+ ? "a minute"
611
+ : "2 minutes"}.<br />
612
+ Keep this page visible to prevent slow down
613
+ `,
614
+ percentage
615
+ );
616
+ hideActions = true;
617
+ } else if (this._installState.state === FlashStateType.FINISHED) {
618
+ heading = undefined;
619
+ const supportsImprov = this._client !== null;
620
+ content = html`
621
+ <ewt-page-message
622
+ .icon=${OK_ICON}
623
+ label="Installation complete!"
624
+ ></ewt-page-message>
625
+ <ewt-button
626
+ slot="primaryAction"
627
+ label="Next"
628
+ @click=${() => {
629
+ this._state =
630
+ supportsImprov && this._installErase ? "PROVISION" : "DASHBOARD";
631
+ }}
632
+ ></ewt-button>
633
+ `;
634
+ } else if (this._installState.state === FlashStateType.ERROR) {
635
+ heading = "Installation failed";
636
+ content = html`
637
+ <ewt-page-message
638
+ .icon=${ERROR_ICON}
639
+ .label=${this._installState.message}
640
+ ></ewt-page-message>
641
+ <ewt-button
642
+ slot="primaryAction"
643
+ label="Back"
644
+ @click=${async () => {
645
+ this._initialize();
646
+ this._state = "DASHBOARD";
647
+ }}
648
+ ></ewt-button>
649
+ `;
650
+ }
651
+ return [heading, content!, hideActions, allowClosing];
652
+ }
653
+
654
+ _renderLogs(): [string | undefined, TemplateResult, boolean] {
655
+ let heading: string | undefined = `Logs`;
656
+ let content: TemplateResult;
657
+ let hideActions = false;
658
+
659
+ content = html`
660
+ <ewt-console .port=${this.port} .logger=${this.logger}></ewt-console>
661
+ <ewt-button
662
+ slot="primaryAction"
663
+ label="Back"
664
+ @click=${async () => {
665
+ await this.shadowRoot!.querySelector("ewt-console")!.disconnect();
666
+ this._state = "DASHBOARD";
667
+ this._initialize();
668
+ }}
669
+ ></ewt-button>
670
+ <ewt-button
671
+ slot="secondaryAction"
672
+ label="Download Logs"
673
+ @click=${() => {
674
+ textDownload(
675
+ this.shadowRoot!.querySelector("ewt-console")!.logs(),
676
+ `esp-web-tools-logs.txt`
677
+ );
678
+
679
+ this.shadowRoot!.querySelector("ewt-console")!.reset();
680
+ }}
681
+ ></ewt-button>
682
+ <ewt-button
683
+ slot="secondaryAction"
684
+ label="Reset Device"
685
+ @click=${async () => {
686
+ await this.shadowRoot!.querySelector("ewt-console")!.reset();
687
+ }}
688
+ ></ewt-button>
689
+ `;
690
+
691
+ return [heading, content!, hideActions];
692
+ }
693
+
694
+ public override willUpdate(changedProps: PropertyValues) {
695
+ if (!changedProps.has("_state")) {
696
+ return;
697
+ }
698
+ // Clear errors when changing between pages unless we change
699
+ // to the error page.
700
+ if (this._state !== "ERROR") {
701
+ this._error = undefined;
702
+ }
703
+ // Scan for SSIDs on provision
704
+ if (this._state === "PROVISION") {
705
+ this._ssids = undefined;
706
+ this._busy = true;
707
+ this._client!.scan().then(
708
+ (ssids) => {
709
+ this._busy = false;
710
+ this._ssids = ssids;
711
+ this._selectedSsid = ssids.length ? 0 : -1;
712
+ },
713
+ () => {
714
+ this._busy = false;
715
+ this._ssids = null;
716
+ this._selectedSsid = -1;
717
+ }
718
+ );
719
+ } else {
720
+ // Reset this value if we leave provisioning.
721
+ this._provisionForce = false;
722
+ }
723
+
724
+ if (this._state === "INSTALL") {
725
+ this._installConfirmed = false;
726
+ this._installState = undefined;
727
+ }
728
+ }
729
+
730
+ protected override firstUpdated(changedProps: PropertyValues) {
731
+ super.firstUpdated(changedProps);
732
+ this._initialize();
733
+ }
734
+
735
+ protected override updated(changedProps: PropertyValues) {
736
+ super.updated(changedProps);
737
+
738
+ if (changedProps.has("_state")) {
739
+ this.setAttribute("state", this._state);
740
+ }
741
+
742
+ if (this._state !== "PROVISION") {
743
+ return;
744
+ }
745
+
746
+ if (changedProps.has("_selectedSsid") && this._selectedSsid === -1) {
747
+ // If we pick "Join other", select SSID input.
748
+ this._focusFormElement("ewt-textfield[name=ssid]");
749
+ } else if (changedProps.has("_ssids")) {
750
+ // Form is shown when SSIDs are loaded/marked not supported
751
+ this._focusFormElement();
752
+ }
753
+ }
754
+
755
+ private _focusFormElement(selector = "ewt-textfield, ewt-select") {
756
+ const formEl = this.shadowRoot!.querySelector(
757
+ selector
758
+ ) as LitElement | null;
759
+ if (formEl) {
760
+ formEl.updateComplete.then(() => setTimeout(() => formEl.focus(), 100));
761
+ }
762
+ }
763
+
764
+ private async _initialize(justInstalled = false) {
765
+ if (this.port.readable === null || this.port.writable === null) {
766
+ this._state = "ERROR";
767
+ this._error =
768
+ "Serial port is not readable/writable. Close any other application using it and try again.";
769
+ return;
770
+ }
771
+
772
+ try {
773
+ this._manifest = await downloadManifest(this.manifestPath);
774
+ } catch (err: any) {
775
+ this._state = "ERROR";
776
+ this._error = "Failed to download manifest";
777
+ return;
778
+ }
779
+
780
+ if (this._manifest.new_install_improv_wait_time === 0) {
781
+ this._client = null;
782
+ return;
783
+ }
784
+
785
+ const client = new ImprovSerial(this.port!, this.logger);
786
+ client.addEventListener("state-changed", () => {
787
+ this.requestUpdate();
788
+ });
789
+ client.addEventListener("error-changed", () => this.requestUpdate());
790
+ try {
791
+ // If a device was just installed, give new firmware 10 seconds (overridable) to
792
+ // format the rest of the flash and do other stuff.
793
+ const timeout = !justInstalled
794
+ ? 1000
795
+ : this._manifest.new_install_improv_wait_time !== undefined
796
+ ? this._manifest.new_install_improv_wait_time * 1000
797
+ : 10000;
798
+ this._info = await client.initialize(timeout);
799
+ this._client = client;
800
+ client.addEventListener("disconnect", this._handleDisconnect);
801
+ } catch (err: any) {
802
+ // Clear old value
803
+ this._info = undefined;
804
+ if (err instanceof PortNotReady) {
805
+ this._state = "ERROR";
806
+ this._error =
807
+ "Serial port is not ready. Close any other application using it and try again.";
808
+ } else {
809
+ this._client = null; // not supported
810
+ this.logger.error("Improv initialization failed.", err);
811
+ }
812
+ }
813
+ }
814
+
815
+ private _startInstall(erase: boolean) {
816
+ this._state = "INSTALL";
817
+ this._installErase = erase;
818
+ this._installConfirmed = false;
819
+ }
820
+
821
+ private async _confirmInstall() {
822
+ this._installConfirmed = true;
823
+ this._installState = undefined;
824
+ if (this._client) {
825
+ await this._closeClientWithoutEvents(this._client);
826
+ }
827
+ this._client = undefined;
828
+
829
+ flash(
830
+ (state) => {
831
+ this._installState = state;
832
+
833
+ if (state.state === FlashStateType.FINISHED) {
834
+ sleep(100)
835
+ .then(() => this._initialize(true))
836
+ .then(() => this.requestUpdate());
837
+ }
838
+ },
839
+ this.port,
840
+ this.logger,
841
+ this.manifestPath,
842
+ this._installErase
843
+ );
844
+ }
845
+
846
+ private async _doProvision() {
847
+ this._busy = true;
848
+ this._wasProvisioned =
849
+ this._client!.state === ImprovSerialCurrentState.PROVISIONED;
850
+ const ssid =
851
+ this._selectedSsid === -1
852
+ ? (
853
+ this.shadowRoot!.querySelector(
854
+ "ewt-textfield[name=ssid]"
855
+ ) as EwtTextfield
856
+ ).value
857
+ : this._ssids![this._selectedSsid].name;
858
+ const password = (
859
+ this.shadowRoot!.querySelector(
860
+ "ewt-textfield[name=password]"
861
+ ) as EwtTextfield
862
+ ).value;
863
+ try {
864
+ await this._client!.provision(ssid, password);
865
+ } catch (err: any) {
866
+ return;
867
+ } finally {
868
+ this._busy = false;
869
+ this._provisionForce = false;
870
+ }
871
+ }
872
+
873
+ private _handleDisconnect = () => {
874
+ this._state = "ERROR";
875
+ this._error = "Disconnected";
876
+ };
877
+
878
+ private async _handleClose() {
879
+ if (this._client) {
880
+ await this._closeClientWithoutEvents(this._client);
881
+ }
882
+ fireEvent(this, "closed" as any);
883
+ this.parentNode!.removeChild(this);
884
+ }
885
+
886
+ /**
887
+ * Return if the device runs same firmware as manifest.
888
+ */
889
+ private get _isSameFirmware() {
890
+ return !this._info
891
+ ? false
892
+ : this.overrides?.checkSameFirmware
893
+ ? this.overrides.checkSameFirmware(this._manifest, this._info)
894
+ : this._info.firmware === this._manifest.name;
895
+ }
896
+
897
+ /**
898
+ * Return if the device runs same firmware and version as manifest.
899
+ */
900
+ private get _isSameVersion() {
901
+ return (
902
+ this._isSameFirmware && this._info!.version === this._manifest.version
903
+ );
904
+ }
905
+
906
+ private async _closeClientWithoutEvents(client: ImprovSerial) {
907
+ client.removeEventListener("disconnect", this._handleDisconnect);
908
+ await client.close();
909
+ }
910
+
911
+ static styles = [
912
+ dialogStyles,
913
+ css`
914
+ :host {
915
+ --mdc-dialog-max-width: 390px;
916
+ }
917
+ ewt-icon-button {
918
+ position: absolute;
919
+ right: 4px;
920
+ top: 10px;
921
+ }
922
+ .table-row {
923
+ display: flex;
924
+ }
925
+ .table-row.last {
926
+ margin-bottom: 16px;
927
+ }
928
+ .table-row svg {
929
+ width: 20px;
930
+ margin-right: 8px;
931
+ }
932
+ ewt-textfield,
933
+ ewt-select {
934
+ display: block;
935
+ margin-top: 16px;
936
+ }
937
+ .dashboard-buttons {
938
+ margin: 0 0 -16px -8px;
939
+ }
940
+ .dashboard-buttons div {
941
+ display: block;
942
+ margin: 4px 0;
943
+ }
944
+ a.has-button {
945
+ text-decoration: none;
946
+ }
947
+ .error {
948
+ color: var(--improv-danger-color);
949
+ }
950
+ .danger {
951
+ --mdc-theme-primary: var(--improv-danger-color);
952
+ --mdc-theme-secondary: var(--improv-danger-color);
953
+ }
954
+ button.link {
955
+ background: none;
956
+ color: inherit;
957
+ border: none;
958
+ padding: 0;
959
+ font: inherit;
960
+ text-align: left;
961
+ text-decoration: underline;
962
+ cursor: pointer;
963
+ }
964
+ :host([state="LOGS"]) ewt-dialog {
965
+ --mdc-dialog-max-width: 90vw;
966
+ }
967
+ ewt-console {
968
+ width: calc(80vw - 48px);
969
+ height: 80vh;
970
+ }
971
+ `,
972
+ ];
973
+ }
974
+
975
+ customElements.define("ewt-install-dialog", EwtInstallDialog);
976
+
977
+ declare global {
978
+ interface HTMLElementTagNameMap {
979
+ "ewt-install-dialog": EwtInstallDialog;
980
+ }
981
+ }