UNPKG

tasmota-esp-web-tools

Version:
1,155 lines (1,153 loc) 120 kB
import { __decorate } from "tslib"; import { LitElement, html, css } from "lit"; import { state } from "lit/decorators.js"; import "./components/ew-text-button"; import "./components/ew-checkbox"; import "./components/ew-console"; import "./components/ew-dialog"; import "./components/ew-icon-button"; import "./components/ew-filled-text-field"; import "./components/ew-filled-select"; import "./components/ew-select-option"; import "./components/ew-divider"; import "./components/ew-list"; import "./components/ew-list-item"; import "./components/ew-littlefs-manager"; import "./pages/ew-page-progress"; import "./pages/ew-page-message"; import { closeIcon, warningIcon, checkCircleIcon, listItemInstallIcon, listItemWifi, listItemConsole, listItemVisitDevice, listItemHomeAssistant, listItemEraseUserData, listItemFundDevelopment, firmwareIcon, downloadIcon, } from "./components/svg"; import { ImprovSerial } from "improv-wifi-serial-sdk/dist/serial"; import { ImprovSerialCurrentState, } from "improv-wifi-serial-sdk/dist/const"; import { flash, detectMatchingBuild, getFirmwareFileName } from "./flash"; import { textDownload } from "./util/file-download"; import { fireEvent } from "./util/fire-event"; import { sleep } from "./util/sleep"; import { downloadManifest } from "./util/manifest"; import { dialogStyles } from "./styles"; import { parsePartitionTable } from "./partition.js"; import { detectFilesystemType } from "./util/partition.js"; import { getChipFamilyName } from "./util/chip-family-name"; const ERROR_ICON = warningIcon; const OK_ICON = checkCircleIcon; export class EwtInstallDialog extends LitElement { constructor() { super(...arguments); this.logger = console; this._state = "DASHBOARD"; this._installErase = false; this._installConfirmed = false; this._provisionForce = false; this._wasProvisioned = false; this._busy = true; // Start as busy until initialization completes // Name of Ssid. Null = other this._selectedSsid = null; // Track if Improv was already checked (to avoid repeated attempts) this._improvChecked = false; // Track if console was already opened once (to avoid repeated resets) this._consoleInitialized = false; // Track if Improv is supported (separate from active client) this._improvSupported = false; // Track if device is using USB-JTAG or USB-OTG (not external serial chip) this._isUsbJtagOrOtgDevice = false; // Track action to perform after port reconnection (for USB-JTAG/OTG devices) this._openConsoleAfterReconnect = false; this._visitDeviceAfterReconnect = false; this._addToHAAfterReconnect = false; this._changeWiFiAfterReconnect = false; this._handleDisconnect = () => { this._state = "ERROR"; this._error = "Disconnected"; // Reset flash size and detected build when device is actually disconnected this._flashSize = undefined; this._detectedBuild = undefined; }; } // Ensure stub is initialized (called before any operation that needs it) async _ensureStub() { if (this._espStub && this._espStub.IS_STUB) { this.logger.log(`Existing stub: IS_STUB=${this._espStub.IS_STUB}, chipFamily=${getChipFamilyName(this._espStub)}`); // Ensure baudrate is set even if stub already exists if (this.baudRate && this.baudRate > 115200) { const currentBaud = this._espStub.currentBaudRate || 115200; if (currentBaud !== this.baudRate) { this.logger.log(`Adjusting baudrate from ${currentBaud} to ${this.baudRate}...`); try { await this._espStub.setBaudrate(this.baudRate); this.logger.log(`Baudrate set to ${this.baudRate}`); // Update currentBaudRate to prevent re-setting this._espStub.currentBaudRate = this.baudRate; } catch (baudErr) { this.logger.log(`Failed to set baudrate: ${baudErr.message}, continuing with current`); // Assume baudrate is already correct if setBaudrate fails this._espStub.currentBaudRate = this.baudRate; } } else { this.logger.log(`Baudrate already at ${this.baudRate}, skipping`); } } return this._espStub; } // Initialize if not already done if (!this.esploader.chipFamily) { this.logger.log("Initializing ESP loader..."); // Try twice before giving up for (let attempt = 1; attempt <= 2; attempt++) { try { if (attempt > 1) { this.logger.log(`Retry attempt ${attempt}/2...`); await sleep(500); // Wait before retry } await this.esploader.initialize(); this.logger.log(`Found ${getChipFamilyName(this.esploader)}`); break; // Success! } catch (err) { this.logger.error(`Connection failed to stub (attempt ${attempt}/2): ${err.message}`); if (attempt === 2) { // Both attempts failed - show error to user this._state = "ERROR"; this._error = `Failed to connect to ESP after 2 attempts: ${err.message}`; throw err; } } } } // Run stub - chip properties are now automatically inherited from parent this.logger.log("Running stub..."); const espStub = await this.esploader.runStub(); this.logger.log(`Stub created: IS_STUB=${espStub.IS_STUB}, chipFamily=${getChipFamilyName(espStub)}`); this._espStub = espStub; // Detect flash size AFTER stub creation (flash size is only available after stub) await this._probeFlashSize(espStub); // Set baudrate BEFORE any operations (use user-selected baudrate if available) if (this.baudRate && this.baudRate > 115200) { this.logger.log(`Setting baudrate to ${this.baudRate}...`); try { // setBaudrate now supports CDC/JTAG on Android (WebUSB) await espStub.setBaudrate(this.baudRate); this.logger.log(`Baudrate set to ${this.baudRate}`); // Update currentBaudRate to prevent re-setting espStub.currentBaudRate = this.baudRate; } catch (baudErr) { this.logger.error(`[DEBUG] setBaudrate() threw error: ${baudErr.message}`); this.logger.log(`Failed to set baudrate: ${baudErr.message}, continuing with default`); } } this.logger.log(`Returning stub: IS_STUB=${this._espStub.IS_STUB}, chipFamily=${getChipFamilyName(this._espStub)}`); return this._espStub; } // Helper to get port from esploader get _port() { return this.esploader.port; } // Helper to probe flash size from a running stub (only available after stub) async _probeFlashSize(espStub) { var _a; if (espStub.detectFlashSize && !this._flashSize) { try { await espStub.detectFlashSize(); this._flashSize = espStub.flashSize; this.logger.log(`Flash size detected: ${this._flashSize}`); } catch (err) { this.logger.debug("Failed to detect flash size:", err); } } this._detectedBuild = detectMatchingBuild(this._manifest, espStub, this._flashSize, this._isUsbJtagOrOtgDevice, (_a = this._info) === null || _a === void 0 ? void 0 : _a.chipFamily); } // Helper to check if device is using USB-JTAG or USB-OTG (not external serial chip) async _isUsbJtagOrOtg() { // Use detectUsbConnectionType from tasmota-webserial-esptool const isUsbJtag = await this.esploader.detectUsbConnectionType(); this.logger.log(`USB-JTAG/OTG detection: ${isUsbJtag ? "YES" : "NO"}`); return isUsbJtag; } // Helper to check if device is WebUSB with external serial chip async _isWebUsbWithExternalSerial() { const isWebUsb = this.esploader.isWebUSB && this.esploader.isWebUSB(); if (!isWebUsb) { return false; } const isUsbJtag = await this._isUsbJtagOrOtg(); const result = !isUsbJtag; // WebUSB but NOT USB-JTAG = external serial this.logger.log(`WebUSB with external serial: ${result ? "YES" : "NO"}`); return result; } // Helper to release reader/writer locks (used by multiple methods) async _releaseReaderWriter() { // CRITICAL: Find the actual object that has the reader // The stub has a _parent pointer, and the reader runs on the parent! let readerOwner = this._espStub || this.esploader; if (readerOwner._parent) { readerOwner = readerOwner._parent; this.logger.log("Using parent loader for reader/writer"); } // Cancel the reader on the correct object if (readerOwner._reader) { const reader = readerOwner._reader; try { await reader.cancel(); this.logger.log("Reader cancelled on correct object"); } catch (err) { this.logger.log("Reader cancel failed:", err); } try { reader.releaseLock(); this.logger.log("Reader released"); } catch (err) { this.logger.log("Reader releaseLock failed:", err); } readerOwner._reader = undefined; } // Release the writer on the correct object if (readerOwner._writer) { const writer = readerOwner._writer; readerOwner._writer = undefined; try { writer.releaseLock(); this.logger.log("Writer lock released"); } catch (err) { this.logger.log("Writer releaseLock failed:", err); } } // For WebUSB (Android), ALWAYS recreate streams // This is CRITICAL for console to work - WebUSB needs fresh streams // Even if no locks were held, streams may have been consumed by other operations if (this.esploader.isWebUSB && this.esploader.isWebUSB()) { try { this.logger.log("WebUSB detected - recreating streams"); await this._port.recreateStreams(); await sleep(200); this.logger.log("WebUSB streams recreated and ready"); } catch (err) { this.logger.log(`Failed to recreate WebUSB streams: ${err.message}`); } } } // Helper to reset baudrate to 115200 for console // The ESP stub might be at higher baudrate (e.g., 460800) for flashing // Firmware console always runs at 115200 async _resetBaudrateForConsole() { if (this._espStub && this._espStub.currentBaudRate !== 115200) { this.logger.log(`Resetting baudrate from ${this._espStub.currentBaudRate} to 115200`); try { // Use setBaudrate from tasmota-webserial-esptool // This now supports CDC/JTAG baudrate changes on Android (WebUSB) await this._espStub.setBaudrate(115200); this.logger.log("Baudrate set to 115200 for console"); } catch (baudErr) { this.logger.log(`Failed to set baudrate to 115200: ${baudErr.message}`); } } } // Helper to prepare ESP for flash operations after Improv check // Resets to bootloader mode and loads stub async _prepareForFlashOperations() { // Reset ESP to BOOTLOADER mode for flash operations await this._resetToBootloaderAndReleaseLocks(); // Wait for ESP to enter bootloader mode await sleep(100); // Reset ESP state (chipFamily preserved from reset if successful) this._espStub = undefined; this.esploader.IS_STUB = false; // Ensure stub is initialized await this._ensureStub(); this.logger.log("ESP reset, stub loaded - ready for flash operations"); } // Helper to handle post-flash cleanup and Improv re-initialization // Called when flash operation completes successfully async _handleFlashComplete() { // Check if this is USB-JTAG or USB-OTG device (not external serial chip) const isUsbJtagOrOtg = await this._isUsbJtagOrOtg(); this._isUsbJtagOrOtgDevice = isUsbJtagOrOtg; // Update state for UI if (isUsbJtagOrOtg) { // For USB-JTAG/OTG devices: Reset to firmware mode (port will change!) // Then user must select new port (User Gesture) and we test Improv this.logger.log("USB-JTAG/OTG device - resetting to firmware mode"); // CRITICAL: Release locks BEFORE resetToFirmware() await this._releaseReaderWriter(); // CRITICAL: Forget the old port so browser doesn't show it in selection try { await this._port.forget(); this.logger.log("Old port forgotten"); } catch (forgetErr) { this.logger.log(`Port forget failed: ${forgetErr.message}`); } try { // Use resetToFirmware() method close the port and device will reboot to firmware await this.esploader.resetToFirmware(); this.logger.log("Device reset to firmware mode - port closed"); } catch (err) { this.logger.debug(`Reset to firmware error (expected): ${err.message}`); } // Reset ESP state await sleep(100); this._espStub = undefined; this.esploader.IS_STUB = false; this.esploader.chipFamily = null; this._improvChecked = false; // Will check after user reconnects this._client = null; // Set to null (not undefined) to avoid "Wrapping up" UI state this._improvSupported = false; // Unknown until after reconnect this.esploader._reader = undefined; this.logger.log("Flash complete - waiting for user to select new port"); // CRITICAL: Set state to REQUEST_PORT_SELECTION to show "Select Port" button this._state = "REQUEST_PORT_SELECTION"; this._error = ""; this.requestUpdate(); return; } // Normal flow for non-USB-JTAG/OTG devices // Release locks and reset ESP state for Improv test await this._releaseReaderWriter(); // Reset ESP state for Improv test this._espStub = undefined; this.esploader.IS_STUB = false; this.esploader.chipFamily = null; this._improvChecked = false; this.esploader._reader = undefined; this.logger.log("ESP state reset for Improv test"); // Reconnect with 115200 baud and reset ESP to boot into new firmware try { // CRITICAL: After flashing at higher baudrate, reconnect at 115200 // reconnectToBootloader() closes port and reopens at 115200 baud // It now automatically detects WebUSB vs WebSerial and uses appropriate methods this.logger.log("Reconnecting at 115200 baud for firmware reset..."); try { await this.esploader.reconnectToBootloader(); this.logger.log("Port reconnected at 115200 baud"); } catch (reconnectErr) { this.logger.log(`Reconnect failed: ${reconnectErr.message}`); } // Reset device and release locks to ensure clean state for new firmware // Uses chip-specific reset methods (S2/S3/C3 with USB-JTAG use watchdog) this.logger.log("Performing hardware reset to start new firmware..."); await this._resetDeviceAndReleaseLocks(); } catch (resetErr) { this.logger.log(`Hard reset failed: ${resetErr.message}`); } // Test Improv with new firmware await this._initialize(true); this.requestUpdate(); } // Reset device and release locks - used when returning to dashboard or recovering from errors // Reset device to FIRMWARE mode (normal execution) async _resetDeviceAndReleaseLocks() { // Find the actual object that has the reader/writer let readerOwner = this._espStub || this.esploader; if (readerOwner._parent) { readerOwner = readerOwner._parent; this.logger.log("Using parent loader for reader/writer"); } // Call hardReset BEFORE releasing locks (so it can communicate) try { await this.esploader.hardReset(false); this.logger.log("Device reset sent"); } catch (err) { this.logger.log("Reset error (expected):", err); } // Wait for reset to complete await sleep(500); // NOW release locks after reset await this._releaseReaderWriter(); this.logger.log("Device reset to firmware mode"); // Reset ESP state this._espStub = undefined; this.esploader.IS_STUB = false; this.esploader.chipFamily = null; } // Reset device to BOOTLOADER mode (for flashing) // Uses ESPLoader's reconnectToBootloader() to properly close/reopen port async _resetToBootloaderAndReleaseLocks() { // Use ESPLoader's reconnectToBootloader() - it handles: // - Closing port completely (releases all locks) // - Reopening port at 115200 baud // - Restarting readLoop() // - Reset strategies to enter bootloader (connectWithResetStrategies) // - Chip detection // - WebUSB vs WebSerial detection and appropriate reset methods try { this.logger.log("Resetting ESP to bootloader mode..."); await this.esploader.reconnectToBootloader(); this.logger.log(`ESP in bootloader mode: ${getChipFamilyName(this.esploader)}`); } catch (err) { this.logger.error(`Failed to reset ESP to bootloader: ${err.message}`); throw err; } // Reset stub state (chipFamily is preserved by reconnectToBootloader) this._espStub = undefined; this.esploader.IS_STUB = false; } render() { if (!this.esploader) { return html ``; } // Safety check: Don't render DASHBOARD state until Improv check is complete if (this._state === "DASHBOARD" && !this._improvChecked) { return html ` <ew-dialog open @cancel=${this._preventDefault}> <div slot="headline">Connecting</div> <div slot="content">${this._renderProgress("Initializing")}</div> </ew-dialog> `; } let heading; let content; let allowClosing = false; // During installation phase we temporarily remove the client if (this._client === undefined && !this._improvChecked && // Only show "Connecting" if we haven't checked yet this._state !== "INSTALL" && this._state !== "LOGS" && this._state !== "PARTITIONS" && this._state !== "LITTLEFS" && this._state !== "REQUEST_PORT_SELECTION" && this._state !== "DASHBOARD" // Don't show "Connecting" when in DASHBOARD state ) { if (this._error) { [heading, content] = this._renderError(this._error); } else { content = this._renderProgress("Connecting"); } } else if (this._state === "INSTALL") { [heading, content, , allowClosing] = this._renderInstall(); } else if (this._state === "REQUEST_PORT_SELECTION") { [heading, content] = this._renderRequestPortSelection(); } else if (this._state === "ASK_ERASE") { [heading, content] = this._renderAskErase(); } else if (this._state === "ERROR") { [heading, content] = this._renderError(this._error); } else if (this._state === "DASHBOARD") { try { [heading, content, , allowClosing] = this._improvSupported && this._info ? this._renderDashboard() : this._renderDashboardNoImprov(); } catch (err) { this.logger.error(`Error rendering dashboard: ${err.message}`, err); [heading, content] = this._renderError(`Dashboard render error: ${err.message}`); } } else if (this._state === "PROVISION") { [heading, content] = this._renderProvision(); } else if (this._state === "LOGS") { [heading, content] = this._renderLogs(); } else if (this._state === "PARTITIONS") { [heading, content] = this._renderPartitions(); } else if (this._state === "LITTLEFS") { [heading, content, , allowClosing] = this._renderLittleFS(); } else { // Fallback for unknown state this.logger.error(`Unknown state: ${this._state}`); [heading, content] = this._renderError(`Unknown state: ${this._state}`); } return html ` <ew-dialog open @cancel=${this._preventDefault} @closed=${this._handleClose} > ${heading ? html ` <div slot="headline">${heading}</div> ${allowClosing ? html ` <ew-icon-button slot="headline" @click=${this._closeDialog}> ${closeIcon} </ew-icon-button> ` : ""} ` : ""} <div slot="content">${content}</div> </ew-dialog> `; } _renderProgress(label, progress) { return html ` <ewt-page-progress .label=${label} .progress=${progress} ></ewt-page-progress> `; } _renderError(label) { const heading = "Error"; const content = html ` <ewt-page-message .icon=${ERROR_ICON} .label=${label}></ewt-page-message> <ew-text-button slot="actions" @click=${this._closeDialog} >Close</ew-text-button > `; const hideActions = false; return [heading, content, hideActions]; } _renderRequestPortSelection() { const heading = "Select Port"; const content = html ` <ewt-page-message .label=${"Device has been reset to firmware mode. The USB port has changed. Please click the button below to select the new port."} ></ewt-page-message> <ew-text-button slot="actions" ?disabled=${this._busy} @click=${this._handleSelectNewPort} >Select Port</ew-text-button > `; const hideActions = false; return [heading, content, hideActions]; } _renderDashboard() { const heading = this._info.name; const hideActions = true; const allowClosing = true; const content = html ` <ew-list> <ew-list-item> <div slot="headline">Connected to ${this._info.name}</div> <div slot="supporting-text"> ${this._info.firmware}&nbsp;${this._info.version} (${this._info.chipFamily}${this._flashSize ? `, ${this._flashSize}` : ""}) </div> </ew-list-item> ${!this._isSameVersion ? html ` <ew-list-item type="button" ?disabled=${this._busy} @click=${() => { if (this._isSameFirmware) { this._startInstall(false); } else if (this._manifest.new_install_prompt_erase) { this._state = "ASK_ERASE"; } else { this._startInstall(true); } }} > ${listItemInstallIcon} <div slot="headline"> ${!this._isSameFirmware ? `Install ${this._manifest.name}` : `Update ${this._manifest.name}`} </div> ${(() => { const label = this._detectedBuild ? this._buildVariantLabel(this._detectedBuild) : undefined; return label ? html `<div slot="supporting-text">Variant: ${label}</div>` : ""; })()} </ew-list-item> ` : ""} ${!this._client || this._client.nextUrl === undefined ? "" : html ` <ew-list-item type="button" ?disabled=${this._busy} @click=${async () => { this._busy = true; // Switch to firmware mode if needed const needsReconnect = await this._switchToFirmwareMode("visit"); if (needsReconnect) { return; // Will continue after port reconnection } // Device is in firmware mode - open URL if (this._client && this._client.nextUrl) { window.open(this._client.nextUrl, "_blank", "noopener,noreferrer"); } this._busy = false; }} > ${listItemVisitDevice} <div slot="headline">Visit Device</div> </ew-list-item> `} ${!this._client || !this._manifest.home_assistant_domain || this._client.state !== ImprovSerialCurrentState.PROVISIONED ? "" : html ` <ew-list-item type="button" ?disabled=${this._busy} @click=${async () => { this._busy = true; // Switch to firmware mode if needed const needsReconnect = await this._switchToFirmwareMode("homeassistant"); if (needsReconnect) { return; // Will continue after port reconnection } // Device is in firmware mode - open HA URL if (this._manifest.home_assistant_domain) { window.open(`https://my.home-assistant.io/redirect/config_flow_start/?domain=${this._manifest.home_assistant_domain}`, "_blank", "noopener,noreferrer"); } this._busy = false; }} > ${listItemHomeAssistant} <div slot="headline">Add to Home Assistant</div> </ew-list-item> `} ${this._client ? html ` <ew-list-item type="button" ?disabled=${this._busy} @click=${async () => { this._busy = true; // Switch to firmware mode if needed const needsReconnect = await this._switchToFirmwareMode("wifi"); if (needsReconnect) { return; // Will continue after port reconnection } // Device is in firmware mode this.logger.log("Device is running firmware for Wi-Fi setup"); // Close Improv client and re-initialize for WiFi setup if (this._client) { try { await this._closeClientWithoutEvents(this._client); this.logger.log("Improv client closed"); } catch (e) { this.logger.log("Failed to close Improv client:", e); } this._client = undefined; // Wait longer for port to be fully released await sleep(500); } // Different handling for different device types: // - WebSerial: Just release locks // - WebUSB CDC: Release locks, hardReset, release locks again // - WebUSB external serial: Just release locks const isWebUsbExternal = await this._isWebUsbWithExternalSerial(); const isWebUsbCdc = this.esploader.isWebUSB && this.esploader.isWebUSB() && !isWebUsbExternal; if (isWebUsbCdc) { // WebUSB CDC needs hardReset to ensure firmware is running this.logger.log("WebUSB CDC: Resetting device for Wi-Fi setup..."); try { // Release locks BEFORE reset await this._releaseReaderWriter(); // Reset device await this.esploader.hardReset(false); this.logger.log("Device reset completed"); // CRITICAL: hardReset consumes streams, recreate them await this._releaseReaderWriter(); this.logger.log("Streams recreated after reset"); // Wait for device to boot await sleep(500); } catch (err) { this.logger.log(`Reset error: ${err.message}`); } } else { // WebSerial or WebUSB external serial: Just release locks if (isWebUsbExternal) { this.logger.log("WebUSB external serial: Preparing port for Wi-Fi setup..."); } else { this.logger.log("WebSerial: Preparing port for Wi-Fi setup..."); } await this._releaseReaderWriter(); await sleep(500); } this.logger.log("Port ready for new Improv client"); // CRITICAL: Recreate streams one more time to flush any buffered firmware output // Firmware debug messages can interfere with Improv protocol this.logger.log("Flushing serial buffer before Improv init..."); await this._releaseReaderWriter(); await sleep(100); // Re-create Improv client (firmware is running at 115200 baud) const client = new ImprovSerial(this._port, this.logger); client.addEventListener("state-changed", () => { this.requestUpdate(); }); client.addEventListener("error-changed", () => this.requestUpdate()); try { // Use 10 second timeout to allow device to get IP address this._info = await client.initialize(10000); this._client = client; client.addEventListener("disconnect", this._handleDisconnect); this.logger.log("Improv client ready for Wi-Fi provisioning"); } catch (improvErr) { try { await this._closeClientWithoutEvents(client); } catch (closeErr) { this.logger.log("Failed to close Improv client after init error:", closeErr); } // CRITICAL: Recreate streams after failed Improv init try { await this._releaseReaderWriter(); this.logger.log("Streams recreated after Improv failure"); } catch (releaseErr) { this.logger.log(`Failed to recreate streams: ${releaseErr.message}`); } this.logger.log(`Improv initialization failed: ${improvErr.message}`); this._error = `Improv initialization failed: ${improvErr.message}`; this._state = "ERROR"; this._busy = false; return; } this._state = "PROVISION"; this._provisionForce = true; this._busy = false; }} > ${listItemWifi} <div slot="headline"> ${this._client.state === ImprovSerialCurrentState.READY ? "Connect to Wi-Fi" : "Change Wi-Fi"} </div> </ew-list-item> ` : ""} ${this._isUsbJtagOrOtgDevice ? html ` <ew-list-item type="button" ?disabled=${this._busy} @click=${async () => { this._busy = true; // Close Improv client if active if (this._client) { try { await this._closeClientWithoutEvents(this._client); } catch (e) { this.logger.log("Failed to close Improv client:", e); } } // Switch to firmware mode if needed const needsReconnect = await this._switchToFirmwareMode("console"); if (needsReconnect) { return; // Will continue after port reconnection } // Device is already in firmware mode this.logger.log("Opening console for USB-JTAG/OTG device (in firmware mode)"); this._state = "LOGS"; this._busy = false; }} > ${listItemConsole} <div slot="headline">Open Console</div> </ew-list-item> ` : ""} ${!this._isUsbJtagOrOtgDevice ? html ` <ew-list-item type="button" ?disabled=${this._busy} @click=${async () => { const client = this._client; if (client) { await this._closeClientWithoutEvents(client); } // switch to Firmware mode for Console await this._switchToFirmwareMode("console"); this._state = "LOGS"; }} > ${listItemConsole} <div slot="headline">Logs &amp; Console</div> </ew-list-item> ` : ""} <ew-list-item type="button" ?disabled=${this._busy} @click=${async () => { // Filesystem management requires bootloader mode // Close Improv client if active (it locks the reader) if (this._client) { try { await this._closeClientWithoutEvents(this._client); } catch (e) { this.logger.log("Failed to close Improv client:", e); } } // Switch to bootloader mode for filesystem operations this.logger.log("Preparing device for filesystem operations (switching to bootloader mode)..."); try { await this._prepareForFlashOperations(); await this._ensureStub(); } catch (err) { this.logger.log(`Failed to prepare for filesystem: ${err.message}`); this._state = "ERROR"; this._error = `Failed to enter bootloader mode: ${err.message}`; return; } this._state = "PARTITIONS"; this._readPartitionTable(); }} > ${firmwareIcon} <div slot="headline">Manage Filesystem</div> </ew-list-item> ${this._isSameFirmware && this._manifest.funding_url ? html ` <ew-list-item type="link" href=${this._manifest.funding_url} target="_blank" > ${listItemFundDevelopment} <div slot="headline">Fund Development</div> </ew-list-item> ` : ""} ${this._isSameVersion ? html ` <ew-list-item type="button" class="danger" ?disabled=${this._busy} @click=${() => this._startInstall(true)} > ${listItemEraseUserData} <div slot="headline">Erase User Data</div> </ew-list-item> ` : ""} </ew-list> `; return [heading, content, hideActions, allowClosing]; } _renderDashboardNoImprov() { const heading = "Device Dashboard"; const hideActions = true; const allowClosing = true; // Build device info string if available const chipFamily = this.esploader.chipFamily ? getChipFamilyName(this.esploader) : null; const deviceInfo = chipFamily ? `(${chipFamily}${this._flashSize ? `, ${this._flashSize}` : ""})` : null; const content = html ` <ew-list> ${deviceInfo ? html ` <ew-list-item> <div slot="headline">${chipFamily}</div> <div slot="supporting-text">${deviceInfo}</div> </ew-list-item> ` : ""} <ew-list-item type="button" ?disabled=${this._busy} @click=${() => { if (this._manifest.new_install_prompt_erase) { this._state = "ASK_ERASE"; } else { // Default is to erase a device that does not support Improv Serial this._startInstall(true); } }} > ${listItemInstallIcon} <div slot="headline">Install ${this._manifest.name}</div> ${(() => { const label = this._detectedBuild ? this._buildVariantLabel(this._detectedBuild) : undefined; return label ? html `<div slot="supporting-text">Variant: ${label}</div>` : ""; })()} </ew-list-item> ${!this._isUsbJtagOrOtgDevice ? html ` <ew-list-item type="button" ?disabled=${this._busy} @click=${async () => { this._busy = true; const client = this._client; if (client) { await this._closeClientWithoutEvents(client); } // switch to Firmware mode for Console const needsReconnect = await this._switchToFirmwareMode("console"); if (needsReconnect) { return; // Will continue after port reconnection } this._state = "LOGS"; this._busy = false; }} > ${listItemConsole} <div slot="headline">Logs &amp; Console</div> </ew-list-item> ` : ""} ${this._isUsbJtagOrOtgDevice ? html ` <ew-list-item type="button" ?disabled=${this._busy} @click=${async () => { this._busy = true; // Close Improv client if active if (this._client) { try { await this._closeClientWithoutEvents(this._client); } catch (e) { this.logger.log("Failed to close Improv client:", e); } } // Switch to firmware mode if needed const needsReconnect = await this._switchToFirmwareMode("console"); if (needsReconnect) { return; // Will continue after port reconnection } // Device is already in firmware mode this.logger.log("Opening console for USB-JTAG/OTG device (in firmware mode)"); this._state = "LOGS"; this._busy = false; }} > ${listItemConsole} <div slot="headline">Open Console</div> </ew-list-item> ` : ""} <ew-list-item type="button" ?disabled=${this._busy} @click=${async () => { // Filesystem management requires bootloader mode // Close Improv client if active (it locks the reader) if (this._client) { try { await this._closeClientWithoutEvents(this._client); } catch (e) { this.logger.log("Failed to close Improv client:", e); } // Keep client object for dashboard rendering; connection already closed above. } // Switch to bootloader mode for filesystem operations this.logger.log("Preparing device for filesystem operations (switching to bootloader mode)..."); try { await this._prepareForFlashOperations(); await this._ensureStub(); } catch (err) { this.logger.log(`Failed to prepare for filesystem: ${err.message}`); this._state = "ERROR"; this._error = `Failed to enter bootloader mode: ${err.message}`; return; } this._state = "PARTITIONS"; this._readPartitionTable(); }} > ${firmwareIcon} <div slot="headline">Manage Filesystem</div> </ew-list-item> </ew-list> `; return [heading, content, hideActions, allowClosing]; } _renderProvision() { let heading = "Configure Wi-Fi"; let content; let hideActions = false; if (this._busy) { return [ heading, this._renderProgress(this._ssids === undefined ? "Scanning for networks" : "Trying to connect"), true, ]; } if (!this._provisionForce && this._client.state === ImprovSerialCurrentState.PROVISIONED) { heading = undefined; const showSetupLinks = !this._wasProvisioned && (this._client.nextUrl !== undefined || "home_assistant_domain" in this._manifest); hideActions = showSetupLinks; content = html ` <ewt-page-message .icon=${OK_ICON} label="Device connected to the network!" ></ewt-page-message> ${showSetupLinks ? html ` <div class="dashboard-buttons"> ${this._client.nextUrl === undefined ? "" : html ` <div> <a href=${this._client.nextUrl} class="has-button" target="_blank" @click=${async (ev) => { ev.preventDefault(); const url = this._client.nextUrl; // Preserve user gesture for popup blockers const popup = window.open("about:blank", "_blank"); // Visit Device opens external page - firmware must running // Check if device is in bootloader mode // Switch to firmware mode if needed const needsReconnect = await this._switchToFirmwareMode("visit"); if (needsReconnect) { popup === null || popup === void 0 ? void 0 : popup.close(); return; // Will continue after port reconnection } // Device is already in firmware mode this.logger.log("Following Link (in firmware mode)"); if (popup) { popup.location.href = url; } else { window.open(url, "_blank", "noopener,noreferrer"); } this._state = "DASHBOARD"; }} > <ew-text-button>Visit Device</ew-text-button> </a> </div> `} ${!this._manifest.home_assistant_domain ? "" : html ` <div> <a href=${`https://my.home-assistant.io/redirect/config_flow_start/?domain=${this._manifest.home_assistant_domain}`} class="has-button" target="_blank" @click=${async (ev) => { ev.preventDefault(); const url = `https://my.home-assistant.io/redirect/config_flow_start/?domain=${this._manifest.home_assistant_domain}`; const popup = window.open("about:blank", "_blank"); // Add to HA opens external page - firmware must running // Check if device is in bootloader mode // Switch to firmware mode if needed const needsReconnect = await this._switchToFirmwareMode("homeassistant"); if (needsReconnect) { popup === null || popup === void 0 ? void 0 : popup.close(); return; // Will continue after port reconnection } // Device is already in firmware mode this.logger.log("Following Link (in firmware mode)"); if (popup) { popup.location.href = url; } else { window.open(url, "_blank", "noopener,noreferrer"); } this._state = "DASHBOARD"; }} > <ew-text-button>Add to Home Assistant</ew-text-button> </a> </div> `} <div> <ew-text-button @click=${async () => { // After WiFi provisioning: Device stays in firmware mode // Close Improv client first if (this._client) { try { await this._closeClientWithoutEvents(this._client); this.logger.log("Improv client closed after provisioning"); } catch (e) { this.logger.log("Failed to close Improv client:", e); } } // Release locks and stay in firmware mode await this._releaseReaderWriter(); this.logger.log("Returning to dashboard (device stays in firmware mode)"); this._state = "DASHBOARD"; }} >Skip</ew-text-button > </div> </div> ` : html ` <ew-text-button slot="actions" @click=${async () => { // After WiFi provisioning: Device stays in firmware mode // Close Improv client first if (this._client) { try { await this._closeClientWithoutEvents(this._client); this.logger.log("Improv client closed after provisioning"); } catch (e) { this.logger.log("Failed to close Improv client:", e); } } // Release locks and stay in firmware mode await this._releaseReaderWriter(); this.logger.log("Returning to dashboard (device stays in firmware mode)"); this._state = "DASHBOARD"; }} >Continue</ew-text-button > `} `; } else { let error; switch (this._client.error) { case 3 /* ImprovSerialErrorState.UNABLE_TO_CONNECT */: error = "Unable to connect"; break; // UNKNOWN_RPC_COMMAND happens when list SSIDs not supported. case 0 /* ImprovSerialErrorState.NO_ERROR */: case 2 /* ImprovSerialErrorState.UNKNOWN_RPC_COMMAND */