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):
|
if isinstance(values, dict):
|
||||||
qemu_manager.set_sensor_state(client_id, values)
|
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'):
|
elif msg_type in ('pi_attach_slave', 'pi_detach_slave'):
|
||||||
# Pluggable hook — pro overlay registers the actual handler
|
# Pluggable hook — pro overlay registers the actual handler
|
||||||
# via qemu_manager.set_pi_slave_handler(). In the OSS image
|
# 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 asyncio
|
||||||
|
import base64
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
|
|
@ -291,6 +292,10 @@ class PiInstance:
|
||||||
# Canvas-fed named values served to the guest via the SENS
|
# Canvas-fed named values served to the guest via the SENS
|
||||||
# protocol op (overlay boards' built-in sensors/buttons).
|
# protocol op (overlay boards' built-in sensors/buttons).
|
||||||
self.sensor_state: dict[str, float] = {}
|
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
|
# 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 = {}
|
||||||
|
|
@ -341,6 +346,17 @@ class QemuManager:
|
||||||
if inst and inst._gpio_writer:
|
if inst and 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:
|
||||||
|
"""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:
|
def set_sensor_state(self, client_id: str, values: dict) -> None:
|
||||||
"""Merge canvas-fed named values (served to the guest via SENS).
|
"""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}')
|
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
|
||||||
|
|
|
||||||
|
|
@ -236,8 +236,15 @@ function pushSerialByte(boardId: string, ch: string, uart: number): void {
|
||||||
const bridge = runtime.getStm32Bridge(boardId);
|
const bridge = runtime.getStm32Bridge(boardId);
|
||||||
bridge?.sendSerialBytes?.([ch.charCodeAt(0)], uart);
|
bridge?.sendSerialBytes?.([ch.charCodeAt(0)], uart);
|
||||||
} else if (isPi3Bridge(entry.kind)) {
|
} else if (isPi3Bridge(entry.kind)) {
|
||||||
const bridge = runtime.getBoardBridge(boardId);
|
const bridge = runtime.getBoardBridge(boardId) as
|
||||||
bridge?.sendSerialBytes?.([ch.charCodeAt(0)]);
|
| { 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.getStm32Bridge(entry.id)
|
||||||
: runtime.getBoardBridge(entry.id);
|
: runtime.getBoardBridge(entry.id);
|
||||||
if (!bridge) return;
|
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;
|
if ((bridge as any).__icSerialHookInstalled) return;
|
||||||
(bridge as any).__icSerialHookInstalled = true;
|
(bridge as any).__icSerialHookInstalled = true;
|
||||||
entry.origSerialCallback = bridge.onSerialData ?? null;
|
entry.origSerialCallback = bridge.onSerialData ?? null;
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,9 @@ export class RaspberryPi3Bridge {
|
||||||
/** Guest display command (opaque base64 payload from the DISP protocol
|
/** Guest display command (opaque base64 payload from the DISP protocol
|
||||||
* op). Overlay boards with built-in screens render it on their element. */
|
* op). Overlay boards with built-in screens render it on their element. */
|
||||||
onDisplay: ((data: string) => void) | null = null;
|
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
|
/** Guest PWM activity (PWM_START / PWM_CHANGE / PWM_STOP). Overlay boards
|
||||||
* use it for built-in buzzers/speakers. */
|
* use it for built-in buzzers/speakers. */
|
||||||
onGpioPwm:
|
onGpioPwm:
|
||||||
|
|
@ -164,6 +167,21 @@ export class RaspberryPi3Bridge {
|
||||||
case 'display':
|
case 'display':
|
||||||
this.onDisplay?.((msg.data.data as string) ?? '');
|
this.onDisplay?.((msg.data.data as string) ?? '');
|
||||||
break;
|
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':
|
case 'gpio_pwm':
|
||||||
this.onGpioPwm?.(
|
this.onGpioPwm?.(
|
||||||
(msg.data.pin as number) ?? 0,
|
(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 } });
|
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
|
/** Push canvas-fed named values (built-in sensors/buttons of overlay
|
||||||
* boards). The guest polls them via SENS protocol requests. */
|
* boards). The guest polls them via SENS protocol requests. */
|
||||||
setSensorState(values: Record<string, number>): void {
|
setSensorState(values: Record<string, number>): void {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue