feat(uart): el puerto serie del header tambien en modo Linux
La placa QEMU-Linux tiene DOS flujos serie y hasta ahora el cableado usaba el equivocado: el enrutado entregaba los bytes del vecino a la consola (el shell) y sacaba al cable la cháchara del arranque. El header, que es lo que el usuario cablea, no existia. Ahora el canal de protocolo lleva dos ops nuevas: UARTTX <b64> el guest transmitio por el header -> al canvas UARTRX el guest pregunta que le llego -> UART_RXQ <b64> El backend guarda una cola por instancia (acotada a 64 KB, que un script que no lee nunca no la haga crecer) y el websocket acepta `pi_uart_rx` con los bytes que el vecino manda. En el frontend el bridge gana onUartTx / sendUartBytes y el Interconnect engancha ESE flujo en vez de la consola para las placas Pi. Con esto el mismo script -- import serial, escribir, dormir, leer -- funciona en los dos motores.
This commit is contained in:
parent
2e0be83f23
commit
508d2e141e
|
|
@ -100,6 +100,13 @@ async def simulation_websocket(websocket: WebSocket, client_id: str):
|
|||
if isinstance(values, dict):
|
||||
qemu_manager.set_sensor_state(client_id, values)
|
||||
|
||||
elif msg_type == 'pi_uart_rx':
|
||||
# Bytes another board on the canvas sent down a TX->RX wire.
|
||||
# Queued for the guest, which drains them with UARTRX.
|
||||
rx: list[int] = msg_data.get('bytes', [])
|
||||
if rx:
|
||||
qemu_manager.push_uart_rx(client_id, bytes(rx))
|
||||
|
||||
elif msg_type in ('pi_attach_slave', 'pi_detach_slave'):
|
||||
# Pluggable hook — pro overlay registers the actual handler
|
||||
# via qemu_manager.set_pi_slave_handler(). In the OSS image
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ Protocol channel (chardev 1) — wired by Phase 2's pi_protocol_mux
|
|||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
|
|
@ -291,6 +292,10 @@ class PiInstance:
|
|||
# Canvas-fed named values served to the guest via the SENS
|
||||
# protocol op (overlay boards' built-in sensors/buttons).
|
||||
self.sensor_state: dict[str, float] = {}
|
||||
# Bytes another board on the canvas sent to this one's header UART
|
||||
# (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()
|
||||
# Raw `start_pi` payload — carries whatever the client declared for
|
||||
# this session (e.g. the packages an overlay must materialise).
|
||||
self.start_payload: dict = {}
|
||||
|
|
@ -341,6 +346,17 @@ class QemuManager:
|
|||
if inst and 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:
|
||||
"""Queue bytes for the guest's header UART (a wired board's TX)."""
|
||||
inst = self._instances.get(client_id)
|
||||
if not inst or not data:
|
||||
return
|
||||
# Bound the queue: a script that never reads must not grow it
|
||||
# without limit (the peer keeps transmitting either way).
|
||||
if len(inst.uart_rx) > 64 * 1024:
|
||||
del inst.uart_rx[: len(inst.uart_rx) - 64 * 1024]
|
||||
inst.uart_rx.extend(data)
|
||||
|
||||
def set_sensor_state(self, client_id: str, values: dict) -> None:
|
||||
"""Merge canvas-fed named values (served to the guest via SENS).
|
||||
|
||||
|
|
@ -790,6 +806,24 @@ 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
|
||||
|
|
|
|||
|
|
@ -236,8 +236,15 @@ function pushSerialByte(boardId: string, ch: string, uart: number): void {
|
|||
const bridge = runtime.getStm32Bridge(boardId);
|
||||
bridge?.sendSerialBytes?.([ch.charCodeAt(0)], uart);
|
||||
} else if (isPi3Bridge(entry.kind)) {
|
||||
const bridge = runtime.getBoardBridge(boardId);
|
||||
bridge?.sendSerialBytes?.([ch.charCodeAt(0)]);
|
||||
const bridge = runtime.getBoardBridge(boardId) as
|
||||
| { sendUartBytes?: (b: number[]) => void; sendSerialBytes?: (b: number[]) => void }
|
||||
| undefined;
|
||||
// The header UART is a different pipe from the console: typing a
|
||||
// peer's bytes into the shell used to be the only option, and it
|
||||
// meant the guest's own boot chatter went out on the wire while the
|
||||
// data a script wrote never did.
|
||||
if (bridge?.sendUartBytes) bridge.sendUartBytes([ch.charCodeAt(0)]);
|
||||
else bridge?.sendSerialBytes?.([ch.charCodeAt(0)]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -352,6 +359,24 @@ function ensureSerialHook(entry: BoardEntry): void {
|
|||
? runtime.getStm32Bridge(entry.id)
|
||||
: runtime.getBoardBridge(entry.id);
|
||||
if (!bridge) return;
|
||||
|
||||
// A QEMU-Linux board has TWO serial streams: the console (the shell)
|
||||
// and the header UART. Only the second one is on the wire — hooking the
|
||||
// console here would send the guest's boot chatter and shell prompt to
|
||||
// the peer board, which is what used to happen for lack of anything
|
||||
// better.
|
||||
const piBridge = bridge as unknown as { onUartTx?: ((t: string) => void) | null };
|
||||
if (isPi3Bridge(entry.kind) && 'onUartTx' in piBridge) {
|
||||
if ((bridge as unknown as { __icUartHook?: boolean }).__icUartHook) return;
|
||||
(bridge as unknown as { __icUartHook?: boolean }).__icUartHook = true;
|
||||
const prevUart = piBridge.onUartTx ?? null;
|
||||
piBridge.onUartTx = (text: string) => {
|
||||
prevUart?.(text);
|
||||
const subs = boards.get(boardId)?.serialFanout.get(0);
|
||||
if (subs) for (const ch of text) for (const cb of subs) cb(ch);
|
||||
};
|
||||
return;
|
||||
}
|
||||
if ((bridge as any).__icSerialHookInstalled) return;
|
||||
(bridge as any).__icSerialHookInstalled = true;
|
||||
entry.origSerialCallback = bridge.onSerialData ?? null;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ export class RaspberryPi3Bridge {
|
|||
/** Guest display command (opaque base64 payload from the DISP protocol
|
||||
* op). Overlay boards with built-in screens render it on their element. */
|
||||
onDisplay: ((data: string) => void) | null = null;
|
||||
/** Bytes the guest wrote to its HEADER UART (not the console): another
|
||||
* board wired to those pads is the destination. Decoded text. */
|
||||
onUartTx: ((text: string) => void) | null = null;
|
||||
/** Guest PWM activity (PWM_START / PWM_CHANGE / PWM_STOP). Overlay boards
|
||||
* use it for built-in buzzers/speakers. */
|
||||
onGpioPwm:
|
||||
|
|
@ -164,6 +167,21 @@ export class RaspberryPi3Bridge {
|
|||
case 'display':
|
||||
this.onDisplay?.((msg.data.data as string) ?? '');
|
||||
break;
|
||||
case 'uart_tx': {
|
||||
const b64 = (msg.data.data as string) ?? '';
|
||||
if (b64) {
|
||||
try {
|
||||
this.onUartTx?.(
|
||||
new TextDecoder().decode(
|
||||
Uint8Array.from(atob(b64), (ch) => ch.charCodeAt(0)),
|
||||
),
|
||||
);
|
||||
} catch {
|
||||
/* malformed payload — never kill the session over it */
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'gpio_pwm':
|
||||
this.onGpioPwm?.(
|
||||
(msg.data.pin as number) ?? 0,
|
||||
|
|
@ -310,6 +328,13 @@ export class RaspberryPi3Bridge {
|
|||
this._send({ type: 'gpio_in', data: { pin: gpioPin, state: state ? 1 : 0 } });
|
||||
}
|
||||
|
||||
/** Bytes for the guest's HEADER UART RX (a wired board's TX). Distinct
|
||||
* from sendSerialBytes, which types into the console/shell. */
|
||||
sendUartBytes(bytes: number[]): void {
|
||||
if (!bytes.length) return;
|
||||
this._send({ type: 'pi_uart_rx', data: { bytes } });
|
||||
}
|
||||
|
||||
/** Push canvas-fed named values (built-in sensors/buttons of overlay
|
||||
* boards). The guest polls them via SENS protocol requests. */
|
||||
setSensorState(values: Record<string, number>): void {
|
||||
|
|
|
|||
Loading…
Reference in New Issue