From 31c722b593b70eb76c89bbb2edba052aaa5cb723 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Thu, 30 Jul 2026 01:09:04 +0200 Subject: [PATCH] feat(pi): salida de pantalla para las Pi + arreglo del guard de connect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seam nuevo registerBoardBuiltins/getBoardBuiltins: una placa que el arbol OSS ya dibuja (la familia Pi) puede recibir perifericos del overlay sin registrar un ProBoardDef, que es lo que secuestraba el arte de la placa y dejaba los cables en la esquina. El overlay lo usa para llevar los frames del guest (cv2.imshow) al panel cableado en el canvas. Arregla ademas el guard de connect() del bridge: comparaba readyState con WebSocket.OPEN leido del global, asi que cuando esas constantes no estaban `undefined === undefined` era cierto con socket a null y connect() volvia sin abrir nada — el bridge se quedaba muerto. Ahora comprueba que el socket exista y usa el valor numerico. Recupera los 12 tests de multi-board-integration que esto habia roto. El test del UART Pi->Uno pasa a comprobar el contrato vigente: por el cable va onUartTx (el UART del header), no la consola del guest; y se anade el caso que fija que la charla de arranque NO se filtra al vecino. --- .../dual-pico-serial-passthrough.test.ts | 37 +++++++++++++++++-- .../components/simulator/SimulatorCanvas.tsx | 8 ++-- frontend/src/lib/proBoardRegistry.ts | 25 +++++++++++++ frontend/src/simulation/RaspberryPi3Bridge.ts | 10 ++++- 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/frontend/src/__tests__/dual-pico-serial-passthrough.test.ts b/frontend/src/__tests__/dual-pico-serial-passthrough.test.ts index 5762d7fc..e5cb1dc3 100644 --- a/frontend/src/__tests__/dual-pico-serial-passthrough.test.ts +++ b/frontend/src/__tests__/dual-pico-serial-passthrough.test.ts @@ -296,6 +296,13 @@ describe('Dual Pico W — Serial1 passthrough across wires', () => { // Wire the Pi3B's UART0 TX (BCM14, physical pin 8) to Uno.D0 (USART0 RX). // This is now the only path — the previous broadcast behaviour has been // replaced by wire-aware routing. + // + // A QEMU-Linux board has TWO serial streams and only one of them is on + // the header: `onUartTx` is what the script writes to /dev/serial0, while + // `onSerialData` is the console (boot chatter, the shell prompt, the + // script's stdout). This test used to feed the console and expect the + // peer to see it, which is exactly the leak that got fixed — the Uno was + // being handed the guest's login banner as if it were data. fullReset(); const store = useSimulatorStore.getState(); const arduinoId = store.boards[0].id; @@ -306,15 +313,37 @@ describe('Dual Pico W — Serial1 passthrough across wires', () => { const arduinoSim = getBoardSimulator(arduinoId) as any; const piBridge = getBoardBridge(piId) as any; - expect(typeof piBridge.onSerialData).toBe('function'); + expect(typeof piBridge.onUartTx).toBe('function'); - piBridge.onSerialData('A'); + piBridge.onUartTx('A'); - const fed = + const fedFromUart = () => (arduinoSim.feedUart as any).mock.calls.some( (c: any[]) => c[0] === 0 && c[1] === 'A', ) || (arduinoSim.serialWrite as any).mock.calls.some((c: any[]) => c[0] === 'A'); - expect(fed).toBe(true); + expect(fedFromUart()).toBe(true); + }); + + it("the Pi's console output stays off the wire", () => { + fullReset(); + const store = useSimulatorStore.getState(); + const arduinoId = store.boards[0].id; + const piId = store.addBoard('raspberry-pi-3', 300, 100); + setWires(useSimulatorStore, [ + { fromBoard: piId, fromPin: '8', toBoard: arduinoId, toPin: 'D0' }, + ]); + + const arduinoSim = getBoardSimulator(arduinoId) as any; + const piBridge = getBoardBridge(piId) as any; + + // What the guest prints to its terminal ('login:' during boot) must not + // reach the peer board, even though the TX wire is there. + piBridge.onSerialData?.('Q'); + + const leaked = + (arduinoSim.feedUart as any).mock.calls.some((c: any[]) => c[1] === 'Q') || + (arduinoSim.serialWrite as any).mock.calls.some((c: any[]) => c[0] === 'Q'); + expect(leaked).toBe(false); }); }); diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index 08c01aa0..b50a4a8c 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -4,7 +4,7 @@ import { getBoardBridge, getBoardSimulator, } from '../../store/useSimulatorStore'; -import { getProBoard } from '../../lib/proBoardRegistry'; +import { getBoardBuiltins } from '../../lib/proBoardRegistry'; import { useElectricalStore } from '../../store/useElectricalStore'; import { openDeviceGateway } from '../../lib/openDeviceGateway'; import React, { useEffect, useState, useRef, useCallback } from 'react'; @@ -253,14 +253,14 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { useSimulatorStore .getState() .boards.forEach((board) => { - const proDef = getProBoard(board.boardKind); - if (!proDef?.attachBuiltins || !board.running) return; + const attachBuiltins = getBoardBuiltins(board.boardKind); + if (!attachBuiltins || !board.running) return; const timeout = setTimeout(() => { const el = document.getElementById(board.id); if (!el) return; try { cleanups.push( - proDef.attachBuiltins!({ + attachBuiltins({ el, sim: getBoardSimulator(board.id), // ESP32-family boards get their QEMU/JS bridge; QEMU-Linux diff --git a/frontend/src/lib/proBoardRegistry.ts b/frontend/src/lib/proBoardRegistry.ts index c0836f73..64fc90f4 100644 --- a/frontend/src/lib/proBoardRegistry.ts +++ b/frontend/src/lib/proBoardRegistry.ts @@ -175,6 +175,31 @@ export function getGuestSetup(kind: string): string | undefined { return registry.get(kind)?.guestSetup ?? guestSetups.get(kind); } +// ── Built-in peripheral attachment for boards the OSS tree already owns ── +// +// Same reasoning as the guest setups above: an overlay may need to wire +// something to a run of an OSS-rendered board — the Pi family has no +// built-in screen, but an overlay can route the guest's display frames to +// a panel wired on the canvas. Carrying that through a stub ProBoardDef +// would take over the board's artwork, so it gets its own registry. +type BuiltinsAttach = (ctx: { + el: HTMLElement; + sim: unknown; + bridge: unknown; +}) => () => void; + +const boardBuiltins = new Map(); + +export function registerBoardBuiltins(kind: string, attach: BuiltinsAttach): void { + boardBuiltins.set(kind, attach); +} + +/** How to attach built-in peripherals for a board kind: an overlay board's + * own `attachBuiltins` first, then anything registered for an OSS kind. */ +export function getBoardBuiltins(kind: string): BuiltinsAttach | undefined { + return registry.get(kind)?.attachBuiltins ?? boardBuiltins.get(kind); +} + export function listProBoards(): ProBoardDef[] { return Array.from(registry.values()); } diff --git a/frontend/src/simulation/RaspberryPi3Bridge.ts b/frontend/src/simulation/RaspberryPi3Bridge.ts index 4428f2b1..b63d282f 100644 --- a/frontend/src/simulation/RaspberryPi3Bridge.ts +++ b/frontend/src/simulation/RaspberryPi3Bridge.ts @@ -105,8 +105,14 @@ export class RaspberryPi3Bridge { } connect(): void { - const state = this.socket?.readyState; - if (state === WebSocket.OPEN || state === WebSocket.CONNECTING) return; + // Numeric readyState, and only when a socket actually exists: reading + // WebSocket.OPEN off the global made `undefined === undefined` true + // whenever the constants were missing (any stand-in socket), so connect() + // returned without opening anything and the bridge sat there dead. + if (this.socket) { + const state = this.socket.readyState; + if (state === 0 /* CONNECTING */ || state === 1 /* OPEN */) return; + } // A socket in CLOSING is on its way out, not usable: the old guard // treated it as live and returned, so "restart in Linux mode" right // after a run silently did nothing — no boot, and the toolbar still