fix(pi): perifericos completos en la familia Pi — entradas, buses y pines
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 <port> TX <hex>` / 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.
This commit is contained in:
parent
508d2e141e
commit
93388c675a
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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<SimulatorState>((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)),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue