import { useState, useCallback, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useEditorStore } from '../../store/useEditorStore'; import { useSimulatorStore } from '../../store/useSimulatorStore'; import { useElectricalStore } from '../../store/useElectricalStore'; import { verifyCircuit, type VerificationResult } from '../../simulation/verify/circuitVerifier'; import { buildInputFromStore } from '../../simulation/spice/storeAdapter'; import { BOARD_PIN_GROUPS } from '../../simulation/spice/boardPinGroups'; import { CircuitVerificationModal } from '../simulator/CircuitVerificationModal'; import type { PinSourceState } from '../../simulation/spice/types'; import type { BoardKind, LanguageMode } from '../../types/board'; import { BOARD_KIND_FQBN, BOARD_KIND_LABELS, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind } from '../../types/board'; import { compileCode } from '../../services/compilation'; import { compileRom, isChipProgramFile, formatForFile, targetForChip, } from '../../services/romCompileService'; import { reportRunEvent } from '../../services/metricsService'; import { useProjectStore } from '../../store/useProjectStore'; import { LibraryManagerModal } from '../simulator/LibraryManagerModal'; import { InstallLibrariesModal } from '../simulator/InstallLibrariesModal'; import { parseCompileResult } from '../../utils/compilationLogger'; import type { CompilationLog } from '../../utils/compilationLogger'; import { exportToWokwiZip } from '../../utils/wokwiZip'; import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject'; import { readFirmwareFile } from '../../utils/firmwareLoader'; import { trackCompileCode, trackRunSimulation, trackStopSimulation, trackResetSimulation, trackOpenLibraryManager, } from '../../utils/analytics'; import './EditorToolbar.css'; interface EditorToolbarProps { consoleOpen: boolean; setConsoleOpen: (open: boolean | ((v: boolean) => boolean)) => void; compileLogs: CompilationLog[]; setCompileLogs: (logs: CompilationLog[] | ((prev: CompilationLog[]) => CompilationLog[])) => void; /** * Optional element rendered between the left action group and the right * action group. The editor passes here so the tabs share the * same row as the toolbar — keeping every action icon pinned and visible * regardless of how narrow the editor pane gets. */ centerSlot?: React.ReactNode; /** * Optional extra elements rendered after the built-in right-group buttons * (Libraries / Import-Export / Output Console). Used by private overlays * to add deployment-specific actions without forking the toolbar. */ rightSlot?: React.ReactNode; } const BOARD_PILL_ICON: Record = { 'arduino-uno': '⬤', 'arduino-nano': '▪', 'arduino-mega': '▬', 'raspberry-pi-pico': '◆', 'raspberry-pi-3': '⬛', 'raspberry-pi-4': '⬛', 'raspberry-pi-5': '⬛', esp32: '⬡', 'esp32-s3': '⬡', 'esp32-c3': '⬡', }; const BOARD_PILL_COLOR: Record = { 'arduino-uno': '#4fc3f7', 'arduino-nano': '#4fc3f7', 'arduino-mega': '#4fc3f7', 'raspberry-pi-pico': '#ce93d8', 'raspberry-pi-3': '#ef9a9a', 'raspberry-pi-4': '#ef9a9a', 'raspberry-pi-5': '#ef9a9a', esp32: '#a5d6a7', 'esp32-s3': '#a5d6a7', 'esp32-c3': '#a5d6a7', }; export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compileLogs, setCompileLogs, centerSlot, rightSlot, }: EditorToolbarProps) => { const { t } = useTranslation(); const { files, codeChangedSinceLastCompile, markCompiled } = useEditorStore(); const { boards, activeBoardId, compileBoardProgram, loadMicroPythonProgram, setBoardLanguageMode, updateBoard, startBoard, stopBoard, resetBoard, // legacy compat startSimulation, stopSimulation, resetSimulation, running, compiledHex, } = useSimulatorStore(); const activeBoard = boards.find((b) => b.id === activeBoardId) ?? boards[0]; const currentProject = useProjectStore((s) => s.currentProject); // Board-less mode: digital / analog SPICE-only circuits. The Run / Stop // buttons toggle the SPICE solver's `paused` flag — pausing freezes every // LED at its current brightness so the user can inspect the state, and // resuming flushes the most recent switch toggle through the engine. const electricalPaused = useElectricalStore((s) => s.paused); const setElectricalPaused = useElectricalStore((s) => s.setPaused); const isBoardless = boards.length === 0; const digitalRunning = isBoardless && !electricalPaused; // Circuit-verification modal state. When `pendingRun` is non-null we've // already paid the cost of solving + analysing — the user can either // bail out or proceed by running `pendingRun()`. const [verification, setVerification] = useState(null); const pendingRunRef = useRef<(() => void) | null>(null); // Helper: report a Run event to the backend for analytics. Resolves the // FQBN from the board kind so the backend can group by family/fqbn. const reportRun = useCallback( (boardKind: BoardKind | undefined) => { const fqbn = boardKind ? BOARD_KIND_FQBN[boardKind] : null; void reportRunEvent({ project_id: currentProject?.id ?? null, board_fqbn: fqbn ?? null, }); }, [currentProject], ); const [compiling, setCompiling] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [libManagerOpen, setLibManagerOpen] = useState(false); const [pendingLibraries, setPendingLibraries] = useState([]); const [installModalOpen, setInstallModalOpen] = useState(false); const importInputRef = useRef(null); const firmwareInputRef = useRef(null); const toolbarRef = useRef(null); const [missingLibHint, setMissingLibHint] = useState(false); // Compile All / Run All — runs sequentially, logs to console (no dialog) const [compileAllRunning, setCompileAllRunning] = useState(false); const addLog = useCallback( (log: CompilationLog) => { setCompileLogs((prev: CompilationLog[]) => [...prev, log]); }, [setCompileLogs], ); const handleCompile = async () => { setCompiling(true); setMessage(null); setConsoleOpen(true); trackCompileCode(); // ── Chip-program path ─────────────────────────────────────────────── // If the editor's active file is a chip-program file we don't compile // Arduino code — we assemble/compile it into ROM bytes via // /api/compile-rom and stash the result on every custom-chip component // that points at this filename through its `programFile` property. The // chip's emulator then reads the bytes on chip_setup via vx_rom_size / // vx_rom_read. // // A file is "chip program" when EITHER its extension is unambiguous // (.s/.asm/.hex/.bin) OR some custom-chip on the canvas has // programFile === activeFile.name. The latter lets .c files route to // SDCC instead of arduino-cli when wired to a CPU chip. const activeFile = files.find((f) => f.id === useEditorStore.getState().activeFileId); const componentsForCompile = useSimulatorStore.getState().components; const chipsBoundToFile = activeFile ? componentsForCompile.filter((c) => { if (c.metadataId !== 'custom-chip') return false; const prog = String((c.properties as any)?.programFile ?? '').trim(); return prog === activeFile.name; }) : []; if (activeFile && (isChipProgramFile(activeFile.name) || chipsBoundToFile.length > 0)) { try { const chips = chipsBoundToFile.length > 0 ? chipsBoundToFile : componentsForCompile.filter((c) => { if (c.metadataId !== 'custom-chip') return false; const prog = String((c.properties as any)?.programFile ?? '').trim(); return prog === '' || prog === activeFile.name; }); if (chips.length === 0) { addLog({ timestamp: new Date(), type: 'error', message: `No custom-chip on the canvas references ${activeFile.name}. Drop an "i8080 CPU" chip, or set its programFile property.`, }); setMessage({ type: 'error', text: 'No matching custom-chip on canvas' }); setCompiling(false); return; } // Resolve target from the first matching chip's chip.json. const firstChipJson = String((chips[0].properties as any)?.chipJson ?? '{}'); const target = targetForChip(firstChipJson); const fmt = formatForFile(activeFile.name); addLog({ timestamp: new Date(), type: 'info', message: `Assembling ${activeFile.name} (target=${target}, format=${fmt}) for ${chips.length} chip(s)...`, }); const result = await compileRom(activeFile.content, target, fmt); if (!result.success || !result.rom_base64) { addLog({ timestamp: new Date(), type: 'error', message: result.error || 'ROM compile failed', }); if (result.stderr) { addLog({ timestamp: new Date(), type: 'error', message: result.stderr }); } setMessage({ type: 'error', text: result.error || 'ROM compile failed' }); setCompiling(false); return; } // Inject into every matching chip's romBytes property. const updateComponent = useSimulatorStore.getState().updateComponent; for (const chip of chips) { updateComponent(chip.id, { properties: { ...(chip.properties as Record), romBytes: result.rom_base64, programFile: activeFile.name, }, }); } addLog({ timestamp: new Date(), type: 'success', message: `ROM compiled: ${result.byte_size} bytes injected into ${chips.length} chip(s).`, }); setMessage({ type: 'success', text: `ROM ready (${result.byte_size} B). Hit Run.`, }); } catch (e) { const errMsg = e instanceof Error ? e.message : String(e); addLog({ timestamp: new Date(), type: 'error', message: errMsg }); setMessage({ type: 'error', text: errMsg }); } finally { setCompiling(false); } return; } // ── End chip-program path ─────────────────────────────────────────── const kind = activeBoard?.boardKind; // Raspberry Pi 3B doesn't need arduino-cli compilation if (isPiBoardKind(kind)) { addLog({ timestamp: new Date(), type: 'info', message: 'Raspberry Pi 3B: no compilation needed — run Python scripts directly.', }); setMessage({ type: 'success', text: 'Ready (no compilation needed)' }); setCompiling(false); return; } // MicroPython mode — no backend compilation needed if (activeBoard?.languageMode === 'micropython' && activeBoardId) { addLog({ timestamp: new Date(), type: 'info', message: 'MicroPython: loading firmware and user files...', }); try { const groupFiles = useEditorStore.getState().getGroupFiles(activeBoard.activeFileGroupId); const pyFiles = groupFiles.map((f) => ({ name: f.name, content: f.content })); await loadMicroPythonProgram(activeBoardId, pyFiles); addLog({ timestamp: new Date(), type: 'success', message: 'MicroPython firmware loaded successfully', }); setMessage({ type: 'success', text: 'MicroPython ready' }); } catch (err) { const errMsg = err instanceof Error ? err.message : 'Failed to load MicroPython'; addLog({ timestamp: new Date(), type: 'error', message: errMsg }); setMessage({ type: 'error', text: errMsg }); } finally { setCompiling(false); } return; } const fqbn = kind ? BOARD_KIND_FQBN[kind] : null; const boardLabel = kind ? BOARD_KIND_LABELS[kind] : 'Unknown'; if (!fqbn) { addLog({ timestamp: new Date(), type: 'error', message: `No FQBN for board kind: ${kind}` }); setMessage({ type: 'error', text: 'Unknown board' }); setCompiling(false); return; } addLog({ timestamp: new Date(), type: 'info', message: `Starting compilation for ${boardLabel} (${fqbn})...`, }); try { const groupFiles = activeBoard?.activeFileGroupId ? useEditorStore.getState().getGroupFiles(activeBoard.activeFileGroupId) : files; const sketchFiles = (groupFiles.length > 0 ? groupFiles : files).map((f) => ({ name: f.name, content: f.content, })); // Stream live cmake + ninja output into the compilation console as // it arrives, instead of waiting for the whole build to finish. // Each poll the backend returns the cumulative stdout buffer; we // append only the delta since the previous call as 'info' lines. let lastStreamedLen = 0; const result = await compileCode( sketchFiles, fqbn, currentProject?.id ?? null, ({ stdout }) => { if (stdout.length <= lastStreamedLen) return; const delta = stdout.slice(lastStreamedLen); lastStreamedLen = stdout.length; const newLines = delta.split('\n').filter((s) => s.trim()); if (!newLines.length) return; const now = new Date(); setCompileLogs((prev: CompilationLog[]) => [ ...prev, ...newLines.map((line) => ({ timestamp: now, type: 'info' as const, message: line, })), ]); }, // Per-board ESP32 build options + SPIFFS uploads. Undefined for AVR // / RP2040 boards (ignored on those paths by the backend). { boardOptions: activeBoard?.boardOptions, spiffsFiles: activeBoard?.spiffsFiles, }, ); // After the build settles, append the structured analysis on top of // the live stream — parseCompileResult highlights FAILED blocks and // tags compiler errors with type='error', which the console uses for // colour + the auto-switch-to-errors filter. const resultLogs = parseCompileResult(result, boardLabel); setCompileLogs((prev: CompilationLog[]) => [...prev, ...resultLogs]); if (result.success) { const program = result.hex_content ?? result.binary_content ?? null; if (program && activeBoardId) { compileBoardProgram(activeBoardId, program); if (result.has_wifi !== undefined) { updateBoard(activeBoardId, { hasWifi: result.has_wifi }); } } setMessage({ type: 'success', text: 'Compiled successfully' }); markCompiled(); setMissingLibHint(false); } else { const errText = result.error || result.stderr || 'Compile failed'; setMessage({ type: 'error', text: errText }); // Detect missing library errors — common patterns: // "No such file or directory" for #include, "fatal error: XXX.h" const looksLikeMissingLib = /No such file or directory|fatal error:.*\.h|library not found/i.test(errText); setMissingLibHint(looksLikeMissingLib); } } catch (err) { const errMsg = err instanceof Error ? err.message : 'Compile failed'; addLog({ timestamp: new Date(), type: 'error', message: errMsg }); setMessage({ type: 'error', text: errMsg }); } finally { setCompiling(false); } }; // Track whether we should auto-run after compilation completes const autoRunAfterCompile = useRef(false); /** * Pre-flight safety check: solves the current circuit and flags shorts, * LED over-current and resistor over-power. Returns the result. When the * solver fails to converge (degenerate netlist, no power source, …) we * silently report a clean result so the user isn't blocked on circuits * that aren't physically meaningful yet. */ const runVerification = useCallback(async (): Promise => { try { const sim = useSimulatorStore.getState(); // Skip if the circuit hasn't got anything analysable on it yet. const hasSource = sim.components.some( (c) => c.metadataId.startsWith('signal-generator') || c.metadataId.startsWith('battery'), ); if (!hasSource && sim.boards.length === 0) return null; const snap = { components: sim.components.map((c) => ({ id: c.id, metadataId: c.metadataId, properties: c.properties, })), wires: sim.wires, boards: sim.boards.map((b) => { // Realistic pre-flight: simulate the WORST CASE — every digital // pin connected to a load is forced HIGH at the board's vcc. // This is what we want because the user's sketch WILL eventually // do `digitalWrite(pin, HIGH)` (otherwise why is the LED wired?). // Testing idle state would never flag a missing series resistor // because the LED draws zero current when its pin is LOW. // // Caveat: pins wired only to inputs (e.g. a pull-up resistor + // button) get over-driven here too. The verifier rules are // already tolerant — a properly-spec'd pull-up sees minimal // current and doesn't trip overcurrent / overpower. A circuit // that would actually fault under HIGH is flagged correctly. const pinStates: Record = {}; const group = BOARD_PIN_GROUPS[b.boardKind] ?? BOARD_PIN_GROUPS.default; const wiredPinNames = new Set(); for (const w of sim.wires) { if (w.start.componentId === b.id) wiredPinNames.add(w.start.pinName); if (w.end.componentId === b.id) wiredPinNames.add(w.end.pinName); } for (const pinName of wiredPinNames) { // Skip GND / power-rail pin names — they belong to the rail // groups and don't need to be re-asserted as digital sources. if (group.gnd.includes(pinName)) continue; if (group.vcc_pins.includes(pinName)) continue; const arduinoPin = Number.parseInt(pinName, 10); // Skip pins we can't identify as a digital GPIO (e.g. // 'AREF', 'RESET', 'TX', 'RX' on some boards). Those are // either rail-ish or non-driven by the sketch. if (Number.isNaN(arduinoPin)) continue; pinStates[pinName] = { type: 'digital', v: group.vcc }; } return { id: b.id, boardKind: b.boardKind, pinStates }; }), }; const input = buildInputFromStore(snap); return await verifyCircuit(input); } catch (err) { console.warn('[verifyCircuit] failed', err); return null; } }, []); /** * Returns true if the caller should proceed inline. If the verifier finds * errors we stash a resume callback in `pendingRunRef` and pop the * verification modal; the resume callback re-enters `handleRun` with * `skipVerify = true` so we don't loop. Warnings-only results don't * block — they surface inline via `setMessage` and the run continues. */ const checkOrBlock = useCallback( async (resume: () => void): Promise => { const result = await runVerification(); if (!result) return true; if (result.errors.length === 0 && result.warnings.length === 0) return true; if (result.errors.length === 0) { // Warnings only — non-blocking. Surface inline and continue. const summary = result.warnings .slice(0, 3) .map((w) => w.message) .join(' • '); const more = result.warnings.length > 3 ? ` (+${result.warnings.length - 3} more)` : ''; setMessage({ type: 'error', text: `${result.warnings.length} circuit warning${result.warnings.length === 1 ? '' : 's'}: ${summary}${more}`, }); return true; } // Errors → block until the user explicitly chooses Run Anyway. pendingRunRef.current = resume; setVerification(result); return false; }, [runVerification], ); const handleRun = async (skipVerify = false) => { console.log('[handleRun] click', { activeBoardId, running, codeChangedSinceLastCompile }); // Pre-flight: solve the circuit and check for shorts / overcurrent / // overpower. If anything trips we hand control to the modal, which // resumes by calling `handleRun(true)` for "Run anyway". if (!skipVerify) { const ok = await checkOrBlock(() => handleRun(true)); if (!ok) return; } // Board-less circuits (SPICE-only digital / analog gallery) have no MCU // to start. Resuming the electrical solver replays any switch toggles // captured while paused so the canvas catches up instantly. if (isBoardless) { setElectricalPaused(false); setMessage(null); return; } if (activeBoardId) { const board = boards.find((b) => b.id === activeBoardId); console.log('[handleRun] active board', { id: board?.id, kind: board?.boardKind, hasCompiledProgram: !!board?.compiledProgram, compiledProgramLen: board?.compiledProgram?.length ?? 0, }); // MicroPython mode: stop any running session first, then reload firmware + start if (board?.languageMode === 'micropython') { trackRunSimulation(board.boardKind); reportRun(board.boardKind); // Always stop the current session so the new run gets a clean QEMU boot. // This also prevents the double start_esp32 that occurs when the bridge // is already connected and startBoard() is called again. if (board.running) { stopBoard(activeBoardId); // Give the WebSocket a moment to close cleanly before reconnecting. await new Promise((resolve) => setTimeout(resolve, 300)); } setCompiling(true); setMessage(null); addLog({ timestamp: new Date(), type: 'info', message: 'MicroPython: loading firmware and user files...', }); try { const groupFiles = useEditorStore.getState().getGroupFiles(board.activeFileGroupId); const pyFiles = groupFiles.map((f) => ({ name: f.name, content: f.content })); await loadMicroPythonProgram(activeBoardId, pyFiles); addLog({ timestamp: new Date(), type: 'success', message: 'MicroPython firmware loaded', }); } catch (err) { const errMsg = err instanceof Error ? err.message : 'Failed to load MicroPython'; addLog({ timestamp: new Date(), type: 'error', message: errMsg }); setMessage({ type: 'error', text: errMsg }); setCompiling(false); return; } setCompiling(false); startBoard(activeBoardId); setMessage(null); return; } const isQemuBoard = board?.boardKind && isPiBoardKind(board.boardKind) || board?.boardKind === 'esp32' || board?.boardKind === 'esp32-s3' || board?.boardKind === 'esp32-cam' || board?.boardKind === 'esp32-c3' || board?.boardKind === 'esp32-devkit-c-v4' || board?.boardKind === 'wemos-lolin32-lite' || board?.boardKind === 'xiao-esp32-s3' || board?.boardKind === 'arduino-nano-esp32' || board?.boardKind === 'xiao-esp32-c3' || board?.boardKind === 'aitewinrobot-esp32c3-supermini'; // QEMU boards: auto-compile if no firmware available yet if (isQemuBoard) { console.log('[handleRun] QEMU path'); if (!board?.compiledProgram || codeChangedSinceLastCompile) { console.log('[handleRun] auto-compile + run'); autoRunAfterCompile.current = true; await handleCompile(); const updatedBoard = useSimulatorStore .getState() .boards.find((b) => b.id === activeBoardId); console.log('[handleRun] after compile', { hasCompiledProgram: !!updatedBoard?.compiledProgram, compiledProgramLen: updatedBoard?.compiledProgram?.length ?? 0, autoRunFlag: autoRunAfterCompile.current, }); if (autoRunAfterCompile.current) { autoRunAfterCompile.current = false; if (updatedBoard?.compiledProgram) { trackRunSimulation(updatedBoard.boardKind); reportRun(updatedBoard.boardKind); console.log('[handleRun] → startBoard', activeBoardId); startBoard(activeBoardId); setMessage(null); } else { // handleCompile returned without producing a firmware/program. // Most common causes: arduino-cli unreachable, ESP-IDF compile // error in the user's sketch, MicroPython firmware download // failed, or the bridge rejected the load. handleCompile has // already addLog'd the underlying error — surface a top-level // toast too so the user knows their Run click didn't silently // succeed. const isMicropython = updatedBoard?.languageMode === 'micropython'; const errText = isMicropython ? 'MicroPython firmware did not load. Click "Load MicroPython" to retry, or check the console for the underlying error.' : 'Compilation produced no firmware. Check the output console for the underlying error.'; console.warn('[handleRun] compile finished but no compiledProgram — not starting'); setMessage({ type: 'error', text: errText }); addLog({ timestamp: new Date(), type: 'error', message: errText }); } } return; } trackRunSimulation(board?.boardKind); reportRun(board?.boardKind); console.log('[handleRun] → startBoard (already compiled)', activeBoardId); startBoard(activeBoardId); setMessage(null); return; } // Auto-compile if no program or code changed since last compile if (!board?.compiledProgram || codeChangedSinceLastCompile) { autoRunAfterCompile.current = true; await handleCompile(); // After compile, check if it succeeded and run const updatedBoard = useSimulatorStore .getState() .boards.find((b) => b.id === activeBoardId); if (autoRunAfterCompile.current && updatedBoard?.compiledProgram) { autoRunAfterCompile.current = false; trackRunSimulation(updatedBoard.boardKind); reportRun(updatedBoard.boardKind); startBoard(activeBoardId); setMessage(null); } else { autoRunAfterCompile.current = false; } return; } trackRunSimulation(board?.boardKind); reportRun(board?.boardKind); startBoard(activeBoardId); setMessage(null); return; } // Legacy fallback if (!compiledHex || codeChangedSinceLastCompile) { autoRunAfterCompile.current = true; await handleCompile(); const hex = useSimulatorStore.getState().compiledHex; if (autoRunAfterCompile.current && hex) { autoRunAfterCompile.current = false; trackRunSimulation(); reportRun(undefined); startSimulation(); setMessage(null); } else { autoRunAfterCompile.current = false; } } else { trackRunSimulation(); reportRun(undefined); startSimulation(); setMessage(null); } }; const handleStop = () => { trackStopSimulation(); if (isBoardless) { // Freeze the SPICE solver — every LED stays at its current brightness // and switch clicks stop re-triggering ngspice until the user hits Run. setElectricalPaused(true); setMessage(null); return; } if (activeBoardId) stopBoard(activeBoardId); else stopSimulation(); setMessage(null); }; const handleReset = () => { trackResetSimulation(); if (activeBoardId) resetBoard(activeBoardId); else resetSimulation(); setMessage(null); }; /** * Compile every board on the canvas sequentially. Progress + per-board * results stream to the existing compilation console — no separate dialog. * Returns the count of boards that ended up with a runnable program (so * Run All can use it to decide whether to proceed to start them). */ const compileAllBoards = async (): Promise<{ ok: number; failed: number }> => { const boardsList = useSimulatorStore.getState().boards; if (boardsList.length === 0) return { ok: 0, failed: 0 }; setCompileAllRunning(true); setConsoleOpen(true); addLog({ timestamp: new Date(), type: 'info', message: `Compiling all ${boardsList.length} board${boardsList.length === 1 ? '' : 's'}...`, }); let ok = 0; let failed = 0; for (const board of boardsList) { const label = BOARD_KIND_LABELS[board.boardKind] ?? board.boardKind; if (isPiBoardKind(board.boardKind)) { addLog({ timestamp: new Date(), type: 'info', message: `${label}: skipped (no compilation needed)`, }); ok++; continue; } const fqbn = BOARD_KIND_FQBN[board.boardKind]; if (!fqbn) { addLog({ timestamp: new Date(), type: 'error', message: `${label}: no FQBN configured`, }); failed++; continue; } addLog({ timestamp: new Date(), type: 'info', message: `${label}: compiling...` }); try { const groupFiles = useEditorStore.getState().getGroupFiles(board.activeFileGroupId); const sketchFiles = groupFiles.map((f) => ({ name: f.name, content: f.content })); // Stream live cmake + ninja output per-board (Compile-All flow). let lastStreamedLen = 0; const result = await compileCode( sketchFiles, fqbn, currentProject?.id ?? null, ({ stdout }) => { if (stdout.length <= lastStreamedLen) return; const delta = stdout.slice(lastStreamedLen); lastStreamedLen = stdout.length; const newLines = delta.split('\n').filter((s) => s.trim()); if (!newLines.length) return; const now = new Date(); setCompileLogs((prev: CompilationLog[]) => [ ...prev, ...newLines.map((line) => ({ timestamp: now, type: 'info' as const, message: `${label}: ${line}`, })), ]); }, { boardOptions: board.boardOptions, spiffsFiles: board.spiffsFiles }, ); const resultLogs = parseCompileResult(result, label); setCompileLogs((prev: CompilationLog[]) => [...prev, ...resultLogs]); if (result.success) { const program = result.hex_content ?? result.binary_content ?? null; if (program) { compileBoardProgram(board.id, program); if (result.has_wifi !== undefined) { updateBoard(board.id, { hasWifi: result.has_wifi }); } } ok++; } else { failed++; } } catch (err) { addLog({ timestamp: new Date(), type: 'error', message: `${label}: ${err instanceof Error ? err.message : String(err)}`, }); failed++; } } addLog({ timestamp: new Date(), type: ok > 0 && failed === 0 ? 'success' : failed > 0 ? 'error' : 'info', message: `Done — ${ok} succeeded, ${failed} failed`, }); if (ok > 0 && failed === 0) markCompiled(); setCompileAllRunning(false); return { ok, failed }; }; const handleCompileAll = () => { trackCompileCode(); void compileAllBoards(); }; /** Run All = compile all (if needed) + start every board, mirroring single Run. */ const handleRunAll = async () => { const boardsList = useSimulatorStore.getState().boards; if (boardsList.length === 0) return; // Compile if anything is missing a program or code changed since last compile const needsCompile = codeChangedSinceLastCompile || boardsList.some( (b) => !isPiBoardKind(b.boardKind) && b.languageMode !== 'micropython' && !b.compiledProgram, ); if (needsCompile) { const { failed } = await compileAllBoards(); if (failed > 0) return; // Don't start anything if any board failed } // Refresh list after compile (compiledProgram may have changed) const refreshed = useSimulatorStore.getState().boards; for (const board of refreshed) { if (board.running) continue; const isQemu = isPiBoardKind(board.boardKind) || board.boardKind === 'esp32' || board.boardKind === 'esp32-s3'; if (isQemu || board.compiledProgram || board.languageMode === 'micropython') { trackRunSimulation(board.boardKind); reportRun(board.boardKind); startBoard(board.id); } } }; const handleExport = async () => { try { const { components, wires, boardPosition, boardType: legacyBoardType, } = useSimulatorStore.getState(); const projectName = files.find((f) => f.name.endsWith('.ino'))?.name.replace('.ino', '') || 'velxio-project'; await exportToWokwiZip(files, components, wires, legacyBoardType, projectName, boardPosition); } catch (err) { setMessage({ type: 'error', text: 'Export failed.' }); } }; const handleFirmwareUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (firmwareInputRef.current) firmwareInputRef.current.value = ''; if (!file) return; setConsoleOpen(true); addLog({ timestamp: new Date(), type: 'info', message: `Loading firmware: ${file.name}...` }); try { const boardKind = activeBoard?.boardKind; if (!boardKind) { setMessage({ type: 'error', text: 'No board selected' }); return; } const result = await readFirmwareFile(file, boardKind); // Architecture mismatch warning for ELF files if (result.elfInfo?.suggestedBoard && result.elfInfo.suggestedBoard !== boardKind) { const detected = result.elfInfo.architectureName; const current = activeBoard ? BOARD_KIND_LABELS[activeBoard.boardKind] : boardKind; addLog({ timestamp: new Date(), type: 'info', message: `Note: Detected ${detected} architecture, but current board is ${current}. Loading anyway.`, }); } if (activeBoardId) { compileBoardProgram(activeBoardId, result.program); markCompiled(); addLog({ timestamp: new Date(), type: 'info', message: result.message }); setMessage({ type: 'success', text: `Firmware loaded: ${file.name}` }); } } catch (err) { const errMsg = err instanceof Error ? err.message : 'Failed to load firmware'; addLog({ timestamp: new Date(), type: 'error', message: errMsg }); setMessage({ type: 'error', text: errMsg }); } }; const handleImportFile = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!importInputRef.current) return; importInputRef.current.value = ''; if (!file) return; try { const result = await importProjectFile(file); if (result.kind === 'vlx') { // importVlxFile already wrote into the stores. setMessage({ type: 'success', text: `Imported ${file.name}` }); return; } // .zip path: apply the parsed payload to the stores ourselves, then // surface any missing libraries via the existing install modal. const { loadFiles } = useEditorStore.getState(); const { setComponents, setWires, setBoardType, setBoardPosition, stopSimulation } = useSimulatorStore.getState(); stopSimulation(); if (result.boardType) setBoardType(result.boardType); setBoardPosition(result.boardPosition); setComponents(result.components); setWires(result.wires); if (result.files.length > 0) loadFiles(result.files); setMessage({ type: 'success', text: `Imported ${file.name}` }); if (result.libraries.length > 0) { setPendingLibraries(result.libraries); setInstallModalOpen(true); } } catch (err: any) { setMessage({ type: 'error', text: err?.message || 'Import failed.' }); } }; return ( <>
{/* MicroPython language selector — only when active board supports it. The board context pill that used to live here was removed: it duplicated the BoardSelector dropdown elsewhere in the toolbar. */} {activeBoard && BOARD_SUPPORTS_MICROPYTHON.has(activeBoard.boardKind) && ( )}
{/* Compile */}
{/* Run */} {/* Stop */} {/* Reset */} {boards.length > 1 && ( <>
{/* Compile All */} {/* Run All */} )}
{/* Center slot — file tabs share the row so action icons stay pinned. */} {centerSlot &&
{centerSlot}
}
{/* Hidden file input for project import. Accepts both .vlx (Velxio native) and .zip (Wokwi bundle); the dispatcher in utils/importProject.ts picks the right loader by extension. */} {/* Hidden file input for firmware upload */} {/* Library Manager — always visible with label */} {/* Import zip — was previously hidden in a 3-dot overflow menu; inlined since there's space and the discoverability cost outweighed the toolbar savings. */}
{/* Output Console toggle */} {rightSlot}
{/* Error detail bar */} {message?.type === 'error' && message.text.length > 40 && !consoleOpen && (
{message.text}
)} {/* Missing library hint */} {missingLibHint && (
{t('editor.toolbar.libHint.message')}
)} setLibManagerOpen(false)} /> setInstallModalOpen(false)} libraries={pendingLibraries} /> {verification && ( { pendingRunRef.current = null; setVerification(null); }} onRunAnyway={() => { const resume = pendingRunRef.current; pendingRunRef.current = null; setVerification(null); resume?.(); }} /> )} ); };