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