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
|
# (TX->RX wire). The guest drains them with the UARTRX op; nothing
|
||||||
# here interprets them, they are a pipe between two boards.
|
# here interprets them, they are a pipe between two boards.
|
||||||
self.uart_rx = bytearray()
|
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
|
# Raw `start_pi` payload — carries whatever the client declared for
|
||||||
# this session (e.g. the packages an overlay must materialise).
|
# this session (e.g. the packages an overlay must materialise).
|
||||||
self.start_payload: dict = {}
|
self.start_payload: dict = {}
|
||||||
|
|
@ -343,7 +346,13 @@ class QemuManager:
|
||||||
def set_pin_state(self, client_id: str, pin: str | int, state: int) -> None:
|
def set_pin_state(self, client_id: str, pin: str | int, state: int) -> None:
|
||||||
"""Drive a GPIO pin from outside (e.g. connected Arduino)."""
|
"""Drive a GPIO pin from outside (e.g. connected Arduino)."""
|
||||||
inst = self._instances.get(client_id)
|
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)))
|
asyncio.create_task(self._send_gpio(inst, int(pin), bool(state)))
|
||||||
|
|
||||||
def push_uart_rx(self, client_id: str, data: bytes) -> None:
|
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}')
|
await self._reply_gpio(inst, f'SENS {parts[1]} {value:g}')
|
||||||
return
|
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:
|
if op == 'DISP' and len(parts) == 2:
|
||||||
# Guest display command (opaque base64 payload). Forwarded
|
# Guest display command (opaque base64 payload). Forwarded
|
||||||
# verbatim to the frontend, which renders it on the board
|
# verbatim to the frontend, which renders it on the board
|
||||||
|
|
@ -832,14 +823,13 @@ class QemuManager:
|
||||||
return
|
return
|
||||||
|
|
||||||
if op == 'GPIO_IN' and len(parts) == 2:
|
if op == 'GPIO_IN' and len(parts) == 2:
|
||||||
# Reply with the last known state of the pin. For Phase 2
|
# Reply with the last externally-driven level of the pin
|
||||||
# we just echo 0 — the canvas-side input wiring fans in
|
# (canvas buttons / PIR / a wired board's output, delivered
|
||||||
# through SET commands which the shim caches on the guest.
|
# through set_pin_state). Unknown pins read 0.
|
||||||
# When canvas-driven inputs land in Phase 2.5 this will
|
|
||||||
# query the gpio event bus' last-state map.
|
|
||||||
try:
|
try:
|
||||||
pin = int(parts[1])
|
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:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
|
|
@ -897,8 +887,28 @@ class QemuManager:
|
||||||
await self._reply_gpio(
|
await self._reply_gpio(
|
||||||
inst, f'SPI_DATA {parts[1]} {parts[2]} {"00" * length}')
|
inst, f'SPI_DATA {parts[1]} {parts[2]} {"00" * length}')
|
||||||
return
|
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':
|
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
|
return
|
||||||
|
|
||||||
# Unknown — log at debug level (not a hot path)
|
# Unknown — log at debug level (not a hot path)
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,16 @@ type CanvasComponent = {
|
||||||
// - SDA1/SCL1 → I2C bus 1
|
// - SDA1/SCL1 → I2C bus 1
|
||||||
// - MOSI/MISO/SCLK → SPI bus 0 (CE0/CE1 distinguish slaves)
|
// - MOSI/MISO/SCLK → SPI bus 0 (CE0/CE1 distinguish slaves)
|
||||||
// - TXD/RXD → primary UART (port 0)
|
// - TXD/RXD → primary UART (port 0)
|
||||||
const I2C_PINS = new Set(['3', '5']);
|
// Both namings are accepted: the wire may carry the PHYSICAL pin number
|
||||||
const SPI_DATA_PINS = new Set(['19', '21', '23']);
|
// ('3') or the BCM label the board art actually exposes ('GPIO2'). The
|
||||||
const SPI_CE0_PIN = '24';
|
// scanner shipped matching only physical numbers while every element
|
||||||
const SPI_CE1_PIN = '26';
|
// names its pads GPIOxx — so no wire ever classified and the whole
|
||||||
const UART_PINS = new Set(['8', '10']);
|
// 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.
|
// Map wokwi component metadata IDs / element types → backend model_id.
|
||||||
// Lowercase. Components not in this table get skipped silently.
|
// Lowercase. Components not in this table get skipped silently.
|
||||||
|
|
@ -83,7 +88,7 @@ function classifyPiPin(pinName: string): {
|
||||||
bus_num: number;
|
bus_num: number;
|
||||||
} | null {
|
} | null {
|
||||||
if (I2C_PINS.has(pinName)) return { bus_kind: 'i2c', bus_num: 1 };
|
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 };
|
return { bus_kind: 'spi', bus_num: 0 };
|
||||||
if (UART_PINS.has(pinName)) return { bus_kind: 'uart', bus_num: 0 };
|
if (UART_PINS.has(pinName)) return { bus_kind: 'uart', bus_num: 0 };
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -142,8 +147,8 @@ export function attachSlavesFromCanvas(
|
||||||
// wires as informational only — the CE wire is the one that
|
// wires as informational only — the CE wire is the one that
|
||||||
// pins down which slave gets attached.
|
// pins down which slave gets attached.
|
||||||
let cs: number;
|
let cs: number;
|
||||||
if (piEndpoint.pinName === SPI_CE0_PIN) cs = 0;
|
if (SPI_CE0_PINS.has(piEndpoint.pinName)) cs = 0;
|
||||||
else if (piEndpoint.pinName === SPI_CE1_PIN) cs = 1;
|
else if (SPI_CE1_PINS.has(piEndpoint.pinName)) cs = 1;
|
||||||
else continue;
|
else continue;
|
||||||
spec = {
|
spec = {
|
||||||
bus_kind: 'spi',
|
bus_kind: 'spi',
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
type ProBoardSimulator,
|
type ProBoardSimulator,
|
||||||
} from '../lib/proBoardRegistry';
|
} from '../lib/proBoardRegistry';
|
||||||
import { AVRSimulator } from '../simulation/AVRSimulator';
|
import { AVRSimulator } from '../simulation/AVRSimulator';
|
||||||
|
import { attachSlavesFromCanvas } from '../simulation/piSlaveScanner';
|
||||||
import { RP2040Simulator } from '../simulation/RP2040Simulator';
|
import { RP2040Simulator } from '../simulation/RP2040Simulator';
|
||||||
import { RiscVSimulator } from '../simulation/RiscVSimulator';
|
import { RiscVSimulator } from '../simulation/RiscVSimulator';
|
||||||
import { Esp32C3Simulator } from '../simulation/Esp32C3Simulator';
|
import { Esp32C3Simulator } from '../simulation/Esp32C3Simulator';
|
||||||
|
|
@ -1297,6 +1298,21 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
||||||
// cannot interleave with it.
|
// cannot interleave with it.
|
||||||
bridge.onBooted = () => {
|
bridge.onBooted = () => {
|
||||||
const setup = getGuestSetup(boardKind);
|
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 = () =>
|
const flip = () =>
|
||||||
set((s) => ({
|
set((s) => ({
|
||||||
boards: s.boards.map((b) => (b.id === id ? { ...b, piBooted: true } : b)),
|
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
|
// table works. `pinName` may be either the physical pin number
|
||||||
// ("1" … "40") OR a BCM-style name ("GPIO14") emitted by the Pi
|
// ("1" … "40") OR a BCM-style name ("GPIO14") emitted by the Pi
|
||||||
// element's pinInfo — power / GND pins return -1.
|
// element's pinInfo — power / GND pins return -1.
|
||||||
if (
|
// The whole QEMU-Linux Pi family shares the 40-pin header (the Zero,
|
||||||
boardId === 'raspberry-pi-3' || boardId.startsWith('raspberry-pi-3') ||
|
// 1B+ and 2B render the same element as the 3) — matching only 3/4/5
|
||||||
boardId === 'raspberry-pi-4' || boardId.startsWith('raspberry-pi-4') ||
|
// left the small boards without any pin mapping at all.
|
||||||
boardId === 'raspberry-pi-5' || boardId.startsWith('raspberry-pi-5')
|
if (boardId.startsWith('raspberry-pi-') && boardId !== 'raspberry-pi-pico') {
|
||||||
) {
|
|
||||||
if (/^(GND|VCC|3V3|5V|ID_S[DC])/.test(pinName)) return -1;
|
if (/^(GND|VCC|3V3|5V|ID_S[DC])/.test(pinName)) return -1;
|
||||||
if (pinName.startsWith('GPIO')) {
|
if (pinName.startsWith('GPIO')) {
|
||||||
const n = parseInt(pinName.substring(4), 10);
|
const n = parseInt(pinName.substring(4), 10);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue