UNPKG

tasmota-esp-web-tools

Version:
393 lines (392 loc) 15.1 kB
import { getChipFamilyName } from "./util/chip-family-name"; import { sleep } from "./util/sleep"; import { corsProxyFetch } from "./util/cors-proxy"; /** * Parse flash size string (e.g., "4MB", "8MB", "16MB") to megabytes number */ export function parseFlashSizeToMB(flashSize) { if (!flashSize) return undefined; const match = flashSize.match(/^(\d+)(MB|GB)$/); if (!match) return undefined; const size = parseInt(match[1], 10); const unit = match[2]; if (unit === "GB") return size * 1024; return size; } /** * Select the best build using most-specific-matching algorithm * - Builds with matching usbInterface are strongly preferred * - Builds with matching flashSizeMB are preferred * - Among builds with same specificity, first one wins * - Builds without these qualifiers are fallback options */ function selectBestBuild(builds, detectedFlashSizeMB, detectedUsbInterface) { if (builds.length === 0) return undefined; // Score builds: higher score = more specific match let bestBuild; let bestScore = -Infinity; for (const build of builds) { let score = 0; // USB interface match - if specified, must match if (build.usbInterface !== undefined) { if (detectedUsbInterface !== undefined && build.usbInterface === detectedUsbInterface) { score += 1000; // Strong preference for explicit usbInterface match } else { // Mismatched usbInterface - disqualify this build continue; } } // Builds without usbInterface stay neutral (compatible with any) // Flash size match gives second priority if (build.flashSizeMB !== undefined && detectedFlashSizeMB !== undefined) { if (build.flashSizeMB === detectedFlashSizeMB) { score += 100; // Exact flash size match } else { score -= 1; // Penalize non-matching specific builds } } else if (build.flashSizeMB !== undefined) { // flashSizeMB is defined but detectedFlashSizeMB is undefined score -= 1; // Penalize non-matching specific builds } // Generic builds (flashSizeMB undefined) stay at score 0 // Prefer this build if it has higher score // If same score, keep the first one (stable selection) if (score > bestScore) { bestScore = score; bestBuild = build; } } return bestScore >= 0 ? bestBuild : undefined; } /** * Extract the most relevant firmware filename from a build's parts. * Prefers parts with "factory" in the name. * Ignores parts with "bootloader" or "partition" in the name. * Returns just the basename (no path prefix). */ export function getFirmwareFileName(build) { const candidates = build.parts .map((p) => p.path) .filter((path) => { const lower = path.toLowerCase(); return !lower.includes("bootloader") && !lower.includes("partition"); }); if (candidates.length === 0) return undefined; const factory = candidates.find((p) => p.toLowerCase().includes("factory")); const chosen = factory !== null && factory !== void 0 ? factory : candidates[0]; // Return only the basename return chosen.split("/").pop().split("\\").pop(); } /** * Find the best matching build for a given chip configuration. * This is the single source of truth for build selection used by both * the flash routine and the install dialog UI. */ export function findMatchingBuild(manifest, chipFamily, chipVariant, flashSizeMB, usbInterface) { const compatible = manifest.builds.filter((b) => { if (b.chipFamily !== chipFamily) return false; if (b.chipVariant && b.chipVariant !== chipVariant) return false; return true; }); const exactVariant = compatible.filter((b) => b.chipVariant !== undefined && b.chipVariant === chipVariant); const variantAgnostic = compatible.filter((b) => b.chipVariant === undefined); return (selectBestBuild(exactVariant, flashSizeMB, usbInterface) || selectBestBuild(variantAgnostic, flashSizeMB, usbInterface)); } /** * Detect the matching build for the currently connected device. * Extracts chip info directly from the esploader instance. * * @param manifest - The loaded firmware manifest * @param esploader - ESPLoader instance (or stub) with chip info * @param flashSize - Detected flash size string (e.g. "4MB"), or undefined * @param isUsbJtagOrOtg - Whether the device uses native USB (CDC) instead of external serial * @param improvChipFamily - Optional chipFamily from Improv Serial info (takes precedence) */ export function detectMatchingBuild(manifest, esploader, flashSize, isUsbJtagOrOtg, improvChipFamily) { var _a; const chipFamily = improvChipFamily || (esploader.chipFamily ? getChipFamilyName(esploader) : null); if (!chipFamily) return undefined; const chipVariant = (_a = esploader.chipVariant) !== null && _a !== void 0 ? _a : null; const flashSizeMB = flashSize ? parseFlashSizeToMB(flashSize) : undefined; const usbInterface = isUsbJtagOrOtg ? "CDC" : "UART"; return findMatchingBuild(manifest, chipFamily, chipVariant, flashSizeMB, usbInterface); } export const flash = async (onEvent, esploader, // ESPLoader instance from tasmota-webserial-esptool logger, manifestPath, eraseFirst, firmwareBuffer, _baudRate) => { let manifest; // eslint-disable-next-line prefer-const let build; // eslint-disable-next-line prefer-const let chipFamily; let chipVariant = null; // eslint-disable-next-line prefer-const let flashSize; const fireStateEvent = (stateUpdate) => onEvent({ ...stateUpdate, manifest, build, chipFamily, chipVariant, flashSize, }); let manifestProm = null; let manifestURL = ""; try { manifestProm = JSON.parse(manifestPath); } catch { manifestURL = new URL(manifestPath, location.toString()).toString(); manifestProm = corsProxyFetch(manifestURL).then((resp) => resp.json()); } // Use the provided ESPLoader instance - NO port logic here! // For debugging window.esploader = esploader; fireStateEvent({ state: "initializing" /* FlashStateType.INITIALIZING */, message: "Initializing...", details: { done: false }, }); // Only initialize if not already done if (!esploader.chipFamily) { try { await esploader.initialize(); } catch (err) { logger.error(err); fireStateEvent({ state: "error" /* FlashStateType.ERROR */, message: "Failed to initialize. Try resetting your device or holding the BOOT button while clicking INSTALL.", details: { error: "failed_initialize" /* FlashError.FAILED_INITIALIZING */, details: err }, }); if (esploader.connected) { await esploader.disconnect(); } return; } } chipFamily = getChipFamilyName(esploader); chipVariant = esploader.chipVariant; // Detect flash size if not already detected if (!esploader.flashSize && esploader.detectFlashSize) { try { await esploader.detectFlashSize(); } catch (err) { logger.debug("Failed to detect flash size:", err); } } flashSize = esploader.flashSize; // e.g., "4MB", "8MB" const flashSizeMB = flashSize ? parseFlashSizeToMB(flashSize) : undefined; // Detect USB connection type to pick CDC vs UART firmware variants // - true: native USB (USB-JTAG/USB-OTG) -> CDC // - false: external USB-to-Serial bridge -> UART let detectedUsbInterface; if (typeof esploader.detectUsbConnectionType === "function") { try { const isUsbJtagOrOtg = await esploader.detectUsbConnectionType(); detectedUsbInterface = isUsbJtagOrOtg ? "CDC" : "UART"; logger.debug(`Detected USB interface: ${detectedUsbInterface}`); } catch (err) { logger.debug("Failed to detect USB connection type:", err); } } fireStateEvent({ state: "initializing" /* FlashStateType.INITIALIZING */, message: `Initialized. Found ${chipFamily}${chipVariant ? ` (${chipVariant})` : ""}${flashSize ? `, ${flashSize}` : ""}`, details: { done: true }, }); fireStateEvent({ state: "manifest" /* FlashStateType.MANIFEST */, message: "Fetching manifest...", details: { done: false }, }); try { manifest = await manifestProm; } catch (err) { fireStateEvent({ state: "error" /* FlashStateType.ERROR */, message: `Unable to fetch manifest: ${err}`, details: { error: "fetch_manifest_failed" /* FlashError.FAILED_MANIFEST_FETCH */, details: err }, }); await esploader.disconnect(); return; } build = findMatchingBuild(manifest, chipFamily, chipVariant, flashSizeMB, detectedUsbInterface); if (!build) { fireStateEvent({ state: "error" /* FlashStateType.ERROR */, message: `Your ${chipFamily}${chipVariant ? ` (${chipVariant})` : ""} is not supported by this firmware.`, details: { error: "not_supported" /* FlashError.NOT_SUPPORTED */, details: chipFamily }, }); await esploader.disconnect(); return; } fireStateEvent({ state: "manifest" /* FlashStateType.MANIFEST */, message: "Manifest fetched", details: { done: true }, }); fireStateEvent({ state: "preparing" /* FlashStateType.PREPARING */, message: "Preparing installation...", details: { done: false }, }); // The esploader passed in is always a stub (from _ensureStub()) // Baudrate was already set in _ensureStub() const espStub = esploader; // Verify stub has chipFamily (should be copied in _ensureStub) if (!espStub.chipFamily) { logger.error("Stub missing chipFamily - this should not happen!"); fireStateEvent({ state: "error" /* FlashStateType.ERROR */, message: "Internal error: Stub not properly initialized", details: { error: "failed_initialize" /* FlashError.FAILED_INITIALIZING */, details: "Missing chipFamily", }, }); return; } // Fetch firmware files const filePromises = build.parts.map(async (part) => { const url = new URL(part.path, manifestURL || location.toString()).toString(); const resp = await corsProxyFetch(url); if (!resp.ok) { throw new Error(`Downlading firmware ${part.path} failed: ${resp.status}`); } return resp.arrayBuffer(); }); // If firmwareBuffer is provided, use it instead of fetching if (firmwareBuffer) { filePromises.push(Promise.resolve(firmwareBuffer.buffer)); } const files = []; let totalSize = 0; for (const prom of filePromises) { try { const data = await prom; files.push(data); totalSize += data.byteLength; } catch (err) { fireStateEvent({ state: "error" /* FlashStateType.ERROR */, message: err.message, details: { error: "failed_firmware_download" /* FlashError.FAILED_FIRMWARE_DOWNLOAD */, details: err }, }); await esploader.disconnect(); return; } } fireStateEvent({ state: "preparing" /* FlashStateType.PREPARING */, message: "Installation prepared", details: { done: true }, }); // CRITICAL: Erase MUST be done BEFORE writing, if requested if (eraseFirst) { fireStateEvent({ state: "erasing" /* FlashStateType.ERASING */, message: "Erasing flash...", details: { done: false }, }); try { logger.log("Erasing flash memory. Please wait..."); await espStub.eraseFlash(); logger.log("Flash erased successfully"); fireStateEvent({ state: "erasing" /* FlashStateType.ERASING */, message: "Flash erased", details: { done: true }, }); } catch (err) { logger.error(`Flash erase failed: ${err.message}`); fireStateEvent({ state: "error" /* FlashStateType.ERROR */, message: `Failed to erase flash: ${err.message}`, details: { error: "write_failed" /* FlashError.WRITE_FAILED */, details: err }, }); await esploader.disconnect(); return; } } fireStateEvent({ state: "writing" /* FlashStateType.WRITING */, message: `Writing progress: 0 %`, details: { bytesTotal: totalSize, bytesWritten: 0, percentage: 0, }, }); let lastPct = 0; let totalBytesWritten = 0; try { for (let i = 0; i < build.parts.length; i++) { const part = build.parts[i]; const data = files[i]; await espStub.flashData(data, (bytesWritten, _bytesTotal) => { const newPct = Math.floor(((totalBytesWritten + bytesWritten) / totalSize) * 100); if (newPct === lastPct) { return; } lastPct = newPct; fireStateEvent({ state: "writing" /* FlashStateType.WRITING */, message: `Writing progress: ${newPct} %`, details: { bytesTotal: totalSize, bytesWritten: totalBytesWritten + bytesWritten, percentage: newPct, }, }); }, part.offset); totalBytesWritten += data.byteLength; } } catch (err) { fireStateEvent({ state: "error" /* FlashStateType.ERROR */, message: err.message, details: { error: "write_failed" /* FlashError.WRITE_FAILED */, details: err }, }); await esploader.disconnect(); return; } fireStateEvent({ state: "writing" /* FlashStateType.WRITING */, message: "Writing complete", details: { bytesTotal: totalSize, bytesWritten: totalSize, percentage: 100, }, }); await sleep(100); // DON'T release locks after flash! // Keep the stub and locks so the port can be used again // (e.g., for Improv, Manage Filesystem, or another flash) fireStateEvent({ state: "finished" /* FlashStateType.FINISHED */, message: "All done!", }); };