From 93388c675ab16d851c4e2c1ae82d7dac1e49b15d Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Wed, 29 Jul 2026 17:05:39 +0200 Subject: [PATCH] =?UTF-8?q?fix(pi):=20perifericos=20completos=20en=20la=20?= =?UTF-8?q?familia=20Pi=20=E2=80=94=20entradas,=20buses=20y=20pines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auditoria de "la Pi tiene todo lo de la placa real" con cuatro huecos encontrados y cerrados: 1) GPIO de entrada en modo Linux: GPIO_IN respondia VAL 0 fijo (stub de la fase 2), asi que GPIO.input() leia 0 eternamente aunque el canvas empujara el nivel. El backend guarda ahora el ultimo nivel por pin (set_pin_state lo escribe) y GPIO_IN contesta de ahi. Los flancos (SET) siguen llegando al guest como antes. 2) UART del header hacia otra placa: el shim del rootfs ya hablaba `UART TX ` / RX_REQ, pero sin modelo de esclavo el backend tragaba los bytes. Ahora TX sin esclavo se emite al canvas (uart_tx) y RX_REQ sin esclavo drena la cola que llena pi_uart_rx — el mismo protocolo de siempre, sin ops nuevas. 3) El escaner de esclavos I2C/SPI/UART estaba doblemente muerto: clasificaba por numero fisico de pin ('3','5','19'...) cuando el elemento expone GPIOxx, y su unico llamador era RaspberryPiWorkspace, que el terminal unificado reemplazo. Acepta ambos nombres y corre en onBooted del store. 4) boardPinToNumber solo mapeaba los pines de la 3/4/5; la Zero, 1B+ y 2B (mismo header de 40 pines, mismo elemento) se quedaban sin mapa. --- backend/app/services/qemu_manager.py | 62 +++++++++++++---------- frontend/src/simulation/piSlaveScanner.ts | 21 +++++--- frontend/src/store/useSimulatorStore.ts | 16 ++++++ frontend/src/utils/boardPinMapping.ts | 9 ++-- 4 files changed, 69 insertions(+), 39 deletions(-) diff --git a/backend/app/services/qemu_manager.py b/backend/app/services/qemu_manager.py index 99b06965..5ae1e4f9 100644 --- a/backend/app/services/qemu_manager.py +++ b/backend/app/services/qemu_manager.py @@ -296,6 +296,9 @@ class PiInstance: # (TX->RX wire). The guest drains them with the UARTRX op; nothing # here interprets them, they are a pipe between two boards. self.uart_rx = bytearray() + # Last externally-driven level per BCM pin (canvas buttons, PIR, + # a wired board's output). GPIO_IN answers from here. + self.pin_levels: dict[int, int] = {} # Raw `start_pi` payload — carries whatever the client declared for # this session (e.g. the packages an overlay must materialise). self.start_payload: dict = {} @@ -343,7 +346,13 @@ class QemuManager: def set_pin_state(self, client_id: str, pin: str | int, state: int) -> None: """Drive a GPIO pin from outside (e.g. connected Arduino).""" inst = self._instances.get(client_id) - if inst and inst._gpio_writer: + if not inst: + return + # Remember the level: GPIO_IN polls answer from this map. Without + # it a button on the canvas fired edge callbacks in the guest but + # GPIO.input() read an eternal 0 (the old stub). + inst.pin_levels[int(pin)] = 1 if state else 0 + if inst._gpio_writer: asyncio.create_task(self._send_gpio(inst, int(pin), bool(state))) def push_uart_rx(self, client_id: str, data: bytes) -> None: @@ -806,24 +815,6 @@ class QemuManager: await self._reply_gpio(inst, f'SENS {parts[1]} {value:g}') return - if op == 'UARTTX' and len(parts) == 2: - # Guest wrote to its header UART: hand the bytes to the canvas, - # which routes them down the wire to whatever board is on the - # other end. Opaque base64, exactly like DISP. - await inst.emit('uart_tx', {'data': parts[1]}) - return - - if op == 'UARTRX': - # Guest polls for bytes received on its header UART. - pending = bytes(inst.uart_rx) - inst.uart_rx.clear() - await self._reply_gpio( - inst, - 'UART_RXQ ' + (base64.b64encode(pending).decode('ascii') - if pending else ''), - ) - return - if op == 'DISP' and len(parts) == 2: # Guest display command (opaque base64 payload). Forwarded # verbatim to the frontend, which renders it on the board @@ -832,14 +823,13 @@ class QemuManager: return if op == 'GPIO_IN' and len(parts) == 2: - # Reply with the last known state of the pin. For Phase 2 - # we just echo 0 — the canvas-side input wiring fans in - # through SET commands which the shim caches on the guest. - # When canvas-driven inputs land in Phase 2.5 this will - # query the gpio event bus' last-state map. + # Reply with the last externally-driven level of the pin + # (canvas buttons / PIR / a wired board's output, delivered + # through set_pin_state). Unknown pins read 0. try: pin = int(parts[1]) - await self._reply_gpio(inst, f'VAL {pin} 0') + await self._reply_gpio( + inst, f'VAL {pin} {inst.pin_levels.get(pin, 0)}') except ValueError: pass return @@ -897,8 +887,28 @@ class QemuManager: await self._reply_gpio( inst, f'SPI_DATA {parts[1]} {parts[2]} {"00" * length}') return + # No slave model on this UART: the port is wired to another + # BOARD on the canvas. TX goes out to it and RX comes back + # from the queue the frontend fills — the guest shim already + # speaks this, it just used to talk into the void. + if op == 'UART' and len(parts) >= 4 and parts[2] == 'TX': + try: + payload = bytes.fromhex(parts[3]) + except ValueError: + return + await inst.emit('uart_tx', { + 'port': parts[1], + 'data': base64.b64encode(payload).decode('ascii'), + }) + return if op == 'UART' and len(parts) >= 3 and parts[2] == 'RX_REQ': - await self._reply_gpio(inst, f'UART_RX {parts[1]}') + pending = bytes(inst.uart_rx) + inst.uart_rx.clear() + await self._reply_gpio( + inst, + f'UART_RX {parts[1]} {pending.hex()}' if pending + else f'UART_RX {parts[1]}', + ) return # Unknown — log at debug level (not a hot path) diff --git a/frontend/src/simulation/piSlaveScanner.ts b/frontend/src/simulation/piSlaveScanner.ts index 81c25a2b..4740535b 100644 --- a/frontend/src/simulation/piSlaveScanner.ts +++ b/frontend/src/simulation/piSlaveScanner.ts @@ -30,11 +30,16 @@ type CanvasComponent = { // - SDA1/SCL1 → I2C bus 1 // - MOSI/MISO/SCLK → SPI bus 0 (CE0/CE1 distinguish slaves) // - TXD/RXD → primary UART (port 0) -const I2C_PINS = new Set(['3', '5']); -const SPI_DATA_PINS = new Set(['19', '21', '23']); -const SPI_CE0_PIN = '24'; -const SPI_CE1_PIN = '26'; -const UART_PINS = new Set(['8', '10']); +// Both namings are accepted: the wire may carry the PHYSICAL pin number +// ('3') or the BCM label the board art actually exposes ('GPIO2'). The +// scanner shipped matching only physical numbers while every element +// names its pads GPIOxx — so no wire ever classified and the whole +// slave-attach path was dead. +const I2C_PINS = new Set(['3', '5', 'GPIO2', 'GPIO3', 'SDA', 'SCL', 'SDA1', 'SCL1']); +const SPI_DATA_PINS = new Set(['19', '21', '23', 'GPIO10', 'GPIO9', 'GPIO11', 'MOSI', 'MISO', 'SCLK', 'SCK']); +const SPI_CE0_PINS = new Set(['24', 'GPIO8', 'CE0']); +const SPI_CE1_PINS = new Set(['26', 'GPIO7', 'CE1']); +const UART_PINS = new Set(['8', '10', 'GPIO14', 'GPIO15', 'TXD', 'RXD', 'TX', 'RX']); // Map wokwi component metadata IDs / element types → backend model_id. // Lowercase. Components not in this table get skipped silently. @@ -83,7 +88,7 @@ function classifyPiPin(pinName: string): { bus_num: number; } | null { if (I2C_PINS.has(pinName)) return { bus_kind: 'i2c', bus_num: 1 }; - if (SPI_DATA_PINS.has(pinName) || pinName === SPI_CE0_PIN || pinName === SPI_CE1_PIN) + if (SPI_DATA_PINS.has(pinName) || SPI_CE0_PINS.has(pinName) || SPI_CE1_PINS.has(pinName)) return { bus_kind: 'spi', bus_num: 0 }; if (UART_PINS.has(pinName)) return { bus_kind: 'uart', bus_num: 0 }; return null; @@ -142,8 +147,8 @@ export function attachSlavesFromCanvas( // wires as informational only — the CE wire is the one that // pins down which slave gets attached. let cs: number; - if (piEndpoint.pinName === SPI_CE0_PIN) cs = 0; - else if (piEndpoint.pinName === SPI_CE1_PIN) cs = 1; + if (SPI_CE0_PINS.has(piEndpoint.pinName)) cs = 0; + else if (SPI_CE1_PINS.has(piEndpoint.pinName)) cs = 1; else continue; spec = { bus_kind: 'spi', diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index f1f5ab59..1b080194 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -7,6 +7,7 @@ import { type ProBoardSimulator, } from '../lib/proBoardRegistry'; import { AVRSimulator } from '../simulation/AVRSimulator'; +import { attachSlavesFromCanvas } from '../simulation/piSlaveScanner'; import { RP2040Simulator } from '../simulation/RP2040Simulator'; import { RiscVSimulator } from '../simulation/RiscVSimulator'; import { Esp32C3Simulator } from '../simulation/Esp32C3Simulator'; @@ -1297,6 +1298,21 @@ export const useSimulatorStore = create((set, get) => { // cannot interleave with it. bridge.onBooted = () => { const setup = getGuestSetup(boardKind); + // Attach the slave models for I2C/SPI/UART components wired to + // this Pi. This used to live in RaspberryPiWorkspace, which the + // unified terminal replaced — leaving the scan with no caller, + // so a BMP280 on the Pi's I2C pins never got its backend model. + try { + const st = get(); + attachSlavesFromCanvas( + id, + bridge, + st.components as never, + st.wires as never, + ); + } catch (e) { + console.warn('[pi] slave scan failed:', e); + } const flip = () => set((s) => ({ boards: s.boards.map((b) => (b.id === id ? { ...b, piBooted: true } : b)), diff --git a/frontend/src/utils/boardPinMapping.ts b/frontend/src/utils/boardPinMapping.ts index d3497a16..12a0d06c 100644 --- a/frontend/src/utils/boardPinMapping.ts +++ b/frontend/src/utils/boardPinMapping.ts @@ -324,11 +324,10 @@ export function boardPinToNumber(boardId: string, pinName: string): number | nul // table works. `pinName` may be either the physical pin number // ("1" … "40") OR a BCM-style name ("GPIO14") emitted by the Pi // element's pinInfo — power / GND pins return -1. - if ( - boardId === 'raspberry-pi-3' || boardId.startsWith('raspberry-pi-3') || - boardId === 'raspberry-pi-4' || boardId.startsWith('raspberry-pi-4') || - boardId === 'raspberry-pi-5' || boardId.startsWith('raspberry-pi-5') - ) { + // The whole QEMU-Linux Pi family shares the 40-pin header (the Zero, + // 1B+ and 2B render the same element as the 3) — matching only 3/4/5 + // left the small boards without any pin mapping at all. + if (boardId.startsWith('raspberry-pi-') && boardId !== 'raspberry-pi-pico') { if (/^(GND|VCC|3V3|5V|ID_S[DC])/.test(pinName)) return -1; if (pinName.startsWith('GPIO')) { const n = parseInt(pinName.substring(4), 10);