feat: add capacitor and inductor components, update netlist builder to return pinNetMap
This commit is contained in:
parent
64f6f76160
commit
61c1ddfc22
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"generatedAt": "2026-04-16T04:07:07.823Z",
|
||||
"generatedAt": "2026-04-17T22:10:37.555Z",
|
||||
"components": [
|
||||
{
|
||||
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"64\" height=\"64\" fill=\"#e0e0e0\" rx=\"4\"/>\n <text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dy=\".3em\" font-size=\"10\" fill=\"#666\">\n DIODE-1N4007\n </text>\n </svg>",
|
||||
|
|
@ -2853,6 +2853,29 @@
|
|||
"led"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "capacitor",
|
||||
"tagName": "wokwi-capacitor",
|
||||
"name": "Capacitor",
|
||||
"category": "passive",
|
||||
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"64\" height=\"64\" fill=\"#e0e0e0\" rx=\"4\"/>\n <text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dy=\".3em\" font-size=\"10\" fill=\"#666\">\n CAPACITOR\n </text>\n </svg>",
|
||||
"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": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"64\" height=\"64\" fill=\"#e0e0e0\" rx=\"4\"/>\n <text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dy=\".3em\" font-size=\"10\" fill=\"#666\">\n INDUCTOR\n </text>\n </svg>",
|
||||
"properties": [
|
||||
{
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"defaultValue": "1m",
|
||||
"control": "text"
|
||||
}
|
||||
],
|
||||
"defaultValues": {
|
||||
"value": "1m"
|
||||
},
|
||||
"pinCount": 0,
|
||||
"tags": [
|
||||
"inductor",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ir-receiver",
|
||||
"tagName": "wokwi-ir-receiver",
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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: {} },
|
||||
|
|
|
|||
|
|
@ -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' } },
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, string>;
|
||||
}
|
||||
|
||||
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<string, string>();
|
||||
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<BuildNetlistInput, 'components' | 'wires' | 'boards'>,
|
||||
): Map<string, string> {
|
||||
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<string, string>();
|
||||
|
||||
// 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';
|
||||
|
|
|
|||
|
|
@ -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<Record<BoardKind, Array<{ pinName: string; channel: n
|
|||
'aitewinrobot-esp32c3-supermini': adcRange('GPIO', 0, 6),
|
||||
};
|
||||
|
||||
export function wireElectricalSolver(): () => void {
|
||||
let lastPinStates: Record<string, Record<string, PinSourceState>> = {};
|
||||
/**
|
||||
* 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<string, Record<string, PinSourceState>> {
|
||||
const boards = useSimulatorStore.getState().boards;
|
||||
const out: Record<string, Record<string, PinSourceState>> = {};
|
||||
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<string, PinSourceState> = {};
|
||||
// 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<Record<BoardKind, (pinName: string, channel: number) => 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<string, PinSourceState> {
|
||||
const pm = getBoardPinManager(boardId);
|
||||
if (!pm) return {};
|
||||
const group = BOARD_PIN_GROUPS[boardKind] ?? BOARD_PIN_GROUPS.default;
|
||||
const vcc = group.vcc;
|
||||
|
||||
const result: Record<string, PinSourceState> = {};
|
||||
// Gather all pin names wired to this board
|
||||
const pinNames = new Set<string>();
|
||||
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 "<boardId>_<pinName>" 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<typeof setInterval> | 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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, string>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ interface ElectricalState {
|
|||
engineLoading: boolean;
|
||||
engineReady: boolean;
|
||||
submittedNetlist: string;
|
||||
/** "boardId:pinName" → SPICE net name. Populated after each solve. */
|
||||
pinNetMap: Map<string, string>;
|
||||
|
||||
setMode: (m: ElectricalMode) => Promise<void>;
|
||||
triggerSolve: (input: BuildNetlistInput) => void;
|
||||
|
|
@ -54,6 +56,7 @@ export const useElectricalStore = create<ElectricalState>((set, get) => {
|
|||
error: r.error,
|
||||
lastSolveMs: r.solveMs,
|
||||
submittedNetlist: r.submittedNetlist,
|
||||
pinNetMap: r.pinNetMap,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -67,6 +70,7 @@ export const useElectricalStore = create<ElectricalState>((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<ElectricalState>((set, get) => {
|
|||
error: null,
|
||||
lastSolveMs: 0,
|
||||
submittedNetlist: '',
|
||||
pinNetMap: new Map(),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ const CATEGORY_MAP: Record<string, ComponentCategory> = {
|
|||
// Passive Components
|
||||
'resistor': 'passive',
|
||||
'capacitor': 'passive',
|
||||
'inductor': 'passive',
|
||||
'diode': 'passive',
|
||||
'analog-multiplexer': 'passive',
|
||||
'ir-receiver': 'passive',
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 14296fa4b6797ce8ddedc55b47eafa0fe9c2a0e0
|
||||
Loading…
Reference in New Issue