UNPKG

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