feat(sim): seated sockets are electrical, and capability is probed
Three generic seams, none naming any board: The pin tracer gains a socket hop: a component that declares boardSocket with a board actually seated on it resolves a pad to the SAME-NAMED pin of that board — seating IS the connection, like a XIAO pushed into a shield header or a HAT on the 40-pin. Until now a seated shield's buttons and LEDs were dead unless the user drew wires the real stack does not have. Wires keep working exactly as before for every other host (Uno, Mega, Pi, STM32, ...). setMicrophoneSource now RETURNS whether a bridge actually took the source, so a part can state that this host has no I2S instead of pretending to stream into a void — capability reported, never invented. createNeopixelDecoder is exported: boards are not the only things carrying an addressable LED, and a part with one on board needs the same decode rather than a drifting copy.
This commit is contained in:
parent
8ff8fdb292
commit
ab7949502d
|
|
@ -127,6 +127,36 @@ type TraceState = ReturnType<typeof useSimulatorStore.getState>;
|
|||
// Lifted to module scope (was inside getArduinoPin) so that getPinResolver
|
||||
// can call it too — the previous nested-scope version caused a runtime
|
||||
// ReferenceError "traceDetailed is not defined" on the simulator page.
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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;
|
||||
}
|
||||
|
||||
export function traceDetailed(
|
||||
state: TraceState,
|
||||
fromId: string,
|
||||
|
|
@ -142,6 +172,18 @@ export function traceDetailed(
|
|||
(w.end.componentId === fromId && w.end.pinName === fromPin),
|
||||
);
|
||||
|
||||
// A board SEATED on a socket component is connected without any wire — that
|
||||
// is what seating means, and it is how the hardware ships: a XIAO pushed
|
||||
// into a shield's header, a Pi HAT dropped onto the 40-pin. So when this
|
||||
// component declares a socket (boardSocket, the same contract the magnet
|
||||
// reads) and a board is seated on it, a pad resolves to the SAME-NAMED pin
|
||||
// of that board. Without this hop a seated shield's buttons and LEDs were
|
||||
// dead until the user drew wires that the real stack does not have.
|
||||
const socketPin = traceThroughSocket(state, fromId, fromPin);
|
||||
if (socketPin !== null) {
|
||||
return { arduinoPin: socketPin.pin, crossedActiveDevice: activeSeen, boardId: socketPin.boardId };
|
||||
}
|
||||
|
||||
// Remember a custom-chip neighbour on this net (if any) as a fallback —
|
||||
// a real board pin found in any branch still takes priority over it.
|
||||
let chipNeighbour: { id: string; pin: string } | null = null;
|
||||
|
|
|
|||
|
|
@ -451,7 +451,13 @@ PartSimulationRegistry.register('stepper-motor', {
|
|||
/**
|
||||
* Decode WS2812B bit-stream from DIN pin changes for NeoPixel devices.
|
||||
*/
|
||||
function createNeopixelDecoder(
|
||||
/**
|
||||
* WS2812/NeoPixel bit-bang decoder, shared. Exported because a board is not
|
||||
* the only thing that carries an addressable LED: any part with one on board
|
||||
* (the reSpeaker Lite's RGB, driven by whatever host is wired to it) needs the
|
||||
* same decode, and re-implementing it per part is how two of them drift.
|
||||
*/
|
||||
export function createNeopixelDecoder(
|
||||
simulator: any,
|
||||
pinDIN: number,
|
||||
onPixel: (index: number, r: number, g: number, b: number) => void,
|
||||
|
|
|
|||
|
|
@ -402,10 +402,16 @@ class Esp32BridgeShim {
|
|||
* setMicrophoneSource (velxio-prod overlay); everywhere else it's a no-op,
|
||||
* which reads as a silent mic.
|
||||
*/
|
||||
setMicrophoneSource(source: (() => number) | null): void {
|
||||
(
|
||||
this.bridge as { setMicrophoneSource?: (s: (() => number) | null) => void }
|
||||
).setMicrophoneSource?.(source);
|
||||
setMicrophoneSource(source: (() => number) | null): boolean {
|
||||
const b = this.bridge as { setMicrophoneSource?: (s: (() => number) | null) => void };
|
||||
if (typeof b?.setMicrophoneSource === 'function') {
|
||||
b.setMicrophoneSource(source);
|
||||
return true;
|
||||
}
|
||||
// No bridge, or one without an audio path: the caller learns the truth
|
||||
// instead of talking into a void — a part can then SAY the host has no
|
||||
// I2S rather than pretending to stream.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue