diff --git a/frontend/public/components-metadata.json b/frontend/public/components-metadata.json index 9f79f76a..388e5b41 100644 --- a/frontend/public/components-metadata.json +++ b/frontend/public/components-metadata.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "generatedAt": "2026-04-16T04:07:07.823Z", + "generatedAt": "2026-04-17T22:10:37.555Z", "components": [ { "thumbnail": "\n \n \n DIODE-1N4007\n \n ", @@ -2853,6 +2853,29 @@ "led" ] }, + { + "id": "capacitor", + "tagName": "wokwi-capacitor", + "name": "Capacitor", + "category": "passive", + "thumbnail": "\n \n \n CAPACITOR\n \n ", + "properties": [ + { + "name": "value", + "type": "string", + "defaultValue": "1u", + "control": "text" + } + ], + "defaultValues": { + "value": "1u" + }, + "pinCount": 0, + "tags": [ + "capacitor", + "" + ] + }, { "id": "franzininho", "tagName": "wokwi-franzininho", @@ -2889,6 +2912,29 @@ "franzininho" ] }, + { + "id": "inductor", + "tagName": "wokwi-inductor", + "name": "Inductor", + "category": "passive", + "thumbnail": "\n \n \n INDUCTOR\n \n ", + "properties": [ + { + "name": "value", + "type": "string", + "defaultValue": "1m", + "control": "text" + } + ], + "defaultValues": { + "value": "1m" + }, + "pinCount": 0, + "tags": [ + "inductor", + "" + ] + }, { "id": "ir-receiver", "tagName": "wokwi-ir-receiver", diff --git a/frontend/src/__tests__/component-to-spice.test.ts b/frontend/src/__tests__/component-to-spice.test.ts index 49493bef..5c51f631 100644 --- a/frontend/src/__tests__/component-to-spice.test.ts +++ b/frontend/src/__tests__/component-to-spice.test.ts @@ -172,7 +172,7 @@ describe('componentToSpice — ngspice accepts every card', () => { }); extraLoadCards.push(`R_load_${i} n_probe_${i} 0 1Meg`); } - const netlist = buildNetlist({ + const { netlist } = buildNetlist({ components: [ { id: 'dut', metadataId: id, properties: fx.properties ?? {} }, ...Array.from({ length: pins.length - 2 }, (_, i) => ({ diff --git a/frontend/src/__tests__/instruments.test.ts b/frontend/src/__tests__/instruments.test.ts index 0c6fe6a2..91625ec4 100644 --- a/frontend/src/__tests__/instruments.test.ts +++ b/frontend/src/__tests__/instruments.test.ts @@ -10,7 +10,7 @@ import { runNetlist } from '../simulation/spice/SpiceEngine'; describe('Voltmeter (instr-voltmeter)', () => { it('reads ~3.33V across the midpoint of a 1k+2k divider', { timeout: 30_000 }, async () => { - const netlist = buildNetlist({ + const { netlist } = buildNetlist({ components: [ { id: 'r1', metadataId: 'resistor', properties: { value: '1k' } }, { id: 'r2', metadataId: 'resistor', properties: { value: '2k' } }, @@ -48,7 +48,7 @@ describe('Ammeter (instr-ammeter)', () => { it('reads ~22.7 mA through a 220 Ω load on 5V', { timeout: 30_000 }, async () => { // Circuit: // +5V ── R (220Ω) ── [A+ ammeter A-] ── GND - const netlist = buildNetlist({ + const { netlist } = buildNetlist({ components: [ { id: 'r1', metadataId: 'resistor', properties: { value: '220' } }, { id: 'am', metadataId: 'instr-ammeter', properties: {} }, diff --git a/frontend/src/__tests__/netlist-builder.test.ts b/frontend/src/__tests__/netlist-builder.test.ts index f6038008..36339185 100644 --- a/frontend/src/__tests__/netlist-builder.test.ts +++ b/frontend/src/__tests__/netlist-builder.test.ts @@ -49,7 +49,7 @@ describe('parseValueWithUnits', () => { describe('NetlistBuilder — simple cases', () => { it('emits a voltage divider and ngspice gets 6V', { timeout: 30_000 }, async () => { - const netlist = buildNetlist({ + const { netlist } = buildNetlist({ components: [ { id: 'r1', metadataId: 'resistor', properties: { value: '1k' } }, { id: 'r2', metadataId: 'resistor', properties: { value: '2k' } }, @@ -86,7 +86,7 @@ describe('NetlistBuilder — simple cases', () => { }); it('emits an LED + resistor from 5V rail and ngspice solves non-linear', { timeout: 30_000 }, async () => { - const netlist = buildNetlist({ + const { netlist } = buildNetlist({ components: [ { id: 'r1', metadataId: 'resistor', properties: { value: '220' } }, { id: 'led1', metadataId: 'led', properties: { color: 'red' } }, @@ -120,7 +120,7 @@ describe('NetlistBuilder — simple cases', () => { }); it('NTC divider: T=25°C produces V(a0) ≈ 2.5V', { timeout: 30_000 }, async () => { - const netlist = buildNetlist({ + const { netlist } = buildNetlist({ components: [ { id: 'r1', metadataId: 'resistor', properties: { value: '10k' } }, { id: 'ntc1', metadataId: 'ntc-temperature-sensor', properties: { temperature: 25 } }, @@ -149,7 +149,7 @@ describe('NetlistBuilder — simple cases', () => { it('adds auto pull-down on floating cap-only node', () => { // Two caps share a node (net x) that only touches capacitors → DC-floating. - const netlist = buildNetlist({ + const { netlist } = buildNetlist({ components: [ { id: 'c1', metadataId: 'capacitor', properties: { value: '1u' } }, { id: 'c2', metadataId: 'capacitor', properties: { value: '1u' } }, @@ -166,7 +166,7 @@ describe('NetlistBuilder — simple cases', () => { }); it('PWM pin emits DC-equivalent voltage source', () => { - const netlist = buildNetlist({ + const { netlist } = buildNetlist({ components: [], wires: [ { id: 'w1', start: { componentId: 'uno', pinName: 'D9' }, end: { componentId: 'uno', pinName: 'GND' } }, diff --git a/frontend/src/data/examples-circuits.ts b/frontend/src/data/examples-circuits.ts index f054cbf2..8f056277 100644 --- a/frontend/src/data/examples-circuits.ts +++ b/frontend/src/data/examples-circuits.ts @@ -55,6 +55,7 @@ void loop() { category: 'circuits', difficulty: 'beginner', code: `// RC Low-Pass Filter // PWM on pin 9 → R=10k → C=10uF → smooth DC on A0 +// DC steady-state: V_out ≈ duty × Vcc = 0.5 × 5 = 2.5 V void setup() { Serial.begin(9600); analogWrite(9, 128); } // 50% duty void loop() { float v = analogRead(A0) * 5.0 / 1023.0; @@ -64,11 +65,13 @@ void loop() { components: [ UNO, { type: 'wokwi-resistor', id: 'r1', x: 350, y: 100, properties: { value: '10000' } }, + { type: 'wokwi-capacitor', id: 'c1', x: 420, y: 200, properties: { value: '10u' } }, ], wires: [ w('w1', ['arduino-uno','9'], ['r1','1'], '#00aaff'), w('w2', ['r1','2'], ['arduino-uno','A0'], '#ffaa00'), - w('w3', ['r1','2'], ['arduino-uno','GND'], '#000000'), + w('w3', ['r1','2'], ['c1','1'], '#ffaa00'), + w('w4', ['c1','2'], ['arduino-uno','GND'], '#000000'), ], }, @@ -290,11 +293,13 @@ void loop() { components: [ UNO, { type: 'wokwi-resistor', id: 'r1', x: 380, y: 100, properties: { value: '10000' } }, + { type: 'wokwi-capacitor', id: 'c1', x: 450, y: 200, properties: { value: '100u' } }, ], wires: [ w('w1', ['arduino-uno','8'], ['r1','1'], '#00aaff'), - w('w2', ['r1','2'], ['arduino-uno','A0'], '#ffaa00'), - w('w3', ['r1','2'], ['arduino-uno','GND'], '#000000'), + w('w2', ['r1','2'], ['c1','1'], '#ffaa00'), + w('w3', ['r1','2'], ['arduino-uno','A0'], '#ffaa00'), + w('w4', ['c1','2'], ['arduino-uno','GND'], '#000000'), ], }, diff --git a/frontend/src/simulation/spice/CircuitScheduler.ts b/frontend/src/simulation/spice/CircuitScheduler.ts index b8a1321b..03de39af 100644 --- a/frontend/src/simulation/spice/CircuitScheduler.ts +++ b/frontend/src/simulation/spice/CircuitScheduler.ts @@ -71,7 +71,7 @@ class CircuitScheduler { this.pending = null; this.inFlight = true; - const netlist = buildNetlist(req.input); + const { netlist, pinNetMap } = buildNetlist(req.input); const t0 = performance.now(); let result: ElectricalSolveResult; try { @@ -99,6 +99,7 @@ class CircuitScheduler { error: null, solveMs: performance.now() - t0, submittedNetlist: netlist, + pinNetMap, }; } catch (err) { result = { @@ -108,6 +109,7 @@ class CircuitScheduler { error: String(err instanceof Error ? err.message : err), solveMs: performance.now() - t0, submittedNetlist: netlist, + pinNetMap, }; } finally { this.inFlight = false; @@ -141,6 +143,7 @@ function noopResult(reason: string): ElectricalSolveResult { error: reason, solveMs: 0, submittedNetlist: '', + pinNetMap: new Map(), }; } diff --git a/frontend/src/simulation/spice/NetlistBuilder.ts b/frontend/src/simulation/spice/NetlistBuilder.ts index 20880010..83a4dd96 100644 --- a/frontend/src/simulation/spice/NetlistBuilder.ts +++ b/frontend/src/simulation/spice/NetlistBuilder.ts @@ -35,7 +35,13 @@ function skipCanonicalization(metadataId: string): boolean { return metadataId.startsWith('instr-'); } -export function buildNetlist(input: BuildNetlistInput): string { +export interface BuildNetlistResult { + netlist: string; + /** "boardId:pinName" → SPICE net name, from the same UF used to build the netlist. */ + pinNetMap: Map; +} + +export function buildNetlist(input: BuildNetlistInput): BuildNetlistResult { const { components, wires, boards, analysis, extraCards = [] } = input; // ── 1. Union-Find over wires ───────────────────────────────────────────── @@ -141,7 +147,23 @@ export function buildNetlist(input: BuildNetlistInput): string { } lines.push('.end'); - return lines.join('\n'); + // ── 9. Build board pin → net map from the same UF ───────────────────────── + // Must use the same `uf` and `netNames` so net names match what ngspice sees. + const pinNetMap = new Map(); + for (const board of boards) { + // All wire endpoints that belong to this board are already in the UF. + for (const w of wires) { + for (const endpoint of [w.start, w.end]) { + if (endpoint.componentId !== board.id) continue; + const key = pinKey(board.id, endpoint.pinName); + if (!uf.has(key)) continue; + const netName = netNames.get(uf.find(key)); + if (netName) pinNetMap.set(key, netName); + } + } + } + + return { netlist: lines.join('\n'), pinNetMap }; } // ── Helpers ──────────────────────────────────────────────────────────────── @@ -262,5 +284,75 @@ export function buildWireNetMap( return result; } +/** + * Build a map from `"${boardId}:${pinName}"` → SPICE net name for every + * board pin that participates in the circuit. Used by the ADC injection + * step in subscribeToStore so it can look up voltages by pin name. + */ +export function buildBoardPinNetMap( + input: Pick, +): Map { + const { wires, boards, components } = input; + const uf = new UnionFind(); + const pin = (cId: string, pName: string) => `${cId}:${pName}`; + + // Collect board IDs for fast lookup + const boardIds = new Set(boards.map((b) => b.id)); + + for (const w of wires) { + const a = pin(w.start.componentId, w.start.pinName); + const b = pin(w.end.componentId, w.end.pinName); + uf.add(a); + uf.add(b); + uf.union(a, b); + } + + // Canonicalize board ground/vcc pins (from boardPinGroups metadata) + for (const board of boards) { + for (const pName of board.groundPinNames ?? []) { + const k = pin(board.id, pName); + uf.add(k); + uf.setCanonical(k, '0'); + } + for (const pName of board.vccPinNames ?? []) { + const k = pin(board.id, pName); + uf.add(k); + uf.setCanonical(k, 'vcc_rail'); + } + } + // Canonicalize non-board component GND/VCC pins referenced by wires + for (const comp of components) { + if (comp.metadataId.startsWith('instr-')) continue; + if (boardIds.has(comp.id)) continue; // board handled above + for (const pName of pinsReferencedByWires(comp.id, wires)) { + if (GROUND_PIN_RE.test(pName)) uf.setCanonical(pin(comp.id, pName), '0'); + else if (VCC_PIN_RE.test(pName)) uf.setCanonical(pin(comp.id, pName), 'vcc_rail'); + } + } + + const netNames = assignDeterministicNetNames(uf); + const result = new Map(); + + // For each board, collect ALL pins that appear in wires (via wire endpoints) + // plus the explicit groundPinNames/vccPinNames/pins lists. + for (const board of boards) { + const wireReferencedPins = pinsReferencedByWires(board.id, wires); + const allPins = new Set([ + ...(board.groundPinNames ?? []), + ...(board.vccPinNames ?? []), + ...Object.keys(board.pins ?? {}), + ...wireReferencedPins, // ← the pins that actually exist in the UF + ]); + for (const pName of allPins) { + const k = pin(board.id, pName); + if (uf.has(k)) { + const netName = netNames.get(uf.find(k)); + if (netName) result.set(k, netName); + } + } + } + return result; +} + /** Re-export types for callers. */ export type { BuildNetlistInput, ComponentForSpice, BoardForSpice, WireForSpice } from './types'; diff --git a/frontend/src/simulation/spice/subscribeToStore.ts b/frontend/src/simulation/spice/subscribeToStore.ts index 2c41d0a0..11f64447 100644 --- a/frontend/src/simulation/spice/subscribeToStore.ts +++ b/frontend/src/simulation/spice/subscribeToStore.ts @@ -7,11 +7,13 @@ * Called once at app startup (typically from EditorPage or main.tsx). * Returns an `unsubscribe()` for cleanup. */ -import { useSimulatorStore } from '../../store/useSimulatorStore'; +import { useSimulatorStore, getBoardSimulator, getBoardPinManager } from '../../store/useSimulatorStore'; import { useElectricalStore } from '../../store/useElectricalStore'; import { buildInputFromStore } from './storeAdapter'; +import { setAdcVoltage } from '../parts/partUtils'; import type { PinSourceState } from './types'; import type { BoardKind } from '../../types/board'; +import { BOARD_PIN_GROUPS } from './boardPinGroups'; // Which Arduino-style pin name maps to which ADC channel, per board. // Used to inject SPICE-solved voltages back into the MCU's ADC peripheral. @@ -62,75 +64,149 @@ const ADC_PIN_MAP: Partial void { - let lastPinStates: Record> = {}; +/** + * Convert an ADC pin name + channel to the GPIO pin number that + * `setAdcVoltage()` (partUtils) expects, per board family. + * + * AVR: A0→14, A1→15, ... (analog pins start at 14) + * RP2040: GP26→26, GP27→27, ... (GPIO number directly) + * ESP32: GPIO32→32, ... or A0→channel-dependent (GPIO number) + */ +function avrPinFromName(_name: string, channel: number): number { return 14 + channel; } +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function gpioPinFromName(name: string, _channel: number): number { + const m = name.match(/(\d+)$/); + return m ? parseInt(m[1], 10) : -1; +} - function collectPinStates(): Record> { - const boards = useSimulatorStore.getState().boards; - const out: Record> = {}; - for (const board of boards) { - const simulator = board.simulator as unknown as { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pinManager?: any; - } | null; - const entries: Record = {}; - // NOTE: Velxio's PinManager tracks state per pin number. Translating - // number → pin name depends on board-specific maps. For Phase 8.3 we - // emit pin states only when `pinManager.getPinState(pin)` is readable; - // boards without that accessor simply contribute no GPIO sources - // (their wires still participate via canonicalized ground/vcc). - if (simulator?.pinManager?.getPinState) { - // Best-effort: we don't yet have a canonical pin-number → pin-name - // map for all boards here. Future work (Phase 8.4) will enrich this. - } - out[board.id] = entries; - } - return out; +const ADC_PIN_TO_GPIO: Partial number>> = { + 'arduino-uno': avrPinFromName, + 'arduino-nano': avrPinFromName, + 'arduino-mega': avrPinFromName, + 'attiny85': avrPinFromName, + + 'raspberry-pi-pico': gpioPinFromName, + 'pi-pico-w': gpioPinFromName, + + 'esp32': gpioPinFromName, + 'esp32-devkit-c-v4': gpioPinFromName, + 'esp32-cam': gpioPinFromName, + 'wemos-lolin32-lite': gpioPinFromName, + 'esp32-s3': gpioPinFromName, + 'xiao-esp32-s3': gpioPinFromName, + 'arduino-nano-esp32': avrPinFromName, // uses A0-A7 naming + 'esp32-c3': gpioPinFromName, + 'xiao-esp32-c3': gpioPinFromName, + 'aitewinrobot-esp32c3-supermini': gpioPinFromName, +}; + +/** + * Convert a board pin name (e.g. "9", "A0", "GP26", "GPIO32") to the + * Arduino-style pin number that PinManager uses internally. + * Returns -1 if the name doesn't map to a GPIO pin. + */ +function pinNameToArduinoPin(pinName: string, boardKind: BoardKind): number { + const group = BOARD_PIN_GROUPS[boardKind] ?? BOARD_PIN_GROUPS.default; + // Skip power/ground pins — they're handled as canonical nets + if (group.gnd.includes(pinName) || group.vcc_pins.includes(pinName)) return -1; + + // RP2040: "GP26" → 26 + if (pinName.startsWith('GP')) { + const n = parseInt(pinName.slice(2), 10); + return Number.isFinite(n) ? n : -1; } + // ESP32: "GPIO32" → 32 + if (pinName.startsWith('GPIO')) { + const n = parseInt(pinName.slice(4), 10); + return Number.isFinite(n) ? n : -1; + } + // AVR analog: "A0" → 14, "A1" → 15, ... + if (/^A\d+$/.test(pinName)) { + return 14 + parseInt(pinName.slice(1), 10); + } + // Bare numeric: "9" → 9, "13" → 13 + if (/^\d+$/.test(pinName)) { + return parseInt(pinName, 10); + } + return -1; +} + +/** + * Collect MCU output pin states from PinManager for pins that participate + * in the circuit (i.e., are referenced by wires). + */ +function collectPinStates( + boardId: string, + boardKind: BoardKind, + wires: Array<{ start: { componentId: string; pinName: string }; end: { componentId: string; pinName: string } }>, +): Record { + const pm = getBoardPinManager(boardId); + if (!pm) return {}; + const group = BOARD_PIN_GROUPS[boardKind] ?? BOARD_PIN_GROUPS.default; + const vcc = group.vcc; + + const result: Record = {}; + // Gather all pin names wired to this board + const pinNames = new Set(); + for (const w of wires) { + if (w.start.componentId === boardId) pinNames.add(w.start.pinName); + if (w.end.componentId === boardId) pinNames.add(w.end.pinName); + } + + for (const pinName of pinNames) { + const arduinoPin = pinNameToArduinoPin(pinName, boardKind); + if (arduinoPin < 0) continue; + const pwmDuty = pm.getPwmValue(arduinoPin); + if (pwmDuty > 0) { + result[pinName] = { type: 'pwm', duty: pwmDuty }; + } else if (pm.getPinState(arduinoPin)) { + result[pinName] = { type: 'digital', v: vcc }; + } + // If pin is LOW or unknown, don't add — treated as input/floating by SPICE + } + return result; +} + +export function wireElectricalSolver(): () => void { function maybeSolve() { const { mode } = useElectricalStore.getState(); if (mode === 'off') return; const storeState = useSimulatorStore.getState(); - const pinStates = collectPinStates(); - lastPinStates = pinStates; const snap = { components: storeState.components, wires: storeState.wires, boards: storeState.boards.map((b) => ({ id: b.id, boardKind: b.boardKind, - pinStates: pinStates[b.id] ?? {}, + pinStates: collectPinStates(b.id, b.boardKind, storeState.wires), })), }; const input = buildInputFromStore(snap); + // pinNetMap is now built inside buildNetlist() from the same UF and + // returned via CircuitScheduler → ElectricalSolveResult → store. useElectricalStore.getState().triggerSolve(input); } function injectVoltagesIntoADC() { - const { nodeVoltages } = useElectricalStore.getState(); + const { nodeVoltages, pinNetMap } = useElectricalStore.getState(); const { boards } = useSimulatorStore.getState(); for (const board of boards) { - const map = ADC_PIN_MAP[board.boardKind]; - if (!map) continue; - const simulator = board.simulator as unknown as { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - getADC?: () => { channelValues: number[] } | null; - } | null; - const adc = simulator?.getADC?.(); - if (!adc) continue; - // Try to find a net matching "_" by string lookup - for (const { pinName, channel } of map) { - const probeKey = `${board.id}:${pinName}`; - // ngspice result keys are net names from NetlistBuilder — we don't - // yet have a direct pin→net lookup here. This is a forward-looking - // scaffold; Phase 8.4 will add the map via storeAdapter. - void probeKey; - // For now: if any net name literally equals the pin label, use it. - const v = nodeVoltages[pinName] ?? nodeVoltages[probeKey] ?? null; - if (v != null) { - adc.channelValues[channel] = Math.max(0, Math.min(board.boardKind.startsWith('esp32') ? 3.3 : 5, v)); - } + const adcPins = ADC_PIN_MAP[board.boardKind]; + if (!adcPins) continue; + const sim = getBoardSimulator(board.id); + if (!sim) continue; + const vMax = board.boardKind.startsWith('esp32') ? 3.3 : 5.0; + for (const { pinName, channel } of adcPins) { + const netName = pinNetMap.get(`${board.id}:${pinName}`); + if (!netName) continue; + const v = nodeVoltages[netName]; + if (v == null) continue; + const clamped = Math.max(0, Math.min(vMax, v)); + // setAdcVoltage handles AVR (pin 14-19), RP2040 (GPIO 26-29), + // and ESP32 (bridge shim) transparently. + const gpioPin = ADC_PIN_TO_GPIO[board.boardKind]?.(pinName, channel); + if (gpioPin != null) setAdcVoltage(sim, gpioPin, clamped); } } } @@ -155,10 +231,53 @@ export function wireElectricalSolver(): () => void { } }); + // Re-inject whenever boards change (e.g. loadHex recreates AVRADC). + // loadHex() creates a fresh AVRADC *before* updating the store, so by the + // time this fires the new ADC already exists and needs the SPICE values. + const unsubBoards = useSimulatorStore.subscribe((state, prev) => { + if (state.boards !== prev.boards) { + const { nodeVoltages } = useElectricalStore.getState(); + if (Object.keys(nodeVoltages).length > 0) { + injectVoltagesIntoADC(); + } + } + }); + + // Periodic re-solve while any board is running, so SPICE picks up + // MCU pin-state changes (e.g. analogWrite → PWM → voltage source). + let solveInterval: ReturnType | null = null; + const SOLVE_INTERVAL_MS = 200; + + function updateSolveTimer() { + const { mode } = useElectricalStore.getState(); + const anyRunning = useSimulatorStore.getState().boards.some((b) => b.running); + if (mode !== 'off' && anyRunning) { + if (!solveInterval) { + solveInterval = setInterval(maybeSolve, SOLVE_INTERVAL_MS); + } + } else if (solveInterval) { + clearInterval(solveInterval); + solveInterval = null; + } + } + + const unsubRunning = useSimulatorStore.subscribe((state, prev) => { + const wasRunning = prev.boards.some((b) => b.running); + const nowRunning = state.boards.some((b) => b.running); + if (wasRunning !== nowRunning) updateSolveTimer(); + }); + + const unsubModeTimer = useElectricalStore.subscribe((state, prev) => { + if (state.mode !== prev.mode) updateSolveTimer(); + }); + return () => { unsubSim(); unsubMode(); unsubResult(); - void lastPinStates; + unsubBoards(); + unsubRunning(); + unsubModeTimer(); + if (solveInterval) clearInterval(solveInterval); }; } diff --git a/frontend/src/simulation/spice/types.ts b/frontend/src/simulation/spice/types.ts index d3db7804..56f5fac2 100644 --- a/frontend/src/simulation/spice/types.ts +++ b/frontend/src/simulation/spice/types.ts @@ -71,4 +71,9 @@ export interface ElectricalSolveResult { solveMs: number; /** The netlist we submitted — useful for debugging in the UI. */ submittedNetlist: string; + /** + * Maps "boardId:pinName" → SPICE net name, built from the same Union-Find + * used to generate the netlist. Used by ADC injection to locate voltages. + */ + pinNetMap: Map; } diff --git a/frontend/src/store/useElectricalStore.ts b/frontend/src/store/useElectricalStore.ts index ca35e319..911cb2ed 100644 --- a/frontend/src/store/useElectricalStore.ts +++ b/frontend/src/store/useElectricalStore.ts @@ -36,6 +36,8 @@ interface ElectricalState { engineLoading: boolean; engineReady: boolean; submittedNetlist: string; + /** "boardId:pinName" → SPICE net name. Populated after each solve. */ + pinNetMap: Map; setMode: (m: ElectricalMode) => Promise; triggerSolve: (input: BuildNetlistInput) => void; @@ -54,6 +56,7 @@ export const useElectricalStore = create((set, get) => { error: r.error, lastSolveMs: r.solveMs, submittedNetlist: r.submittedNetlist, + pinNetMap: r.pinNetMap, }); }); @@ -67,6 +70,7 @@ export const useElectricalStore = create((set, get) => { engineLoading: false, engineReady: false, submittedNetlist: '', + pinNetMap: new Map(), async setMode(m) { if (!ELECTRICAL_SIM_ENABLED && m !== 'off') { @@ -111,6 +115,7 @@ export const useElectricalStore = create((set, get) => { error: null, lastSolveMs: 0, submittedNetlist: '', + pinNetMap: new Map(), }); }, }; diff --git a/scripts/generate-component-metadata.ts b/scripts/generate-component-metadata.ts index e4cfb77c..5ba64021 100644 --- a/scripts/generate-component-metadata.ts +++ b/scripts/generate-component-metadata.ts @@ -72,6 +72,7 @@ const CATEGORY_MAP: Record = { // Passive Components 'resistor': 'passive', 'capacitor': 'passive', + 'inductor': 'passive', 'diode': 'passive', 'analog-multiplexer': 'passive', 'ir-receiver': 'passive', diff --git a/wokwi-libs/fritzing-parts b/wokwi-libs/fritzing-parts new file mode 160000 index 00000000..14296fa4 --- /dev/null +++ b/wokwi-libs/fritzing-parts @@ -0,0 +1 @@ +Subproject commit 14296fa4b6797ce8ddedc55b47eafa0fe9c2a0e0