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": "",
@@ -2853,6 +2853,29 @@
"led"
]
},
+ {
+ "id": "capacitor",
+ "tagName": "wokwi-capacitor",
+ "name": "Capacitor",
+ "category": "passive",
+ "thumbnail": "",
+ "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": "",
+ "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