diff --git a/frontend/src/__tests__/pi-uart-wire.test.ts b/frontend/src/__tests__/pi-uart-wire.test.ts new file mode 100644 index 00000000..02dadf61 --- /dev/null +++ b/frontend/src/__tests__/pi-uart-wire.test.ts @@ -0,0 +1,27 @@ +/** + * A Raspberry Pi header pad wired to an Arduino serial pin must be seen + * as a UART link. + * + * The pads are LABELLED 'GPIO14' / 'GPIO15' — that is what the board art + * shows and what every example wire uses. `normalizePinName` only accepted + * physical pin numbers for the Pi, so parseInt('GPIO14') was NaN, the pin + * classified as nothing, and Interconnect never built the route: a Pi + * sending commands to an Arduino was talking into the void. + */ +import { describe, it, expect } from 'vitest'; +import { classifyPin, isUartWire } from '../utils/boardProtocols'; + +describe('Raspberry Pi UART pins', () => { + it('classifies the BCM-named header pads', () => { + expect(classifyPin('raspberry-pi-3', 'GPIO14')).toEqual({ kind: 'uart-tx', uart: 0 }); + expect(classifyPin('raspberry-pi-3', 'GPIO15')).toEqual({ kind: 'uart-rx', uart: 0 }); + // Physical numbering keeps working (pin 8 = BCM14, pin 10 = BCM15). + expect(classifyPin('raspberry-pi-3', '8')).toEqual({ kind: 'uart-tx', uart: 0 }); + expect(classifyPin('raspberry-pi-3', '10')).toEqual({ kind: 'uart-rx', uart: 0 }); + }); + + it('recognises the Pi <-> Arduino cross-board wires', () => { + expect(isUartWire('raspberry-pi-3', 'GPIO14', 'arduino-uno', '0')).toBeTruthy(); + expect(isUartWire('arduino-uno', '1', 'raspberry-pi-3', 'GPIO15')).toBeTruthy(); + }); +}); diff --git a/frontend/src/simulation/Interconnect.ts b/frontend/src/simulation/Interconnect.ts index 34e658ef..dff5e313 100644 --- a/frontend/src/simulation/Interconnect.ts +++ b/frontend/src/simulation/Interconnect.ts @@ -182,12 +182,44 @@ function pushPinState(boardId: string, pin: number, state: boolean): void { } } +// ── Serial seam for boards driven from outside the sim/bridge pair ─────── +// +// A QEMU-Linux board can also run its Python in the tab (no WebSocket, no +// bridge object). Such a board still has UART wires on the canvas, so it +// needs both directions of the routing: a sink to receive bytes, and a way +// to announce the ones it sends. Both are plain callbacks — nothing here +// knows what is on the other end. +const serialSinks = new Map void>(); + +/** Receive UART bytes addressed to this board. Returns an unregister fn. */ +export function registerSerialSink( + boardId: string, + sink: (ch: string, uart: number) => void, +): () => void { + serialSinks.set(boardId, sink); + return () => { + if (serialSinks.get(boardId) === sink) serialSinks.delete(boardId); + }; +} + +/** Announce a UART byte this board just transmitted, so the wires route it. */ +export function feedBoardSerialOut(boardId: string, ch: string, uart = 0): void { + const subs = boards.get(boardId)?.serialFanout.get(uart); + if (subs) for (const cb of subs) cb(ch); +} + /** Push a UART byte into the receiving board's UART RX. */ function pushSerialByte(boardId: string, ch: string, uart: number): void { if (!runtime) return; const entry = boards.get(boardId); if (!entry) return; + const sink = serialSinks.get(boardId); + if (sink) { + sink(ch, uart); + return; + } + if (isBrowserSim(entry.kind)) { const sim = runtime.getBoardSimulator(boardId); // RP2040Simulator doesn't yet expose feedUart per-UART — fall back diff --git a/frontend/src/utils/boardProtocols.ts b/frontend/src/utils/boardProtocols.ts index 4d3b68e0..4624219b 100644 --- a/frontend/src/utils/boardProtocols.ts +++ b/frontend/src/utils/boardProtocols.ts @@ -191,6 +191,13 @@ function normalizePinName(boardKind: string, pinName: string): string | null { boardKind.startsWith('raspberry-pi-4') || boardKind.startsWith('raspberry-pi-5') ) { + // The header pads are LABELLED 'GPIO14' (that is what the board art + // and every example wire use); only physical numbers were accepted + // here, so parseInt('GPIO14') was NaN and the pin classified as + // nothing at all — a Pi TX wired to an Arduino RX was never seen as a + // UART link, and the two boards could not talk. + const bcm = trimmed.match(/^(?:GPIO|BCM)(\d+)$/); + if (bcm) return bcm[1]; const phys = parseInt(trimmed, 10); if (!isNaN(phys)) { const bcm = PI3_PHYSICAL_TO_BCM[phys];