349 lines
15 KiB
TypeScript
349 lines
15 KiB
TypeScript
|
|
/**
|
|||
|
|
* PinTrace — "which board pin is this component pin wired to?"
|
|||
|
|
*
|
|||
|
|
* The wire graph is not a chain of point-to-point links: a NET is every pin
|
|||
|
|
* the wires transitively join, and a component pin can carry more than one
|
|||
|
|
* wire. This walk answers, for one (component, pin), the board GPIO that owns
|
|||
|
|
* its net — tracing through 2-terminal passives (a resistor in series is
|
|||
|
|
* electrically transparent to the digital layer), through breadboard strips,
|
|||
|
|
* and through a socket a board is seated on.
|
|||
|
|
*
|
|||
|
|
* It used to live inside components/DynamicComponent.tsx. It moved here so the
|
|||
|
|
* store can use the SAME answer when it pre-registers backend sensors: two
|
|||
|
|
* copies of "find the board pin" that disagreed is exactly how an HC-SR04
|
|||
|
|
* behind a level-shifting divider ended up with an echo pin nobody had wired
|
|||
|
|
* (see the sensor pre-registration in useSimulatorStore.startBoard).
|
|||
|
|
*
|
|||
|
|
* Dependency-light on purpose (no React, no store VALUE import) so both the
|
|||
|
|
* renderer and the store can import it.
|
|||
|
|
*/
|
|||
|
|
import type { useSimulatorStore } from '../store/useSimulatorStore';
|
|||
|
|
import { isBoardComponent, boardPinToNumber } from '../utils/boardPinMapping';
|
|||
|
|
import { isBoardSeated } from '../utils/socketSnap';
|
|||
|
|
import { isActiveDevice } from './PinResolver';
|
|||
|
|
import { breadboardGroupKey } from '../utils/breadboardNets';
|
|||
|
|
import { syntheticChipPin, SYNTHETIC_CHIP_PIN_BASE } from './customChips/syntheticPins';
|
|||
|
|
import { resolveChipNetKey } from './customChips/chipNets';
|
|||
|
|
|
|||
|
|
// Map metadataId → [pinA, pinB] for 2-terminal passives.
|
|||
|
|
// "Tracing through" means: if the caller arrived on pinA, continue from pinB
|
|||
|
|
// (and vice-versa).
|
|||
|
|
//
|
|||
|
|
// NOTE: diodes / transistors / op-amps are NOT traced through as passives —
|
|||
|
|
// they have polarity / Vf / non-linear behaviour that the digital layer
|
|||
|
|
// cannot interpret as "same pin". BJTs are an explicit shortcut for the
|
|||
|
|
// canonical "Arduino digital pin controls a load via transistor" pattern so
|
|||
|
|
// 7-segment multiplex circuits with BJT digit drivers still resolve.
|
|||
|
|
const PASSIVE_PIN_PAIRS_BASE: Record<string, [string, string]> = {
|
|||
|
|
resistor: ['1', '2'],
|
|||
|
|
'resistor-us': ['1', '2'],
|
|||
|
|
capacitor: ['1', '2'],
|
|||
|
|
'capacitor-electrolytic': ['+', '−'],
|
|||
|
|
inductor: ['1', '2'],
|
|||
|
|
'analog-resistor': ['A', 'B'],
|
|||
|
|
'analog-capacitor': ['A', 'B'],
|
|||
|
|
'analog-inductor': ['A', 'B'],
|
|||
|
|
'bjt-2n2222': ['C', 'B'],
|
|||
|
|
'bjt-bc547': ['C', 'B'],
|
|||
|
|
'bjt-2n3055': ['C', 'B'],
|
|||
|
|
'bjt-2n3906': ['C', 'B'],
|
|||
|
|
'bjt-bc557': ['C', 'B'],
|
|||
|
|
};
|
|||
|
|
// Preset variants of the generic passives share their parent's tag and pin
|
|||
|
|
// layout. Mirrors the PASSIVE_PRESETS map in spice/componentToSpice.ts.
|
|||
|
|
const PRESET_TO_BASE: Record<string, string> = {
|
|||
|
|
'resistor-220': 'resistor',
|
|||
|
|
'resistor-330': 'resistor',
|
|||
|
|
'resistor-470': 'resistor',
|
|||
|
|
'resistor-1k': 'resistor',
|
|||
|
|
'resistor-2k2': 'resistor',
|
|||
|
|
'resistor-4k7': 'resistor',
|
|||
|
|
'resistor-10k': 'resistor',
|
|||
|
|
'resistor-22k': 'resistor',
|
|||
|
|
'resistor-47k': 'resistor',
|
|||
|
|
'resistor-100k': 'resistor',
|
|||
|
|
'resistor-1m': 'resistor',
|
|||
|
|
'cap-10p': 'capacitor',
|
|||
|
|
'cap-22p': 'capacitor',
|
|||
|
|
'cap-100p': 'capacitor',
|
|||
|
|
'cap-1n': 'capacitor',
|
|||
|
|
'cap-10n': 'capacitor',
|
|||
|
|
'cap-100n': 'capacitor',
|
|||
|
|
'cap-1u': 'capacitor',
|
|||
|
|
'cap-elec-1u': 'capacitor-electrolytic',
|
|||
|
|
'cap-elec-10u': 'capacitor-electrolytic',
|
|||
|
|
'cap-elec-47u': 'capacitor-electrolytic',
|
|||
|
|
'cap-elec-100u': 'capacitor-electrolytic',
|
|||
|
|
'cap-elec-470u': 'capacitor-electrolytic',
|
|||
|
|
'cap-elec-1000u': 'capacitor-electrolytic',
|
|||
|
|
'ind-100u': 'inductor',
|
|||
|
|
'ind-1m': 'inductor',
|
|||
|
|
'ind-10m': 'inductor',
|
|||
|
|
};
|
|||
|
|
export const PASSIVE_PIN_PAIRS: Record<string, [string, string]> = {
|
|||
|
|
...PASSIVE_PIN_PAIRS_BASE,
|
|||
|
|
};
|
|||
|
|
for (const [preset, base] of Object.entries(PRESET_TO_BASE)) {
|
|||
|
|
PASSIVE_PIN_PAIRS[preset] = PASSIVE_PIN_PAIRS_BASE[base];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export type TraceState = ReturnType<typeof useSimulatorStore.getState>;
|
|||
|
|
|
|||
|
|
export interface TraceResult {
|
|||
|
|
/** >=0 board pin (or a synthetic chip pin), -1 supply/GND pad, null unreached. */
|
|||
|
|
arduinoPin: number | null;
|
|||
|
|
/** True iff the trace passed through a BJT/MOSFET/op-amp/diode. */
|
|||
|
|
crossedActiveDevice: boolean;
|
|||
|
|
/** Id of the board the pin belongs to, when one was reached. */
|
|||
|
|
boardId?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** A driven pin — a real GPIO or a custom-chip pin — is a stronger answer than
|
|||
|
|
* a supply/GND pad (-1), which only says "this net is tied to a rail". */
|
|||
|
|
function isDrivenPin(pin: number | null): pin is number {
|
|||
|
|
return pin !== null && pin >= 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Resolve a component pad through a SEATED board rather than a wire.
|
|||
|
|
*
|
|||
|
|
* Sockets are a real electrical connection with nothing to draw: the board's
|
|||
|
|
* pads sit on the component's pads. `boardSocket` (read off the element, the
|
|||
|
|
* rule-6a way) says the component is a socket; isBoardSeated says a board is
|
|||
|
|
* actually in it; and the shared pad NAME is the contract that makes the two
|
|||
|
|
* grids one net — which is exactly why a socket's pinInfo uses the board's own
|
|||
|
|
* names. Returns null for anything that is not a seated socket pad.
|
|||
|
|
*/
|
|||
|
|
function traceThroughSocket(
|
|||
|
|
state: TraceState,
|
|||
|
|
componentId: string,
|
|||
|
|
pinName: string,
|
|||
|
|
): { pin: number; boardId: string } | null {
|
|||
|
|
if (typeof document === 'undefined') return null;
|
|||
|
|
const el = document.getElementById(componentId) as
|
|||
|
|
| (HTMLElement & { boardSocket?: { anchorPin: string; accepts: string[] } })
|
|||
|
|
| null;
|
|||
|
|
const sock = el?.boardSocket;
|
|||
|
|
if (!sock || !Array.isArray(sock.accepts)) return null;
|
|||
|
|
for (const b of state.boards) {
|
|||
|
|
if (!sock.accepts.some((prefix) => b.boardKind.startsWith(prefix))) continue;
|
|||
|
|
if (!isBoardSeated(b.id, b.boardKind, b.x, b.y, state.components)) continue;
|
|||
|
|
const pin = boardPinToNumber(b.boardKind, pinName);
|
|||
|
|
if (pin !== null) return { pin, boardId: b.id };
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Depth-limited walk: trace from (fromId, fromPin) through wires, traversing
|
|||
|
|
// through passive components to reach a board pin. Returns the arduino pin
|
|||
|
|
// plus a `crossedActiveDevice` flag so the resolver factory can decide
|
|||
|
|
// between digital fast-path and SPICE-resolved per-pin.
|
|||
|
|
//
|
|||
|
|
// A real board pin always wins (digital GPIO semantics are unchanged). Only
|
|||
|
|
// when NO board pin is reachable do we fall back to a custom-chip pin on the
|
|||
|
|
// net — either a neighbour chip pin, or (when the trace itself started at a
|
|||
|
|
// chip pin) the starting chip pin — resolving it to its synthetic number.
|
|||
|
|
export function traceDetailed(
|
|||
|
|
state: TraceState,
|
|||
|
|
fromId: string,
|
|||
|
|
fromPin: string,
|
|||
|
|
depth: number,
|
|||
|
|
activeSeen = false,
|
|||
|
|
visited?: Set<string>,
|
|||
|
|
): TraceResult {
|
|||
|
|
if (depth > 6) return { arduinoPin: null, crossedActiveDevice: activeSeen };
|
|||
|
|
|
|||
|
|
// The ROOT call is the only one allowed to mint a chip-net / own-chip-pin
|
|||
|
|
// fallback (see the rules at the bottom) — the same guard `depth === 0`
|
|||
|
|
// gave before same-node hops existed, which recurse at the SAME depth.
|
|||
|
|
const isRoot = visited === undefined;
|
|||
|
|
// A net is walked, not a chain: the tap of a divider carries the wire to
|
|||
|
|
// the MCU *and* the wire to the lower resistor, and two pins of one net
|
|||
|
|
// would otherwise hand each other back and forth forever.
|
|||
|
|
const seen = visited ?? new Set<string>();
|
|||
|
|
const selfKey = `${fromId} |