diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index 3af4affe..e9ece5c6 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -3,11 +3,9 @@ import { useTranslation } from 'react-i18next'; import { useEditorStore, chipFileGroupId } 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 { type VerificationResult } from '../../simulation/verify/circuitVerifier'; +import { verifyCircuitFromStore } from '../../simulation/verify/verifyFromStore'; import { CircuitVerificationModal } from '../simulator/CircuitVerificationModal'; -import type { PinSourceState } from '../../simulation/spice/types'; import type { BoardKind, LanguageMode } from '../../types/board'; import { BOARD_KIND_FQBN, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind, boardDisplayName } from '../../types/board'; import { compileCode } from '../../services/compilation'; @@ -646,77 +644,10 @@ export const EditorToolbar = ({ * 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); - const result = await verifyCircuit(input); - // Concise outcome log — verification failing silently in production is - // hard to spot otherwise (the rules read 0 A when currents are missing). - console.log( - '[verify]', - JSON.stringify({ - errors: result.errors.map((e) => e.code), - warnings: result.warnings.map((w) => w.code), - solved: !!result.solve, - branches: result.solve ? Object.keys(result.solve.branchCurrents) : null, - nodes: result.solve ? Object.keys(result.solve.nodeVoltages) : null, - }), - ); - return result; - } catch (err) { - console.warn('[verifyCircuit] failed', err); - return null; - } - }, []); + const runVerification = useCallback( + (): Promise => verifyCircuitFromStore(), + [], + ); /** * Returns true if the caller should proceed inline. All findings are written diff --git a/frontend/src/simulation/verify/verifyFromStore.ts b/frontend/src/simulation/verify/verifyFromStore.ts new file mode 100644 index 00000000..a0f0c5d9 --- /dev/null +++ b/frontend/src/simulation/verify/verifyFromStore.ts @@ -0,0 +1,87 @@ +/** + * Store-driven pre-flight verification — shared by the Run button + * (EditorToolbar) and any programmatic runner (extensions can gate their own + * run paths on the same rules). Builds the worst-case snapshot — every wired + * digital pin driven HIGH at the board's vcc — and solves it. + * + * Returns null when there is nothing analysable yet or the solver failed to + * converge; callers treat null as "don't block". + */ + +import { useSimulatorStore } from '../../store/useSimulatorStore'; +import { buildInputFromStore } from '../spice/storeAdapter'; +import { BOARD_PIN_GROUPS } from '../spice/boardPinGroups'; +import type { PinSourceState } from '../spice/types'; +import { verifyCircuit, type VerificationResult } from './circuitVerifier'; + +export async function verifyCircuitFromStore(): 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); + const result = await verifyCircuit(input); + // Concise outcome log — verification failing silently in production is + // hard to spot otherwise (the rules read 0 A when currents are missing). + console.log( + '[verify]', + JSON.stringify({ + errors: result.errors.map((e) => e.code), + warnings: result.warnings.map((w) => w.code), + solved: !!result.solve, + branches: result.solve ? Object.keys(result.solve.branchCurrents) : null, + nodes: result.solve ? Object.keys(result.solve.nodeVoltages) : null, + }), + ); + return result; + } catch (err) { + console.warn('[verifyCircuit] failed', err); + return null; + } +}