velxio/frontend/src/simulation/spice/collectPinStates.ts

137 lines
5.6 KiB
TypeScript

/**
* collectPinStates — read MCU output pin states from each board's
* PinManager for pins that are referenced by a wire.
*
* Domain helper used by `CircuitSimulationService` to populate
* `BoardForSpice.pins` at solve time. Lives in its own module so
* the service doesn't depend on (and the legacy `subscribeToStore`
* doesn't own) the per-board pin-number mapping.
*/
import { getBoardPinManager } from '../../store/useSimulatorStore';
import type { PinSourceState } from './types';
import type { BoardKind } from '../../types/board';
import { isStm32BoardKind } from '../../types/board';
import { stm32PinNameToLinear } from '../Stm32Bridge';
import { BOARD_PIN_GROUPS } from './boardPinGroups';
/**
* 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.
*
* Exported so connectMcuEdgesToService can subscribe its MCU-edge
* listeners under the SAME name→pin mapping the netlist collector uses —
* a reverse mapping that disagrees (e.g. "GPIO2" vs the wire's "2")
* detaches every listener on resubscription and freezes LEDs mid-run.
*/
export function pinNameToArduinoPin(pinName: string, boardKind: BoardKind): number {
const group = BOARD_PIN_GROUPS[boardKind] ?? BOARD_PIN_GROUPS.default;
if (group.gnd.includes(pinName) || group.vcc_pins.includes(pinName)) return -1;
// 'GPIO' must be tested BEFORE 'GP': the GP branch used to shadow it,
// turning 'GPIO32' into parseInt('IO32') = NaN → -1 (dead branch).
if (pinName.startsWith('GPIO')) {
const n = parseInt(pinName.slice(4), 10);
return Number.isFinite(n) ? n : -1;
}
if (pinName.startsWith('GP')) {
const n = parseInt(pinName.slice(2), 10);
return Number.isFinite(n) ? n : -1;
}
if (/^A\d+$/.test(pinName)) {
return 14 + parseInt(pinName.slice(1), 10);
}
// ATtiny85 port-style names (PB0..PB5) map 1:1 to Arduino pin numbers
// because ATTinyCore's pinMap is identity for the tiny85 variant.
if (/^PB\d+$/.test(pinName)) {
return parseInt(pinName.slice(2), 10);
}
if (/^\d+$/.test(pinName)) {
return parseInt(pinName, 10);
}
return -1;
}
/**
* Build the `pinStates` field for a board going into the
* NetlistBuilder. Iterates pins that any wire references for this
* board, asks PinManager for the current digital / PWM value, and
* shapes it into the SPICE-side PinSourceState union.
*
* MCU output pins not in any wire are skipped — the netlist doesn't
* need a V source for an unconnected pin.
*/
export 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> = {};
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);
}
const outputPins = pm.getOutputPins();
// STM32 names pins PA0/PC13 and keys its PinManager on the linear pin
// (port*16+pin). It runs in backend QEMU, where its OUTPUT pins are surfaced
// to the canvas via the part layer (not SPICE), so here we only contribute
// the INPUT internal pull (reported by the worker's gpio_pull) — enough for
// NetlistBuilder to stamp the weak resistor so an INPUT_PULLUP button-to-GND
// solves to idle-HIGH / pressed-LOW. connectDigitalInputsToMcu then drives
// the guest IDR from the solve. Leaving outputs out keeps STM32 LED rendering
// exactly as before.
const isStm32 = isStm32BoardKind(boardKind);
if (isStm32) {
for (const pinName of pinNames) {
const linear = stm32PinNameToLinear(pinName);
if (linear < 0 || outputPins.has(linear)) continue;
const pull = pm.getPinPull(linear);
if (pull !== 0) result[pinName] = { type: 'input', pull };
}
return result;
}
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 };
continue;
}
if (outputPins.has(arduinoPin)) {
// The MCU has actually driven this pin at least once
// (digitalWrite / PWM / port-listener fire), so emit a V-source
// for NetlistBuilder. MixedModeScheduler.onMcuPinChange will
// alterSource() on each subsequent edge; the V-source needs to
// exist in the netlist for the alter to bind (otherwise it's a
// silent no-op and the LED never updates).
result[pinName] = {
type: 'digital',
v: pm.getPinState(arduinoPin) ? vcc : 0,
};
} else {
// Not driven by the MCU. If the firmware enabled an internal pull
// (INPUT_PULLUP / INPUT_PULLDOWN), surface it so NetlistBuilder can
// stamp the weak resistor — without it a button-to-GND input floats
// to 0 V and reads LOW even at idle, so active-low buttons never work.
// For pull-less inputs leave the net free: external components (sensor
// divider, pull-up, button, etc.) drive the SPICE node, and an
// unconditional 0 V source would short out any analog sensor on the pin.
const pull = pm.getPinPull(arduinoPin);
if (pull !== 0) result[pinName] = { type: 'input', pull };
}
}
return result;
}