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 |
|
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 |
|
74 |
|
75 | @state() private _ssids?: Ssid[] | null;
|
76 |
|
77 |
|
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 |
|
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} ${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 |
|
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 |
|
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} ${this._info!.version}.<br /><br />`
|
552 | : ""}
|
553 | Do you want to ${action}
|
554 | ${this._manifest.name} ${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.PREPARING
|
575 | ) {
|
576 | heading = "Installing";
|
577 | content = this._renderProgress("Preparing installation");
|
578 | hideActions = true;
|
579 | } else if (this._installState.state === FlashStateType.ERASING) {
|
580 | heading = "Installing";
|
581 | content = this._renderProgress("Erasing");
|
582 | hideActions = true;
|
583 | } else if (
|
584 | this._installState.state === FlashStateType.WRITING ||
|
585 | // When we're finished, keep showing this screen with 100% written
|
586 | // until Improv is initialized / not detected.
|
587 | (this._installState.state === FlashStateType.FINISHED &&
|
588 | this._client === undefined)
|
589 | ) {
|
590 | heading = "Installing";
|
591 | let percentage: number | undefined;
|
592 | let undeterminateLabel: string | undefined;
|
593 | if (this._installState.state === FlashStateType.FINISHED) {
|
594 | // We're done writing and detecting improv, show spinner
|
595 | undeterminateLabel = "Wrapping up";
|
596 | } else if (this._installState.details.percentage < 4) {
|
597 | // We're writing the firmware under 4%, show spinner or else we don't show any pixels
|
598 | undeterminateLabel = "Installing";
|
599 | } else {
|
600 | // We're writing the firmware over 4%, show progress bar
|
601 | percentage = this._installState.details.percentage;
|
602 | }
|
603 | content = this._renderProgress(
|
604 | html`
|
605 | ${undeterminateLabel ? html`${undeterminateLabel}<br />` : ""}
|
606 | <br />
|
607 | This will take
|
608 | ${this._installState.chipFamily === "ESP8266"
|
609 | ? "a minute"
|
610 | : "2 minutes"}.<br />
|
611 | Keep this page visible to prevent slow down
|
612 | `,
|
613 | percentage
|
614 | );
|
615 | hideActions = true;
|
616 | } else if (this._installState.state === FlashStateType.FINISHED) {
|
617 | heading = undefined;
|
618 | const supportsImprov = this._client !== null;
|
619 | content = html`
|
620 | <ewt-page-message
|
621 | .icon=${OK_ICON}
|
622 | label="Installation complete!"
|
623 | ></ewt-page-message>
|
624 | <ewt-button
|
625 | slot="primaryAction"
|
626 | label="Next"
|
627 | @click=${() => {
|
628 | this._state =
|
629 | supportsImprov && this._installErase ? "PROVISION" : "DASHBOARD";
|
630 | }}
|
631 | ></ewt-button>
|
632 | `;
|
633 | } else if (this._installState.state === FlashStateType.ERROR) {
|
634 | heading = "Installation failed";
|
635 | content = html`
|
636 | <ewt-page-message
|
637 | .icon=${ERROR_ICON}
|
638 | .label=${this._installState.message}
|
639 | ></ewt-page-message>
|
640 | <ewt-button
|
641 | slot="primaryAction"
|
642 | label="Back"
|
643 | @click=${async () => {
|
644 | this._initialize();
|
645 | this._state = "DASHBOARD";
|
646 | }}
|
647 | ></ewt-button>
|
648 | `;
|
649 | }
|
650 | return [heading, content!, hideActions, allowClosing];
|
651 | }
|
652 |
|
653 | _renderLogs(): [string | undefined, TemplateResult, boolean] {
|
654 | let heading: string | undefined = `Logs`;
|
655 | let content: TemplateResult;
|
656 | let hideActions = false;
|
657 |
|
658 | content = html`
|
659 | <ewt-console .port=${this.port} .logger=${this.logger}></ewt-console>
|
660 | <ewt-button
|
661 | slot="primaryAction"
|
662 | label="Back"
|
663 | @click=${async () => {
|
664 | await this.shadowRoot!.querySelector("ewt-console")!.disconnect();
|
665 | this._state = "DASHBOARD";
|
666 | this._initialize();
|
667 | }}
|
668 | ></ewt-button>
|
669 | <ewt-button
|
670 | slot="secondaryAction"
|
671 | label="Download Logs"
|
672 | @click=${() => {
|
673 | textDownload(
|
674 | this.shadowRoot!.querySelector("ewt-console")!.logs(),
|
675 | `esp-web-tools-logs.txt`
|
676 | );
|
677 |
|
678 | this.shadowRoot!.querySelector("ewt-console")!.reset();
|
679 | }}
|
680 | ></ewt-button>
|
681 | <ewt-button
|
682 | slot="secondaryAction"
|
683 | label="Reset Device"
|
684 | @click=${async () => {
|
685 | await this.shadowRoot!.querySelector("ewt-console")!.reset();
|
686 | }}
|
687 | ></ewt-button>
|
688 | `;
|
689 |
|
690 | return [heading, content!, hideActions];
|
691 | }
|
692 |
|
693 | public override willUpdate(changedProps: PropertyValues) {
|
694 | if (!changedProps.has("_state")) {
|
695 | return;
|
696 | }
|
697 | // Clear errors when changing between pages unless we change
|
698 | // to the error page.
|
699 | if (this._state !== "ERROR") {
|
700 | this._error = undefined;
|
701 | }
|
702 | // Scan for SSIDs on provision
|
703 | if (this._state === "PROVISION") {
|
704 | this._ssids = undefined;
|
705 | this._busy = true;
|
706 | this._client!.scan().then(
|
707 | (ssids) => {
|
708 | this._busy = false;
|
709 | this._ssids = ssids;
|
710 | this._selectedSsid = ssids.length ? 0 : -1;
|
711 | },
|
712 | () => {
|
713 | this._busy = false;
|
714 | this._ssids = null;
|
715 | this._selectedSsid = -1;
|
716 | }
|
717 | );
|
718 | } else {
|
719 | // Reset this value if we leave provisioning.
|
720 | this._provisionForce = false;
|
721 | }
|
722 |
|
723 | if (this._state === "INSTALL") {
|
724 | this._installConfirmed = false;
|
725 | this._installState = undefined;
|
726 | }
|
727 | }
|
728 |
|
729 | protected override firstUpdated(changedProps: PropertyValues) {
|
730 | super.firstUpdated(changedProps);
|
731 | this._initialize();
|
732 | }
|
733 |
|
734 | protected override updated(changedProps: PropertyValues) {
|
735 | super.updated(changedProps);
|
736 |
|
737 | if (changedProps.has("_state")) {
|
738 | this.setAttribute("state", this._state);
|
739 | }
|
740 |
|
741 | if (this._state !== "PROVISION") {
|
742 | return;
|
743 | }
|
744 |
|
745 | if (changedProps.has("_selectedSsid") && this._selectedSsid === -1) {
|
746 | // If we pick "Join other", select SSID input.
|
747 | this._focusFormElement("ewt-textfield[name=ssid]");
|
748 | } else if (changedProps.has("_ssids")) {
|
749 | // Form is shown when SSIDs are loaded/marked not supported
|
750 | this._focusFormElement();
|
751 | }
|
752 | }
|
753 |
|
754 | private _focusFormElement(selector = "ewt-textfield, ewt-select") {
|
755 | const formEl = this.shadowRoot!.querySelector(
|
756 | selector
|
757 | ) as LitElement | null;
|
758 | if (formEl) {
|
759 | formEl.updateComplete.then(() => setTimeout(() => formEl.focus(), 100));
|
760 | }
|
761 | }
|
762 |
|
763 | private async _initialize(justInstalled = false) {
|
764 | if (this.port.readable === null || this.port.writable === null) {
|
765 | this._state = "ERROR";
|
766 | this._error =
|
767 | "Serial port is not readable/writable. Close any other application using it and try again.";
|
768 | return;
|
769 | }
|
770 |
|
771 | try {
|
772 | this._manifest = await downloadManifest(this.manifestPath);
|
773 | } catch (err: any) {
|
774 | this._state = "ERROR";
|
775 | this._error = "Failed to download manifest";
|
776 | return;
|
777 | }
|
778 |
|
779 | if (this._manifest.new_install_improv_wait_time === 0) {
|
780 | this._client = null;
|
781 | return;
|
782 | }
|
783 |
|
784 | const client = new ImprovSerial(this.port!, this.logger);
|
785 | client.addEventListener("state-changed", () => {
|
786 | this.requestUpdate();
|
787 | });
|
788 | client.addEventListener("error-changed", () => this.requestUpdate());
|
789 | try {
|
790 | // If a device was just installed, give new firmware 10 seconds (overridable) to
|
791 | // format the rest of the flash and do other stuff.
|
792 | const timeout = !justInstalled
|
793 | ? 1000
|
794 | : this._manifest.new_install_improv_wait_time !== undefined
|
795 | ? this._manifest.new_install_improv_wait_time * 1000
|
796 | : 10000;
|
797 | this._info = await client.initialize(timeout);
|
798 | this._client = client;
|
799 | client.addEventListener("disconnect", this._handleDisconnect);
|
800 | } catch (err: any) {
|
801 | // Clear old value
|
802 | this._info = undefined;
|
803 | if (err instanceof PortNotReady) {
|
804 | this._state = "ERROR";
|
805 | this._error =
|
806 | "Serial port is not ready. Close any other application using it and try again.";
|
807 | } else {
|
808 | this._client = null; // not supported
|
809 | this.logger.error("Improv initialization failed.", err);
|
810 | }
|
811 | }
|
812 | }
|
813 |
|
814 | private _startInstall(erase: boolean) {
|
815 | this._state = "INSTALL";
|
816 | this._installErase = erase;
|
817 | this._installConfirmed = false;
|
818 | }
|
819 |
|
820 | private async _confirmInstall() {
|
821 | this._installConfirmed = true;
|
822 | this._installState = undefined;
|
823 | if (this._client) {
|
824 | await this._closeClientWithoutEvents(this._client);
|
825 | }
|
826 | this._client = undefined;
|
827 |
|
828 | // Close port. ESPLoader likes opening it.
|
829 | await this.port.close();
|
830 | flash(
|
831 | (state) => {
|
832 | this._installState = state;
|
833 |
|
834 | if (state.state === FlashStateType.FINISHED) {
|
835 | sleep(100)
|
836 | // Flashing closes the port
|
837 | .then(() => this.port.open({ baudRate: 115200 }))
|
838 | .then(() => this._initialize(true))
|
839 | .then(() => this.requestUpdate());
|
840 | } else if (state.state === FlashStateType.ERROR) {
|
841 | sleep(100)
|
842 | // Flashing closes the port
|
843 | .then(() => this.port.open({ baudRate: 115200 }));
|
844 | }
|
845 | },
|
846 | this.port,
|
847 | this.manifestPath,
|
848 | this._manifest,
|
849 | this._installErase
|
850 | );
|
851 | }
|
852 |
|
853 | private async _doProvision() {
|
854 | this._busy = true;
|
855 | this._wasProvisioned =
|
856 | this._client!.state === ImprovSerialCurrentState.PROVISIONED;
|
857 | const ssid =
|
858 | this._selectedSsid === -1
|
859 | ? (
|
860 | this.shadowRoot!.querySelector(
|
861 | "ewt-textfield[name=ssid]"
|
862 | ) as EwtTextfield
|
863 | ).value
|
864 | : this._ssids![this._selectedSsid].name;
|
865 | const password = (
|
866 | this.shadowRoot!.querySelector(
|
867 | "ewt-textfield[name=password]"
|
868 | ) as EwtTextfield
|
869 | ).value;
|
870 | try {
|
871 | await this._client!.provision(ssid, password);
|
872 | } catch (err: any) {
|
873 | return;
|
874 | } finally {
|
875 | this._busy = false;
|
876 | this._provisionForce = false;
|
877 | }
|
878 | }
|
879 |
|
880 | private _handleDisconnect = () => {
|
881 | this._state = "ERROR";
|
882 | this._error = "Disconnected";
|
883 | };
|
884 |
|
885 | private async _handleClose() {
|
886 | if (this._client) {
|
887 | await this._closeClientWithoutEvents(this._client);
|
888 | }
|
889 | fireEvent(this, "closed" as any);
|
890 | this.parentNode!.removeChild(this);
|
891 | }
|
892 |
|
893 | /**
|
894 | * Return if the device runs same firmware as manifest.
|
895 | */
|
896 | private get _isSameFirmware() {
|
897 | return !this._info
|
898 | ? false
|
899 | : this.overrides?.checkSameFirmware
|
900 | ? this.overrides.checkSameFirmware(this._manifest, this._info)
|
901 | : this._info.firmware === this._manifest.name;
|
902 | }
|
903 |
|
904 | /**
|
905 | * Return if the device runs same firmware and version as manifest.
|
906 | */
|
907 | private get _isSameVersion() {
|
908 | return (
|
909 | this._isSameFirmware && this._info!.version === this._manifest.version
|
910 | );
|
911 | }
|
912 |
|
913 | private async _closeClientWithoutEvents(client: ImprovSerial) {
|
914 | client.removeEventListener("disconnect", this._handleDisconnect);
|
915 | await client.close();
|
916 | }
|
917 |
|
918 | static styles = [
|
919 | dialogStyles,
|
920 | css`
|
921 | :host {
|
922 | --mdc-dialog-max-width: 390px;
|
923 | }
|
924 | ewt-icon-button {
|
925 | position: absolute;
|
926 | right: 4px;
|
927 | top: 10px;
|
928 | }
|
929 | .table-row {
|
930 | display: flex;
|
931 | }
|
932 | .table-row.last {
|
933 | margin-bottom: 16px;
|
934 | }
|
935 | .table-row svg {
|
936 | width: 20px;
|
937 | margin-right: 8px;
|
938 | }
|
939 | ewt-textfield,
|
940 | ewt-select {
|
941 | display: block;
|
942 | margin-top: 16px;
|
943 | }
|
944 | .dashboard-buttons {
|
945 | margin: 0 0 -16px -8px;
|
946 | }
|
947 | .dashboard-buttons div {
|
948 | display: block;
|
949 | margin: 4px 0;
|
950 | }
|
951 | a.has-button {
|
952 | text-decoration: none;
|
953 | }
|
954 | .error {
|
955 | color: var(--improv-danger-color);
|
956 | }
|
957 | .danger {
|
958 | --mdc-theme-primary: var(--improv-danger-color);
|
959 | --mdc-theme-secondary: var(--improv-danger-color);
|
960 | }
|
961 | button.link {
|
962 | background: none;
|
963 | color: inherit;
|
964 | border: none;
|
965 | padding: 0;
|
966 | font: inherit;
|
967 | text-align: left;
|
968 | text-decoration: underline;
|
969 | cursor: pointer;
|
970 | }
|
971 | :host([state="LOGS"]) ewt-dialog {
|
972 | --mdc-dialog-max-width: 90vw;
|
973 | }
|
974 | ewt-console {
|
975 | width: calc(80vw - 48px);
|
976 | height: 80vh;
|
977 | }
|
978 | `,
|
979 | ];
|
980 | }
|
981 |
|
982 | customElements.define("ewt-install-dialog", EwtInstallDialog);
|
983 |
|
984 | declare global {
|
985 | interface HTMLElementTagNameMap {
|
986 | "ewt-install-dialog": EwtInstallDialog;
|
987 | }
|
988 | }
|
989 |
|
\ | No newline at end of file |