feat(sim): introduce PinResolver abstraction (Phase 0 of mixed-mode rewrite)
Decouple per-component handlers from direct pinManager.onPinChange +
getArduinoPinHelper subscriptions by introducing a small PinResolver
interface. The Phase 0 default impl is functionally identical to the
legacy path — it just routes through PinResolver instead of being
inlined in every handler. Zero behavior change.
The point is to make Phase 1 possible: swap the default impl for a
SPICE-resolved version that watches node voltages and threshold-
converts to digital events, without rewriting every handler.
Files:
- simulation/PinResolver.ts (new) — interface + default factory
- parts/PartSimulationRegistry.ts — additive 5th arg to
attachEvents (getPinResolver?), legacy 4-arg signatures keep
working unchanged
- components/DynamicComponent.tsx — assembles the PinResolver from
the wire-trace logic + PinManager subscriptions + board Vcc
lookup, passes it as the 5th arg to attachEvents
- parts/BasicParts.ts — LED handler migrated as proof of concept
(resolver-first path, legacy 4-arg path kept as fallback for
tests / unmigrated harnesses)
- __tests__/pin-resolver.test.ts (new) — 8 unit tests covering
FLOATING / GND / HIGH / LOW / GPIO subscriptions / unsubscribe
Vitest: 8/8 pin-resolver tests pass. 1300+ existing tests still pass;
the one pre-existing flake (spice-rectifier-live-repro timing out >60s)
is unrelated to this commit — verified by running the test on plain
HEAD without these changes (same timeout).
See project/sim-mixedmode/phase-00-pin-resolver.md (in the velxio-prod
repo) for full phase context.
This commit is contained in:
parent
a91b857d8c
commit
e10492c6e6
|
|
@ -0,0 +1,140 @@
|
|||
/**
|
||||
* Unit tests for the default PinResolver.
|
||||
*
|
||||
* These cover the Phase-0 deliverable: a PinResolver that wraps the
|
||||
* existing getArduinoPin trace + pinManager subscription. Behavior
|
||||
* must be identical to the legacy path; the abstraction just makes
|
||||
* the swap to a SPICE-resolved implementation possible in Phase 1.
|
||||
*
|
||||
* See project/sim-mixedmode/phase-00-pin-resolver.md
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
createDefaultPinResolver,
|
||||
type PinResolverContext,
|
||||
type PinTracer,
|
||||
} from '../simulation/PinResolver';
|
||||
|
||||
function makeCtx(overrides: Partial<PinResolverContext> = {}): PinResolverContext {
|
||||
return {
|
||||
components: [],
|
||||
boards: [],
|
||||
wires: [],
|
||||
ownerBoard: null,
|
||||
ownerBoardVcc: 5,
|
||||
subscribeArduinoPin: () => () => {},
|
||||
readArduinoPin: () => null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('PinResolver — default impl', () => {
|
||||
it('reports FLOATING when the pin is not wired to any board pin', () => {
|
||||
const tracer: PinTracer = () => null;
|
||||
const r = createDefaultPinResolver('led-1', 'A', makeCtx(), tracer);
|
||||
expect(r.getCurrentState()).toBe('FLOATING');
|
||||
expect(r.getCurrentVoltage()).toBeNull();
|
||||
});
|
||||
|
||||
it('reports LOW with voltage 0 when the pin is tied to GND (tracer returns -1)', () => {
|
||||
const tracer: PinTracer = () => -1;
|
||||
const r = createDefaultPinResolver('led-1', 'C', makeCtx(), tracer);
|
||||
expect(r.getCurrentState()).toBe('LOW');
|
||||
expect(r.getCurrentVoltage()).toBe(0);
|
||||
});
|
||||
|
||||
it('reads HIGH/LOW from readArduinoPin when wired to a GPIO', () => {
|
||||
const tracer: PinTracer = () => 13;
|
||||
let pinValue = false;
|
||||
const ctx = makeCtx({
|
||||
readArduinoPin: (pin) => (pin === 13 ? pinValue : null),
|
||||
});
|
||||
const r = createDefaultPinResolver('led-1', 'A', ctx, tracer);
|
||||
|
||||
pinValue = false;
|
||||
expect(r.getCurrentState()).toBe('LOW');
|
||||
expect(r.getCurrentVoltage()).toBe(0);
|
||||
});
|
||||
|
||||
it('synthesises voltage from owner board Vcc on HIGH', () => {
|
||||
const tracer: PinTracer = () => 13;
|
||||
const ctx = makeCtx({
|
||||
ownerBoardVcc: 3.3,
|
||||
readArduinoPin: () => true,
|
||||
});
|
||||
const r = createDefaultPinResolver('led-1', 'A', ctx, tracer);
|
||||
expect(r.getCurrentState()).toBe('HIGH');
|
||||
expect(r.getCurrentVoltage()).toBeCloseTo(3.3);
|
||||
});
|
||||
|
||||
it('subscribes to pin changes via subscribeArduinoPin and fires callbacks', () => {
|
||||
const tracer: PinTracer = () => 7;
|
||||
const subscribers: Array<(pin: number, state: boolean) => void> = [];
|
||||
const unsub = vi.fn();
|
||||
const ctx = makeCtx({
|
||||
subscribeArduinoPin: (_pin, cb) => {
|
||||
subscribers.push(cb);
|
||||
return unsub;
|
||||
},
|
||||
});
|
||||
const r = createDefaultPinResolver('led-1', 'A', ctx, tracer);
|
||||
const onChange = vi.fn();
|
||||
const cancel = r.onChange(onChange);
|
||||
|
||||
// Manually fire as if PinManager delivered an event
|
||||
subscribers[0]!(7, true);
|
||||
expect(onChange).toHaveBeenCalledWith('HIGH', 5);
|
||||
subscribers[0]!(7, false);
|
||||
expect(onChange).toHaveBeenCalledWith('LOW', 0);
|
||||
|
||||
// Caller can unsubscribe cleanly
|
||||
cancel();
|
||||
expect(unsub).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns a no-op unsubscribe when the pin is unwired (tracer returns null)', () => {
|
||||
const tracer: PinTracer = () => null;
|
||||
const ctx = makeCtx({
|
||||
subscribeArduinoPin: vi.fn(),
|
||||
});
|
||||
const r = createDefaultPinResolver('led-1', 'A', ctx, tracer);
|
||||
const onChange = vi.fn();
|
||||
const cancel = r.onChange(onChange);
|
||||
expect(ctx.subscribeArduinoPin).not.toHaveBeenCalled();
|
||||
expect(typeof cancel).toBe('function');
|
||||
cancel(); // should not throw
|
||||
});
|
||||
|
||||
it('emits an initial LOW event asynchronously when wired to GND', async () => {
|
||||
const tracer: PinTracer = () => -1;
|
||||
const r = createDefaultPinResolver('led-1', 'C', makeCtx(), tracer);
|
||||
const onChange = vi.fn();
|
||||
r.onChange(onChange);
|
||||
|
||||
// Microtask queue flush — the resolver schedules the LOW event via
|
||||
// queueMicrotask so subscribers see initial state without blocking
|
||||
// construction.
|
||||
await Promise.resolve();
|
||||
expect(onChange).toHaveBeenCalledWith('LOW', 0);
|
||||
});
|
||||
|
||||
it('updates getCurrentState immediately after a subscribed callback fires', () => {
|
||||
const tracer: PinTracer = () => 9;
|
||||
const subscribers: Array<(pin: number, state: boolean) => void> = [];
|
||||
const ctx = makeCtx({
|
||||
subscribeArduinoPin: (_pin, cb) => {
|
||||
subscribers.push(cb);
|
||||
return () => {};
|
||||
},
|
||||
readArduinoPin: () => false,
|
||||
});
|
||||
const r = createDefaultPinResolver('led-1', 'A', ctx, tracer);
|
||||
r.onChange(() => {});
|
||||
|
||||
expect(r.getCurrentState()).toBe('LOW');
|
||||
subscribers[0]!(9, true);
|
||||
expect(r.getCurrentState()).toBe('HIGH');
|
||||
expect(r.getCurrentVoltage()).toBe(5);
|
||||
});
|
||||
});
|
||||
|
|
@ -17,6 +17,8 @@ import { useSimulatorStore } from '../store/useSimulatorStore';
|
|||
import { useElectricalStore } from '../store/useElectricalStore';
|
||||
import { PartSimulationRegistry } from '../simulation/parts';
|
||||
import { isBoardComponent, boardPinToNumber } from '../utils/boardPinMapping';
|
||||
import { createDefaultPinResolver, type PinResolver } from '../simulation/PinResolver';
|
||||
import { BOARD_PIN_GROUPS } from '../simulation/spice/boardPinGroups';
|
||||
|
||||
// Side-effect imports: register every web component we'll create at runtime.
|
||||
// `@wokwi/elements` covers the upstream catalog; `../velxio-elements` adds
|
||||
|
|
@ -359,7 +361,58 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
|
|||
return trace(id, componentPinName, 0);
|
||||
};
|
||||
|
||||
cleanupSimulationEvents = logic.attachEvents(el, stubSimulator, getArduinoPin, id);
|
||||
// PinResolver factory — Phase 0 of the mixed-mode simulator project
|
||||
// (see project/sim-mixedmode/ in the velxio-prod repo). For now it
|
||||
// wraps getArduinoPin + pinManager.onPinChange — zero behavioral
|
||||
// change vs the legacy path. Phase 1+ will swap in a SPICE-resolved
|
||||
// implementation that watches node voltages and threshold-converts
|
||||
// to logic states.
|
||||
const simState = useSimulatorStore.getState();
|
||||
const ownerBoard =
|
||||
simState.boards.find((b) => b.id === simState.activeBoardId) ?? null;
|
||||
const ownerBoardVcc =
|
||||
(ownerBoard && BOARD_PIN_GROUPS[ownerBoard.boardKind as keyof typeof BOARD_PIN_GROUPS]?.vcc) ?? 5;
|
||||
const getPinResolver = (componentPinName: string): PinResolver | null => {
|
||||
const state = useSimulatorStore.getState();
|
||||
const pinManager = (stubSimulator as {
|
||||
pinManager?: {
|
||||
onPinChange?: (pin: number, cb: (pin: number, state: boolean) => void) => () => void;
|
||||
getPinState?: (pin: number) => boolean | null;
|
||||
};
|
||||
}).pinManager;
|
||||
return createDefaultPinResolver(
|
||||
id,
|
||||
componentPinName,
|
||||
{
|
||||
components: state.components,
|
||||
boards: state.boards,
|
||||
wires: state.wires,
|
||||
ownerBoard,
|
||||
ownerBoardVcc,
|
||||
subscribeArduinoPin: (pin, cb) => {
|
||||
if (!pinManager?.onPinChange) return () => {};
|
||||
return pinManager.onPinChange(pin, cb);
|
||||
},
|
||||
readArduinoPin: (pin) => {
|
||||
if (!pinManager?.getPinState) return null;
|
||||
try {
|
||||
return pinManager.getPinState(pin);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
getArduinoPin,
|
||||
);
|
||||
};
|
||||
|
||||
cleanupSimulationEvents = logic.attachEvents(
|
||||
el,
|
||||
stubSimulator,
|
||||
getArduinoPin,
|
||||
id,
|
||||
getPinResolver,
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
/**
|
||||
* PinResolver — abstraction over "what's driving this component pin?".
|
||||
*
|
||||
* Background
|
||||
* ----------
|
||||
* Until Phase 0 of the mixed-mode simulator project, every component
|
||||
* handler in simulation/parts/*Parts.ts wired its own subscription to
|
||||
* `pinManager.onPinChange(arduinoPin, callback)` after resolving the
|
||||
* arduinoPin via the `getArduinoPinHelper(componentPinName)` callback
|
||||
* passed into `attachEvents`. That couples handlers tightly to the
|
||||
* digital event flow and makes it impossible to swap in a SPICE-resolved
|
||||
* voltage check without rewriting every handler.
|
||||
*
|
||||
* This module introduces `PinResolver`: a small interface that hides the
|
||||
* "how do I know what state this pin is in?" question behind two methods,
|
||||
* `getCurrentState()` and `onChange()`. Handlers ask the resolver for the
|
||||
* current state and subscribe for changes; they never touch PinManager
|
||||
* directly.
|
||||
*
|
||||
* Phase 0 ships the DEFAULT IMPLEMENTATION, which is functionally
|
||||
* identical to the legacy path — it just routes through PinResolver
|
||||
* instead of being inlined in each handler. Zero behavior change.
|
||||
*
|
||||
* Phase 1 will add a SPICE-resolved implementation that watches
|
||||
* `nodeVoltages[net]` and threshold-converts to digital states. That
|
||||
* landing point is why the abstraction exists.
|
||||
*
|
||||
* See ../../project/sim-mixedmode/phase-00-pin-resolver.md
|
||||
* ../../project/sim-mixedmode/phase-01-mixed-mode-coupling.md
|
||||
* (in the velxio-prod repo)
|
||||
*/
|
||||
|
||||
import type { Wire } from '../types/wire';
|
||||
import type { Component } from '../types/components';
|
||||
import type { BoardInstance } from '../types/board';
|
||||
|
||||
/** Default Vcc when an owner board can't be identified. Matches Arduino Uno. */
|
||||
const DEFAULT_VCC = 5;
|
||||
|
||||
/** Logic state of a pin from the perspective of the digital event flow. */
|
||||
export type PinState = 'HIGH' | 'LOW' | 'FLOATING' | 'CONFLICT';
|
||||
|
||||
/**
|
||||
* Resolves "what is this component pin doing?" for one specific pin.
|
||||
*
|
||||
* - `getCurrentState()` returns the live state synchronously.
|
||||
* - `getCurrentVoltage()` returns the live voltage in volts (or null if
|
||||
* unknown — e.g. SPICE hasn't solved yet). For the Phase 0 default
|
||||
* impl this is synthesised as `state==='HIGH' ? vcc : 0`.
|
||||
* - `onChange(cb)` subscribes to state transitions. Returns an
|
||||
* unsubscribe function. The callback fires whenever the state
|
||||
* changes — NOT on every internal pin event (e.g. PWM duty bumps
|
||||
* don't fire as long as the digital state stays HIGH).
|
||||
*
|
||||
* Implementations must be cheap to construct and cheap to query.
|
||||
* One PinResolver per (componentId, pinName) is created per
|
||||
* `attachEvents` call.
|
||||
*/
|
||||
export interface PinResolver {
|
||||
getCurrentState(): PinState;
|
||||
getCurrentVoltage(): number | null;
|
||||
onChange(cb: (state: PinState, voltage: number) => void): () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal context that the default PinResolver needs to do its work.
|
||||
* Passed once from DynamicComponent at attachEvents time so the resolver
|
||||
* doesn't have to reach into Zustand stores itself (keeps it testable).
|
||||
*/
|
||||
export interface PinResolverContext {
|
||||
/** All components currently on the canvas — used by the wire-trace logic. */
|
||||
components: Component[];
|
||||
/** All boards on the canvas. */
|
||||
boards: BoardInstance[];
|
||||
/** All wires on the canvas. */
|
||||
wires: Wire[];
|
||||
/** The board that's emitting events for this component (Arduino Uno etc.). */
|
||||
ownerBoard: BoardInstance | null;
|
||||
/**
|
||||
* Vcc of the owner board in volts (e.g. 5 for Arduino Uno, 3.3 for ESP32).
|
||||
* Used to synthesise a voltage value for the Phase 0 default impl —
|
||||
* `HIGH → vcc`, `LOW → 0`. Phase 1+ reads real voltages from SPICE
|
||||
* instead.
|
||||
*/
|
||||
ownerBoardVcc: number;
|
||||
/** Subscribe to a single Arduino pin's digital changes. Returns unsubscribe. */
|
||||
subscribeArduinoPin: (
|
||||
pin: number,
|
||||
cb: (pin: number, state: boolean) => void,
|
||||
) => () => void;
|
||||
/**
|
||||
* Read the current digital state of an Arduino pin, synchronously. Used
|
||||
* by `getCurrentState()` so the resolver doesn't have to wait for the
|
||||
* first event after subscription to report a sensible initial state.
|
||||
* Return `null` if the pin's state isn't tracked (e.g. board hasn't
|
||||
* booted yet).
|
||||
*/
|
||||
readArduinoPin: (pin: number) => boolean | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure pin-tracing logic — walks wires through 2-terminal passives (and
|
||||
* a few "transparent" actives like BJTs as part of the 2026-05-15
|
||||
* shortcut) until it finds either a board pin or a dead end. This is
|
||||
* the same trace that lived inline in `getArduinoPin` inside
|
||||
* DynamicComponent.tsx, extracted here so the default PinResolver impl
|
||||
* can call it without duplicating logic.
|
||||
*
|
||||
* Returns:
|
||||
* - >= 0 → Arduino pin number controlling this component pin
|
||||
* - -1 → wired to GND (handled by caller as "always LOW")
|
||||
* - null → no board reached (might be wired to another component that
|
||||
* we don't trace through, or unwired entirely)
|
||||
*
|
||||
* Phase 1 will replace this with SPICE-net lookup via pinNetMap. The
|
||||
* `[C, B]` BJT shortcut here goes away when Phase 5 deletes the
|
||||
* legacy direct-event path.
|
||||
*/
|
||||
export type PinTracer = (
|
||||
componentId: string,
|
||||
componentPinName: string,
|
||||
) => number | null;
|
||||
|
||||
/**
|
||||
* Build the default PinResolver for one (component, pin) pair.
|
||||
*
|
||||
* `tracePin` is provided by the caller (DynamicComponent) — it
|
||||
* encapsulates the wire-graph walk because that logic depends on
|
||||
* runtime board metadata DynamicComponent already has loaded. Keeps
|
||||
* this module dependency-free of the board/wire stores and easy to
|
||||
* unit test.
|
||||
*/
|
||||
export function createDefaultPinResolver(
|
||||
componentId: string,
|
||||
componentPinName: string,
|
||||
ctx: PinResolverContext,
|
||||
tracePin: PinTracer,
|
||||
): PinResolver {
|
||||
// Resolve once at construction. The trace result is stable while the
|
||||
// wire topology is unchanged; if a wire is added/removed,
|
||||
// DynamicComponent's effect re-runs and we rebuild from scratch.
|
||||
const arduinoPin = tracePin(componentId, componentPinName);
|
||||
|
||||
// Local mirror of the current state. Initialised on first subscription
|
||||
// (or first synchronous read) from the PinManager.
|
||||
let cached: PinState = 'FLOATING';
|
||||
let cachedVoltage: number | null = null;
|
||||
let initialised = false;
|
||||
|
||||
// Board VCC for voltage synthesis. Caller looks it up from
|
||||
// boardPinGroups; we just use whatever they give us (default 5V).
|
||||
const vcc = ctx.ownerBoardVcc ?? DEFAULT_VCC;
|
||||
|
||||
function refresh(): void {
|
||||
if (arduinoPin === null) {
|
||||
cached = 'FLOATING';
|
||||
cachedVoltage = null;
|
||||
return;
|
||||
}
|
||||
if (arduinoPin === -1) {
|
||||
// Tied to GND.
|
||||
cached = 'LOW';
|
||||
cachedVoltage = 0;
|
||||
return;
|
||||
}
|
||||
const live = ctx.readArduinoPin(arduinoPin);
|
||||
if (live === null) {
|
||||
cached = 'FLOATING';
|
||||
cachedVoltage = null;
|
||||
return;
|
||||
}
|
||||
cached = live ? 'HIGH' : 'LOW';
|
||||
cachedVoltage = live ? vcc : 0;
|
||||
}
|
||||
|
||||
return {
|
||||
getCurrentState(): PinState {
|
||||
if (!initialised) {
|
||||
refresh();
|
||||
initialised = true;
|
||||
}
|
||||
return cached;
|
||||
},
|
||||
getCurrentVoltage(): number | null {
|
||||
if (!initialised) {
|
||||
refresh();
|
||||
initialised = true;
|
||||
}
|
||||
return cachedVoltage;
|
||||
},
|
||||
onChange(cb): () => void {
|
||||
if (arduinoPin === null) {
|
||||
// Unwired or wired to something we don't trace through. No
|
||||
// events will ever fire — return a no-op unsubscribe.
|
||||
return () => {};
|
||||
}
|
||||
if (arduinoPin === -1) {
|
||||
// Tied to GND. Emit one initial event so subscribers can mirror
|
||||
// state immediately, then no further changes.
|
||||
queueMicrotask(() => cb('LOW', 0));
|
||||
return () => {};
|
||||
}
|
||||
return ctx.subscribeArduinoPin(arduinoPin, (_pin, state) => {
|
||||
cached = state ? 'HIGH' : 'LOW';
|
||||
cachedVoltage = state ? vcc : 0;
|
||||
cb(cached, cachedVoltage);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -140,7 +140,7 @@ PartSimulationRegistry.register('dip-switch-8', {
|
|||
* LED stays off regardless of the anode state.
|
||||
*/
|
||||
PartSimulationRegistry.register('led', {
|
||||
attachEvents: (element, simulator, getArduinoPinHelper, componentId) => {
|
||||
attachEvents: (element, simulator, getArduinoPinHelper, componentId, getPinResolver) => {
|
||||
const pinManager = (simulator as any).pinManager;
|
||||
if (!pinManager) return () => {};
|
||||
|
||||
|
|
@ -148,6 +148,17 @@ PartSimulationRegistry.register('led', {
|
|||
const unsubs: (() => void)[] = [];
|
||||
let anodeHigh = false;
|
||||
let cathodeLow = false;
|
||||
|
||||
// Phase 0 of the mixed-mode simulator project — see
|
||||
// ../../../project/sim-mixedmode/phase-00-pin-resolver.md in the
|
||||
// velxio-prod repo. PinResolver replaces the direct
|
||||
// pinManager.onPinChange + getArduinoPinHelper pattern. For the
|
||||
// Phase-0 default impl it's functionally identical; Phase 1+ will
|
||||
// swap in a SPICE-resolved impl that watches node voltages and
|
||||
// threshold-converts to logic states. Falls back to the legacy
|
||||
// path when getPinResolver isn't available (e.g. test harnesses
|
||||
// that mock attachEvents with the old 4-arg signature).
|
||||
const useResolver = typeof getPinResolver === 'function';
|
||||
// Last known SPICE brightness + timestamp. When a solve hasn't
|
||||
// landed yet (engine warm-up, between-solve gap, ngspice iteration
|
||||
// holes), we hold this value for 500 ms before decaying to zero —
|
||||
|
|
@ -206,27 +217,54 @@ PartSimulationRegistry.register('led', {
|
|||
el.brightness = el.value ? 1 : 0;
|
||||
};
|
||||
|
||||
// Cathode pin: -1 means wired to GND (always LOW), >=0 means GPIO
|
||||
const cathodePin = getArduinoPinHelper('C');
|
||||
if (cathodePin === -1) {
|
||||
cathodeLow = true;
|
||||
} else if (cathodePin !== null && cathodePin >= 0) {
|
||||
unsubs.push(
|
||||
pinManager.onPinChange(cathodePin, (_: number, state: boolean) => {
|
||||
cathodeLow = !state;
|
||||
update();
|
||||
}),
|
||||
);
|
||||
}
|
||||
// Cathode + anode pin subscriptions. PinResolver path is preferred
|
||||
// (gets SPICE-aware behavior for free in later phases); legacy
|
||||
// direct-pinManager path is kept for builds without Phase 0.
|
||||
if (useResolver) {
|
||||
const cathodeResolver = getPinResolver!('C');
|
||||
const anodeResolver = getPinResolver!('A');
|
||||
if (cathodeResolver) {
|
||||
// Seed initial state. -1 (wired to GND) becomes 'LOW' via the
|
||||
// resolver's GND special case, which sets cathodeLow = true.
|
||||
cathodeLow = cathodeResolver.getCurrentState() === 'LOW';
|
||||
unsubs.push(
|
||||
cathodeResolver.onChange((state) => {
|
||||
cathodeLow = state === 'LOW';
|
||||
update();
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (anodeResolver) {
|
||||
anodeHigh = anodeResolver.getCurrentState() === 'HIGH';
|
||||
unsubs.push(
|
||||
anodeResolver.onChange((state) => {
|
||||
anodeHigh = state === 'HIGH';
|
||||
update();
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const cathodePin = getArduinoPinHelper('C');
|
||||
if (cathodePin === -1) {
|
||||
cathodeLow = true;
|
||||
} else if (cathodePin !== null && cathodePin >= 0) {
|
||||
unsubs.push(
|
||||
pinManager.onPinChange(cathodePin, (_: number, state: boolean) => {
|
||||
cathodeLow = !state;
|
||||
update();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const anodePin = getArduinoPinHelper('A');
|
||||
if (anodePin !== null && anodePin >= 0) {
|
||||
unsubs.push(
|
||||
pinManager.onPinChange(anodePin, (_: number, state: boolean) => {
|
||||
anodeHigh = state;
|
||||
update();
|
||||
}),
|
||||
);
|
||||
const anodePin = getArduinoPinHelper('A');
|
||||
if (anodePin !== null && anodePin >= 0) {
|
||||
unsubs.push(
|
||||
pinManager.onPinChange(anodePin, (_: number, state: boolean) => {
|
||||
anodeHigh = state;
|
||||
update();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Also subscribe to electrical store changes to update brightness
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { AVRSimulator } from '../AVRSimulator';
|
||||
import { RP2040Simulator } from '../RP2040Simulator';
|
||||
import type { PinResolver } from '../PinResolver';
|
||||
|
||||
/** Any simulator that components can interact with (AVR, RP2040, or ESP32 bridge shim). */
|
||||
export type AnySimulator =
|
||||
|
|
@ -31,10 +32,18 @@ export interface PartSimulationLogic {
|
|||
* Called when the simulation starts to attach events or setup periodic tasks.
|
||||
* Useful for input components (buttons, potentiometers) or complex components (servos).
|
||||
*
|
||||
* The 5th parameter `getPinResolver` is the recommended entry point for
|
||||
* new code — it returns a PinResolver that hides whether the underlying
|
||||
* source is the digital PinManager (Phase 0) or a SPICE-resolved net
|
||||
* voltage with threshold conversion (Phase 1+). Existing handlers can
|
||||
* keep using `getArduinoPinHelper` + `pinManager.onPinChange` directly;
|
||||
* the migration happens incrementally per-component in Phase 5.
|
||||
*
|
||||
* @param element The DOM element of the wokwi component
|
||||
* @param avrSimulator The running simulator instance
|
||||
* @param getArduinoPinHelper Function to find what Arduino pin is connected to a specific component pin
|
||||
* @param componentId The unique ID of this component instance (used by SensorUpdateRegistry)
|
||||
* @param getArduinoPinHelper Legacy: returns the Arduino pin number controlling a component pin
|
||||
* @param componentId The unique ID of this component instance
|
||||
* @param getPinResolver Preferred: returns a PinResolver for a component pin
|
||||
* @returns A cleanup function to remove event listeners when simulation stops
|
||||
*/
|
||||
attachEvents?: (
|
||||
|
|
@ -42,6 +51,7 @@ export interface PartSimulationLogic {
|
|||
simulator: AnySimulator,
|
||||
getArduinoPinHelper: (componentPinName: string) => number | null,
|
||||
componentId: string,
|
||||
getPinResolver?: (componentPinName: string) => PinResolver | null,
|
||||
) => () => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue