tasmota-esp-web-tools
Version:
Web tools for ESP devices
1,155 lines (1,153 loc) • 120 kB
JavaScript
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} ${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 & 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 & 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 */