fix(simulator): circuitVerifier worst-case GPIO + LED NaN guard
Two related correctness fixes that make the simulator's realism match what users actually see. 1. circuitVerifier was running pre-flight against the IDLE circuit (every pin LOW). A Blink sketch is going to write pin 13 HIGH eventually — at which point a missing series resistor produces a ~500 mA spike through the diode. But because pre-flight ran with pin 13 LOW the led-overcurrent rule never fired, and the user sailed through Run only to see the LED stay mysteriously dark on the canvas. The verifier now forces every digital pin connected to a load to HIGH = vcc, the worst case any well-defined sketch will eventually impose. The existing rules (led-overcurrent, resistor-overpower, short-circuit) now fire correctly and the existing CircuitVerificationModal blocks Run until the user adds a proper current limiter or chooses Run Anyway. Pins that are inputs-only (a pull-up + button) get over-driven here too, but the rules tolerate that — a pull-up at 5 V draws ~0.5 mA, well below all thresholds. A circuit that would actually fault under HIGH is flagged. 2. LED simulator was crashing visually on non-finite ngspice branch currents. A degenerate diode (no series R) makes ngspice return NaN, which fell through 'raw !== undefined && current > 1e-6' as false and never triggered the digital fallback. Now we check Number.isFinite(raw) before trusting it — non-finite returns route to the digital fallback so the LED at least lights visually when its driver pin is HIGH (the user still sees the verifier warning that the real-world circuit is wrong, but Run Anyway is not a black screen).
This commit is contained in:
parent
8a6d72aa50
commit
06d6e0afbb
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEditorStore } from '../../store/useEditorStore';
|
||||
import { useSimulatorStore, getBoardPinManager } from '../../store/useSimulatorStore';
|
||||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||
import { useElectricalStore } from '../../store/useElectricalStore';
|
||||
import { verifyCircuit, type VerificationResult } from '../../simulation/verify/circuitVerifier';
|
||||
import { buildInputFromStore } from '../../simulation/spice/storeAdapter';
|
||||
|
|
@ -310,25 +310,36 @@ export const EditorToolbar = ({
|
|||
})),
|
||||
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<string, PinSourceState> = {};
|
||||
const pm = getBoardPinManager(b.id);
|
||||
const group = BOARD_PIN_GROUPS[b.boardKind] ?? BOARD_PIN_GROUPS.default;
|
||||
if (pm) {
|
||||
const pinNames = new Set<string>();
|
||||
for (const w of sim.wires) {
|
||||
if (w.start.componentId === b.id) pinNames.add(w.start.pinName);
|
||||
if (w.end.componentId === b.id) pinNames.add(w.end.pinName);
|
||||
}
|
||||
for (const pinName of pinNames) {
|
||||
// No mapping table here — verification is a best-effort
|
||||
// snapshot; pins we can't identify just stay floating, which
|
||||
// matches their pre-Run state anyway.
|
||||
const arduinoPin = Number.parseInt(pinName, 10);
|
||||
if (Number.isNaN(arduinoPin)) continue;
|
||||
if (pm.getPinState(arduinoPin)) {
|
||||
pinStates[pinName] = { type: 'digital', v: group.vcc };
|
||||
}
|
||||
}
|
||||
const wiredPinNames = new Set<string>();
|
||||
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 };
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -196,7 +196,16 @@ PartSimulationRegistry.register('led', {
|
|||
raw = sum / samples.length;
|
||||
}
|
||||
}
|
||||
if (raw !== undefined) {
|
||||
// Guard against NaN / Infinity coming back from ngspice. They
|
||||
// happen on degenerate circuits (a forward-biased diode with no
|
||||
// series resistor — the textbook "missing 220Ω" mistake — is
|
||||
// the most common case). Without this guard the LED would mark
|
||||
// `el.value = NaN > 1e-6 = false` and stay visually dark even
|
||||
// when the user clicks "Run Anyway" past the verifier warning.
|
||||
// Treat non-finite branch currents as "SPICE has nothing useful
|
||||
// to say" → fall through to the digital fallback so at least the
|
||||
// LED visually lights when its driver pin is HIGH.
|
||||
if (raw !== undefined && Number.isFinite(raw)) {
|
||||
const current = Math.abs(raw);
|
||||
lastSpiceBrightness = Math.min(1, current / 0.02);
|
||||
lastSpiceTs = Date.now();
|
||||
|
|
@ -204,7 +213,7 @@ PartSimulationRegistry.register('led', {
|
|||
el.brightness = lastSpiceBrightness;
|
||||
return;
|
||||
}
|
||||
if (Date.now() - lastSpiceTs < HOLD_MS && lastSpiceTs > 0) {
|
||||
if (Date.now() - lastSpiceTs < HOLD_MS && lastSpiceTs > 0 && Number.isFinite(lastSpiceBrightness)) {
|
||||
el.value = lastSpiceBrightness > 1e-3;
|
||||
el.brightness = lastSpiceBrightness;
|
||||
return;
|
||||
|
|
|
|||
Loading…
Reference in New Issue