2026-05-15 20:50:09 +07:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
|
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection
Adds the architecture pieces for mixed-mode coupling without yet
driving the SPICE engine. Components on a path that crosses an active
device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route
through a new SPICE-resolved PinResolver variant; everything else
keeps the digital fast-path from Phase 0.
What ships:
- simulation/PinResolver.ts
* `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES`
list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay).
* `DetailedPinTrace` / `DetailedPinTracer` types — the trace
function now reports whether it crossed an active device, on
top of the Arduino pin number.
* `createSpiceResolvedPinResolver()` — new factory; reads voltages
from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW
with hysteresis (thresholdHigh != thresholdLow → Schmitt-like).
- simulation/spice/MixedModeScheduler.ts (new)
* Singleton orchestrator that holds the NgSpiceInteractive engine
and the SpiceVoltageSource subscription registry.
* `start()` / `stop()` / `dispose()` lifecycle.
* `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource.
* `onMcuPinChange()` placeholder for the alter+tran event loop.
* Skeleton: subscribers register but never receive events yet.
Phase 1b continued will wire NgSpiceInteractive into the loop.
- components/DynamicComponent.tsx
* Trace function extended with `traceDetailed()` that tracks
whether the BFS crossed an active component.
* PinResolver factory branches: active-path → SPICE-resolved (uses
the scheduler), digital-only → existing default impl. Default
threshold = vcc/2 with no hysteresis; Phase 3 will replace with
per-logic-family Vil/Vih.
Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved
path falls back to FLOATING until Phase 1b continued wires the engine.
Tests:
- pin-resolver-phase1b.test.ts (new) — 8/8 passing.
Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator
metadata id; SPICE-resolved resolver state reporting, threshold
conversion, hysteresis dead-band, unsubscribe.
- pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression).
- tsc --noEmit on the new files: clean.
No deploy in this commit — staged for end-of-session rebuild + push
per user preference.
2026-05-15 21:38:30 +07:00
|
|
|
/**
|
|
|
|
|
* Extended tracer that ALSO reports whether the path crossed an active
|
|
|
|
|
* device (BJT, MOSFET, op-amp, diode). Used by the PinResolver factory
|
|
|
|
|
* to decide between digital fast-path (no active devices on the path)
|
|
|
|
|
* and SPICE-resolved (an active device is in series, so binary HIGH/LOW
|
|
|
|
|
* doesn't capture the real signal).
|
|
|
|
|
*
|
|
|
|
|
* `crossedActiveDevice` is set during the trace walk in DynamicComponent;
|
|
|
|
|
* the legacy `PinTracer` API (returns just the number) is preserved for
|
|
|
|
|
* backward compat.
|
|
|
|
|
*/
|
|
|
|
|
export interface DetailedPinTrace {
|
|
|
|
|
/** Same convention as PinTracer: >=0 pin, -1 GND, null unreached. */
|
|
|
|
|
arduinoPin: number | null;
|
|
|
|
|
/** True iff the trace passed through a BJT/MOSFET/op-amp/diode. */
|
|
|
|
|
crossedActiveDevice: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type DetailedPinTracer = (
|
|
|
|
|
componentId: string,
|
|
|
|
|
componentPinName: string,
|
|
|
|
|
) => DetailedPinTrace;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Active-component metadata ids whose presence on a wire path means we
|
|
|
|
|
* should defer to the SPICE solver instead of treating the path as a
|
|
|
|
|
* direct digital connection. Includes every BJT, MOSFET, op-amp, and
|
|
|
|
|
* diode the simulator currently models. Logic ICs (74HC, 4000-series)
|
|
|
|
|
* are intentionally NOT in this list because they have their own
|
|
|
|
|
* dedicated handlers that pre-decode the I2C/SPI/digital protocol —
|
|
|
|
|
* SPICE-routing them would be wasteful and slower.
|
|
|
|
|
*
|
|
|
|
|
* Phase 5 will simplify this once every active component goes through
|
|
|
|
|
* the same SPICE-resolved path uniformly.
|
|
|
|
|
*/
|
|
|
|
|
export const ACTIVE_DEVICE_PREFIXES: readonly string[] = Object.freeze([
|
|
|
|
|
'bjt-',
|
|
|
|
|
'mosfet-',
|
|
|
|
|
'opamp-',
|
|
|
|
|
'diode', // diode, diode-1n4148, etc.
|
|
|
|
|
'led', // technically a diode — current sense uses SPICE
|
|
|
|
|
'relay', // coil drive + contact behavior is electrically active
|
|
|
|
|
'7805', '7812', '7905', 'lm317', // regulators (3-terminal active)
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
export function isActiveDevice(metadataId: string): boolean {
|
|
|
|
|
return ACTIVE_DEVICE_PREFIXES.some((prefix) => metadataId.startsWith(prefix));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 20:50:09 +07:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection
Adds the architecture pieces for mixed-mode coupling without yet
driving the SPICE engine. Components on a path that crosses an active
device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route
through a new SPICE-resolved PinResolver variant; everything else
keeps the digital fast-path from Phase 0.
What ships:
- simulation/PinResolver.ts
* `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES`
list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay).
* `DetailedPinTrace` / `DetailedPinTracer` types — the trace
function now reports whether it crossed an active device, on
top of the Arduino pin number.
* `createSpiceResolvedPinResolver()` — new factory; reads voltages
from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW
with hysteresis (thresholdHigh != thresholdLow → Schmitt-like).
- simulation/spice/MixedModeScheduler.ts (new)
* Singleton orchestrator that holds the NgSpiceInteractive engine
and the SpiceVoltageSource subscription registry.
* `start()` / `stop()` / `dispose()` lifecycle.
* `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource.
* `onMcuPinChange()` placeholder for the alter+tran event loop.
* Skeleton: subscribers register but never receive events yet.
Phase 1b continued will wire NgSpiceInteractive into the loop.
- components/DynamicComponent.tsx
* Trace function extended with `traceDetailed()` that tracks
whether the BFS crossed an active component.
* PinResolver factory branches: active-path → SPICE-resolved (uses
the scheduler), digital-only → existing default impl. Default
threshold = vcc/2 with no hysteresis; Phase 3 will replace with
per-logic-family Vil/Vih.
Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved
path falls back to FLOATING until Phase 1b continued wires the engine.
Tests:
- pin-resolver-phase1b.test.ts (new) — 8/8 passing.
Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator
metadata id; SPICE-resolved resolver state reporting, threshold
conversion, hysteresis dead-band, unsubscribe.
- pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression).
- tsc --noEmit on the new files: clean.
No deploy in this commit — staged for end-of-session rebuild + push
per user preference.
2026-05-15 21:38:30 +07:00
|
|
|
|
|
|
|
|
// ── SPICE-resolved variant (Phase 1b) ─────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Subscriber registry the SPICE-resolved resolver hooks into. Lives in
|
|
|
|
|
* MixedModeScheduler so the scheduler can fan voltage updates out to
|
|
|
|
|
* all interested resolvers.
|
|
|
|
|
*
|
|
|
|
|
* We use this seam type (instead of importing MixedModeScheduler here
|
|
|
|
|
* directly) to keep PinResolver dependency-free of the scheduler.
|
|
|
|
|
* Phase 1b continued can swap in any implementation conforming to this
|
|
|
|
|
* shape, including test mocks.
|
|
|
|
|
*/
|
|
|
|
|
export interface SpiceVoltageSource {
|
|
|
|
|
subscribe(
|
|
|
|
|
componentId: string,
|
|
|
|
|
componentPinName: string,
|
|
|
|
|
cb: (state: PinState, voltage: number) => void,
|
|
|
|
|
): () => void;
|
|
|
|
|
/** Current voltage on the node connecting (componentId, pinName),
|
|
|
|
|
* if SPICE has solved at least once. Null when unknown. */
|
|
|
|
|
getCurrentVoltage(componentId: string, componentPinName: string): number | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface SpiceResolvedConfig {
|
|
|
|
|
/** Vcc/2 by default — the threshold above which a voltage reads HIGH.
|
|
|
|
|
* Phase 3 will replace this with per-logic-family thresholds. */
|
|
|
|
|
thresholdHigh: number;
|
|
|
|
|
/** Below this voltage reads LOW. Hysteresis between low/high prevents
|
|
|
|
|
* oscillation; Phase 3 will refine per Schmitt-trigger inputs. */
|
|
|
|
|
thresholdLow: number;
|
|
|
|
|
/** Vcc — used to synthesise a digital voltage when the resolver
|
|
|
|
|
* reports a logic state synchronously and SPICE hasn't yet solved. */
|
|
|
|
|
vcc: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SPICE-resolved PinResolver — instead of mirroring an Arduino pin's
|
|
|
|
|
* digital state, it watches a SPICE node's voltage and threshold-
|
|
|
|
|
* converts to HIGH/LOW/FLOATING. Used on paths that pass through
|
|
|
|
|
* active devices (BJT, MOSFET, op-amp, diode) where the digital
|
|
|
|
|
* fast-path can't represent the real signal.
|
|
|
|
|
*
|
|
|
|
|
* Phase 1b scaffolding: subscribes to the provided `SpiceVoltageSource`
|
|
|
|
|
* and translates voltage events. When the source doesn't have a
|
|
|
|
|
* voltage yet, the resolver reports `FLOATING` (NOT the legacy "tied to
|
|
|
|
|
* Arduino HIGH/LOW") — that's intentional, because the scheduler hasn't
|
|
|
|
|
* yet wired in the real SPICE engine. After Phase 1b continued, real
|
|
|
|
|
* voltages will flow through this same surface.
|
|
|
|
|
*/
|
|
|
|
|
export function createSpiceResolvedPinResolver(
|
|
|
|
|
componentId: string,
|
|
|
|
|
componentPinName: string,
|
|
|
|
|
voltageSource: SpiceVoltageSource,
|
|
|
|
|
config: SpiceResolvedConfig,
|
|
|
|
|
): PinResolver {
|
|
|
|
|
let cached: PinState = 'FLOATING';
|
|
|
|
|
let cachedVoltage: number | null = null;
|
|
|
|
|
|
|
|
|
|
function voltageToState(v: number | null): PinState {
|
|
|
|
|
if (v === null || !Number.isFinite(v)) return 'FLOATING';
|
|
|
|
|
if (v >= config.thresholdHigh) return 'HIGH';
|
|
|
|
|
if (v <= config.thresholdLow) return 'LOW';
|
|
|
|
|
// Voltages in the dead band stay at the last known state — this is
|
|
|
|
|
// the basic hysteresis behavior that Phase 3 will refine per-family.
|
|
|
|
|
return cached;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Seed initial state from whatever the voltage source currently knows.
|
|
|
|
|
cachedVoltage = voltageSource.getCurrentVoltage(componentId, componentPinName);
|
|
|
|
|
cached = voltageToState(cachedVoltage);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
getCurrentState(): PinState {
|
|
|
|
|
return cached;
|
|
|
|
|
},
|
|
|
|
|
getCurrentVoltage(): number | null {
|
|
|
|
|
return cachedVoltage;
|
|
|
|
|
},
|
|
|
|
|
onChange(cb): () => void {
|
|
|
|
|
return voltageSource.subscribe(componentId, componentPinName, (_state, voltage) => {
|
|
|
|
|
cachedVoltage = voltage;
|
|
|
|
|
const next = voltageToState(voltage);
|
|
|
|
|
if (next !== cached) {
|
|
|
|
|
cached = next;
|
|
|
|
|
cb(next, voltage);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|