UNPKG

33.1 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 listItemHomeAssistant,
21 listItemInstallIcon,
22 listItemVisitDevice,
23 listItemWifi,
24 refreshIcon,
25} from "./components/svg";
26import { Logger, Manifest, FlashStateType, FlashState } from "./const.js";
27import { ImprovSerial, Ssid } from "improv-wifi-serial-sdk/dist/serial";
28import {
29 ImprovSerialCurrentState,
30 ImprovSerialErrorState,
31 PortNotReady,
32} from "improv-wifi-serial-sdk/dist/const";
33import { flash } from "./flash";
34import { textDownload } from "./util/file-download";
35import { fireEvent } from "./util/fire-event";
36import { sleep } from "./util/sleep";
37import { downloadManifest } from "./util/manifest";
38import { dialogStyles } from "./styles";
39import { version } from "./version";
40import type { EwFilledSelect } from "./components/ew-filled-select";
41
42console.log(
43 `ESP Web Tools ${version} by Nabu Casa; https://esphome.github.io/esp-web-tools/`,
44);
45
46const ERROR_ICON = "⚠️";
47const OK_ICON = "🎉";
48
49export 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 // null = NOT_SUPPORTED
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 // undefined = not loaded
90 // null = not available
91 @state() private _ssids?: Ssid[] | null;
92
93 // Name of Ssid. Null = other
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 // During installation phase we temporarily remove the client
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}&nbsp;${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 // Default is to erase a device that does not support Improv Serial
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 // Also set `null` back to undefined.
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}&nbsp;${this._info!.version}.<br /><br />`
594 : ""}
595 Do you want to ${action}
596 ${this._manifest.name}&nbsp;${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
1098customElements.define("ewt-install-dialog", EwtInstallDialog);
1099
1100declare global {
1101 interface HTMLElementTagNameMap {
1102 "ewt-install-dialog": EwtInstallDialog;
1103 }
1104}
1105
\No newline at end of file