93 lines
3.8 KiB
TypeScript
93 lines
3.8 KiB
TypeScript
|
|
/**
|
||
|
|
* connectChipInputsToSolve — feed solved net voltages back into custom-chip
|
||
|
|
* INPUT pins, so a chip can read buttons / switches / sensors with no board.
|
||
|
|
*
|
||
|
|
* A custom chip already drives its OUTPUT pins into the netlist (chipPinDrives
|
||
|
|
* -> SPICE voltage sources -> LEDs light). The INPUT direction was missing: a
|
||
|
|
* chip pin wired to a pushbutton / switch / sensor had its net solved by
|
||
|
|
* ngspice, but NOTHING wrote that net's state back to the PinManager key the
|
||
|
|
* chip reads via `vx_pin_read`. So a chip could light LEDs board-less but never
|
||
|
|
* read an input without an Arduino driving the pin.
|
||
|
|
*
|
||
|
|
* After every solve we look up each wired chip pin's net voltage, threshold it
|
||
|
|
* to HIGH/LOW, and `triggerPinChange()` the chip's synthetic pin — which both
|
||
|
|
* updates `getPinState` (polling reads) and fires the chip's `onPinChange` edge
|
||
|
|
* handlers. Pins the chip is actively DRIVING are skipped so we never fight its
|
||
|
|
* own outputs. This is the input counterpart of the chip-output SPICE path and
|
||
|
|
* is solver-agnostic — it reads only the electrical store shape.
|
||
|
|
*/
|
||
|
|
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||
|
|
import { useElectricalStore } from '../../store/useElectricalStore';
|
||
|
|
import { syntheticChipPin } from '../customChips/syntheticPins';
|
||
|
|
import { getChipDrivenPins } from '../customChips/chipPinDrives';
|
||
|
|
|
||
|
|
// 5V-logic thresholds with a hysteresis band so a node hovering near the
|
||
|
|
// midpoint doesn't chatter HIGH/LOW every solve. Digital inputs (a button to
|
||
|
|
// VCC with a pull-down) swing fully, so the band is rarely entered.
|
||
|
|
const V_HIGH = 3.0;
|
||
|
|
const V_LOW = 2.0;
|
||
|
|
|
||
|
|
/** Pin names declared by a chip.json (entries may be strings or {name,...}). */
|
||
|
|
function chipPinNames(chipJsonStr: string): string[] {
|
||
|
|
try {
|
||
|
|
const obj = JSON.parse(chipJsonStr);
|
||
|
|
if (Array.isArray(obj.pins)) {
|
||
|
|
return obj.pins
|
||
|
|
.map((p: unknown) =>
|
||
|
|
typeof p === 'string' ? p : String((p as { name?: string } | null)?.name ?? ''),
|
||
|
|
)
|
||
|
|
.filter(Boolean);
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
/* malformed chip.json — no readable pins */
|
||
|
|
}
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
export function connectChipInputsToSolve(): () => void {
|
||
|
|
// Last logic level written per synthetic pin, so we only emit real edges
|
||
|
|
// (and so the hysteresis band can hold the previous level).
|
||
|
|
const lastState = new Map<number, boolean>();
|
||
|
|
|
||
|
|
function writeChipInputs() {
|
||
|
|
const { nodeVoltages, pinNetMap } = useElectricalStore.getState();
|
||
|
|
const sim = useSimulatorStore.getState();
|
||
|
|
const pinManager = sim.pinManager;
|
||
|
|
if (!pinManager) return;
|
||
|
|
|
||
|
|
for (const comp of sim.components) {
|
||
|
|
if (comp.metadataId !== 'custom-chip') continue;
|
||
|
|
const props = comp.properties as Record<string, unknown>;
|
||
|
|
const names = chipPinNames(String(props.chipJson ?? '{}'));
|
||
|
|
if (names.length === 0) continue;
|
||
|
|
// Pins the chip is currently driving as outputs — never overwrite those.
|
||
|
|
const driven = new Set(getChipDrivenPins(comp.id).map((d) => d.pin));
|
||
|
|
|
||
|
|
for (const pinName of names) {
|
||
|
|
if (driven.has(pinName)) continue;
|
||
|
|
const net = pinNetMap.get(`${comp.id}:${pinName}`);
|
||
|
|
if (!net) continue;
|
||
|
|
const v = nodeVoltages[net];
|
||
|
|
if (v == null) continue;
|
||
|
|
|
||
|
|
const synth = syntheticChipPin(comp.id, pinName);
|
||
|
|
const prev = lastState.get(synth);
|
||
|
|
let next: boolean;
|
||
|
|
if (v >= V_HIGH) next = true;
|
||
|
|
else if (v <= V_LOW) next = false;
|
||
|
|
else next = prev ?? false; // inside the hysteresis band — hold
|
||
|
|
if (prev === next) continue;
|
||
|
|
lastState.set(synth, next);
|
||
|
|
pinManager.triggerPinChange(synth, next);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const unsub = useElectricalStore.subscribe((state, prev) => {
|
||
|
|
if (state.nodeVoltages !== prev.nodeVoltages) writeChipInputs();
|
||
|
|
});
|
||
|
|
// Initial pass for examples that pre-populate the store before mount.
|
||
|
|
writeChipInputs();
|
||
|
|
return () => unsub();
|
||
|
|
}
|