feat(pi-family): built-in peripheral plumbing for overlay QEMU-Linux boards
- profile key extra_drive: optional read-only second virtio-blk so an overlay can ship guest-side shim libraries (/dev/vdb) - SENS <name> protocol op: canvas-fed named values (built-in sensors / buttons) served from PiInstance.sensor_state, pushed by the frontend via the new pi_sensor_state WS message - DISP <b64> protocol op: guest display commands forwarded to the frontend as 'display' events (built-in screens) - RaspberryPi3Bridge: onDisplay / onGpioPwm callbacks + setSensorState - SimulatorCanvas hands piFamily boards their Pi bridge in attachBuiltins (was ESP32-only)
This commit is contained in:
parent
55dd25eeba
commit
1cdcb5a967
|
|
@ -91,6 +91,13 @@ async def simulation_websocket(websocket: WebSocket, client_id: str):
|
|||
state = msg_data.get('state', 0)
|
||||
qemu_manager.set_pin_state(client_id, pin, state)
|
||||
|
||||
elif msg_type == 'pi_sensor_state':
|
||||
# Canvas-fed named values for overlay boards' built-in
|
||||
# sensors/buttons; the guest polls them via SENS requests.
|
||||
values = msg_data.get('values', {})
|
||||
if isinstance(values, dict):
|
||||
qemu_manager.set_sensor_state(client_id, values)
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -254,6 +254,9 @@ class PiInstance:
|
|||
self._proto_out_fd: int | None = None # we read here ← guest writes
|
||||
self._tasks: list[asyncio.Task] = []
|
||||
self.running = False
|
||||
# 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] = {}
|
||||
|
||||
async def emit(self, event_type: str, data: dict) -> None:
|
||||
try:
|
||||
|
|
@ -299,6 +302,22 @@ class QemuManager:
|
|||
if inst and inst._gpio_writer:
|
||||
asyncio.create_task(self._send_gpio(inst, int(pin), bool(state)))
|
||||
|
||||
def set_sensor_state(self, client_id: str, values: dict) -> None:
|
||||
"""Merge canvas-fed named values (served to the guest via SENS).
|
||||
|
||||
Used by overlay boards whose built-in sensors/buttons live on the
|
||||
canvas element: the frontend pushes updates over the WebSocket and
|
||||
the guest polls them with ``SENS <name>`` protocol requests.
|
||||
"""
|
||||
inst = self._instances.get(client_id)
|
||||
if not inst:
|
||||
return
|
||||
for key, value in values.items():
|
||||
try:
|
||||
inst.sensor_state[str(key)] = float(value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
async def send_serial_bytes(self, client_id: str, data: bytes) -> None:
|
||||
inst = self._instances.get(client_id)
|
||||
if not inst:
|
||||
|
|
@ -465,6 +484,17 @@ class QemuManager:
|
|||
'-append', 'console=hvc0 root=/dev/vda rw quiet panic=10',
|
||||
]
|
||||
|
||||
# Optional read-only auxiliary disk (overlay-registered board
|
||||
# profiles use it to ship guest-side shim libraries). Shows up as
|
||||
# the second virtio-blk — /dev/vdb on the pci transport.
|
||||
extra_drive = cfg.get('extra_drive')
|
||||
if extra_drive and os.path.exists(extra_drive):
|
||||
cmd += [
|
||||
'-drive', f'if=none,file={extra_drive},format=raw,readonly=on,id=aux',
|
||||
'-device', ('virtio-blk-pci,drive=aux' if cfg['bus'] == 'pci'
|
||||
else 'virtio-blk-device,drive=aux'),
|
||||
]
|
||||
|
||||
logger.info('Launching QEMU for %s: %s',
|
||||
inst.client_id, ' '.join(cmd))
|
||||
|
||||
|
|
@ -674,6 +704,21 @@ class QemuManager:
|
|||
pass
|
||||
return
|
||||
|
||||
if op == 'SENS' and len(parts) == 2:
|
||||
# Canvas-fed named value (overlay boards' built-in sensors /
|
||||
# buttons). Unknown names read as 0 so guest shims degrade
|
||||
# gracefully when nothing on the canvas feeds them.
|
||||
value = inst.sensor_state.get(parts[1], 0.0)
|
||||
await self._reply_gpio(inst, f'SENS {parts[1]} {value:g}')
|
||||
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
|
||||
# element (overlay boards with built-in screens).
|
||||
await inst.emit('display', {'data': parts[1]})
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
useSimulatorStore,
|
||||
getEsp32Bridge,
|
||||
getBoardBridge,
|
||||
getBoardSimulator,
|
||||
} from '../../store/useSimulatorStore';
|
||||
import { getProBoard } from '../../lib/proBoardRegistry';
|
||||
|
|
@ -250,7 +251,9 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
proDef.attachBuiltins!({
|
||||
el,
|
||||
sim: getBoardSimulator(board.id),
|
||||
bridge: getEsp32Bridge(board.id),
|
||||
// ESP32-family boards get their QEMU/JS bridge; QEMU-Linux
|
||||
// (piFamily) boards get the Raspberry Pi bridge instead.
|
||||
bridge: getEsp32Bridge(board.id) ?? getBoardBridge(board.id),
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,14 @@ export class RaspberryPi3Bridge {
|
|||
onDisconnected: (() => void) | null = null;
|
||||
onError: ((msg: string) => void) | null = null;
|
||||
onSystemEvent: ((event: string, data: Record<string, unknown>) => void) | null = null;
|
||||
/** 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;
|
||||
/** Guest PWM activity (PWM_START / PWM_CHANGE / PWM_STOP). Overlay boards
|
||||
* use it for built-in buzzers/speakers. */
|
||||
onGpioPwm:
|
||||
| ((pin: number, frequency: number, dutyCycle: number, event: string) => void)
|
||||
| null = null;
|
||||
/** Fires once when the guest Linux has finished booting and reached an
|
||||
* interactive shell prompt. `connected` only means the WebSocket is open
|
||||
* (~1s); the guest still takes 30-60s to boot. Drives the "booting" UI and
|
||||
|
|
@ -123,6 +131,17 @@ export class RaspberryPi3Bridge {
|
|||
case 'system':
|
||||
this.onSystemEvent?.(msg.data.event as string, msg.data);
|
||||
break;
|
||||
case 'display':
|
||||
this.onDisplay?.((msg.data.data as string) ?? '');
|
||||
break;
|
||||
case 'gpio_pwm':
|
||||
this.onGpioPwm?.(
|
||||
(msg.data.pin as number) ?? 0,
|
||||
(msg.data.frequency as number) ?? 0,
|
||||
(msg.data.duty_cycle as number) ?? 0,
|
||||
(msg.data.event as string) ?? 'change',
|
||||
);
|
||||
break;
|
||||
case 'error':
|
||||
this.onError?.(msg.data.message as string);
|
||||
break;
|
||||
|
|
@ -235,6 +254,12 @@ export class RaspberryPi3Bridge {
|
|||
this._send({ type: 'gpio_in', data: { pin: gpioPin, state: state ? 1 : 0 } });
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
this._send({ type: 'pi_sensor_state', data: { values } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach an I2C/SPI/UART slave model to the running Pi. The backend
|
||||
* pro overlay turns this into a PiSlaveRegistry entry that the
|
||||
|
|
|
|||
Loading…
Reference in New Issue