velxio/backend/app/services/qemu_manager.py

489 lines
20 KiB
Python
Raw Normal View History

"""
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
QemuManager backend service for Raspberry Pi simulator emulation via QEMU.
Architecture
------------
Each Pi board instance gets:
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
- qemu-system-aarch64 process (-M virt -cpu cortex-a53 for Pi 3, other
cpu models for the rest of the family see Phase 3 plan)
- Velxio-built kernel + initramfs + rootfs (not the rpi-firmware kernel
and not raspios; see ``project/pi-emulation/`` for why)
- virtio-blk root from a qcow2 overlay over the cached rootfs ext4
- virtio-console on chardev 0 (TCP socket) the user shell at /dev/hvc0
- virtio-serial port on chardev 1 (TCP socket) multiplexed text
protocol channel for GPIO/I2C/SPI/UART/PWM (Phase 2 wires it up)
We were on ``-M raspi3b`` previously but QEMU 10 + kernel 6.12 had a
pl011 RX bug that broke userspace tty open see
``project/pi-emulation/decisions.md`` for the full debugging trail.
The ``raspberry-pi-3`` manifest entry remains around (marked deprecated)
to ease rollback; ``raspberry-pi-3-virt`` is the live one.
Boot files are resolved at runtime via BootImageProvider (downloads,
verifies, caches under /var/cache/velxio/boot-images/...). The lifespan
hook at the bottom pre-warms the cache so first-time user requests
don't pay the download latency.
Protocol channel (chardev 1) wired by Phase 2's pi_protocol_mux
Pi backend : "GPIO <bcm> <0|1>\\n" (also I2C/SPI/UART/PWM lines)
backend Pi : "SET <bcm> <0|1>\\n" and reply frames
"""
import asyncio
import logging
import os
import socket
import subprocess
import tempfile
feat(sim): boot_images module + Pi 3 emulation restored Pi 3 simulation had been broken since at least April 2026 (51 fail-events / 24h per docs/PI3_EMULATION_BROKEN.md). Two distinct defects compounded: 1. qemu_manager.py hard-coded paths for kernel8.img, a device-tree blob (under a DOS 8.3 short name!), and a 5.4 GiB Raspberry Pi OS SD image — none of which shipped in the repo or were pulled at image build. 2. qemu-system-arm + qemu-utils were missing from the Docker image entirely, so even with the boot files in place QEMU couldn't launch. Add both to Dockerfile.standalone (~200 MB). The architecture fix is a new `app.services.boot_images` module: * Manifest-driven (boot_images/manifest.json, versioned in repo, declares SHA256 + size for each file, supports an optional `compressed.{encoding,sha256,size_bytes}` block for assets shipped as .zst). * `BootImageProvider` materialises files lazily, atomically (temp + rename), verifies SHA256 pre- AND post-decompression, caches under /var/cache/velxio/boot-images, serialises concurrent get() calls per image set via asyncio.Lock. * `AssetDownloader` Protocol with two impls: - `LicenseGatedDownloader` — same flow ESP32 / RISC-V QEMU libs use (VELXIO_BINARY_BASE_URL + VELXIO_LICENSE_KEY). - `LocalDirectoryDownloader` — for tests + in-prod use where the licence-module storage is already on the same filesystem (saves the loopback HTTP roundtrip on a 1.4 GiB blob). * `build_downloader_from_env()` picks one — local-dir wins if both sets of env vars are present, so the prod box short-circuits to direct disk reads automatically. * Lifespan hook in qemu_manager.py pre-warms the cache on container boot so first-time user requests don't pay the 30-60 s download + decompress latency. Adding a future board kind (Pi 4 / Pi 5) is now: upload assets via upload-binary.sh, append an entry to manifest.json, register a lifespan pre-warm in the new board's service module. Zero edits to provider.py / downloader.py. Manifest entries for raspberry-pi-3: kernel8.img 9 695 883 bytes (uncompressed) bcm2710-rpi-3-b.dtb 34 687 bytes (uncompressed) raspios-trixie-armhf.img 5 729 419 264 bytes raw / 1 488 002 803 bytes .zst on wire (zstd -19) source: 2026-04-21 build from raspberrypi.com Tests: 21 new unit tests covering manifest parsing, integrity helpers, both downloaders, and the provider's idempotent / concurrent / integrity / decompression / warmup paths. In-process FakeDownloader keeps the suite under 1 s and httpx-free. Docs: new docs/BOOT_IMAGES.md describes the architecture, on-disk layout, named-volume operation, and the procedure for adding a new image set.
2026-05-16 10:41:01 +07:00
from pathlib import Path
from typing import Callable, Awaitable
feat(sim): boot_images module + Pi 3 emulation restored Pi 3 simulation had been broken since at least April 2026 (51 fail-events / 24h per docs/PI3_EMULATION_BROKEN.md). Two distinct defects compounded: 1. qemu_manager.py hard-coded paths for kernel8.img, a device-tree blob (under a DOS 8.3 short name!), and a 5.4 GiB Raspberry Pi OS SD image — none of which shipped in the repo or were pulled at image build. 2. qemu-system-arm + qemu-utils were missing from the Docker image entirely, so even with the boot files in place QEMU couldn't launch. Add both to Dockerfile.standalone (~200 MB). The architecture fix is a new `app.services.boot_images` module: * Manifest-driven (boot_images/manifest.json, versioned in repo, declares SHA256 + size for each file, supports an optional `compressed.{encoding,sha256,size_bytes}` block for assets shipped as .zst). * `BootImageProvider` materialises files lazily, atomically (temp + rename), verifies SHA256 pre- AND post-decompression, caches under /var/cache/velxio/boot-images, serialises concurrent get() calls per image set via asyncio.Lock. * `AssetDownloader` Protocol with two impls: - `LicenseGatedDownloader` — same flow ESP32 / RISC-V QEMU libs use (VELXIO_BINARY_BASE_URL + VELXIO_LICENSE_KEY). - `LocalDirectoryDownloader` — for tests + in-prod use where the licence-module storage is already on the same filesystem (saves the loopback HTTP roundtrip on a 1.4 GiB blob). * `build_downloader_from_env()` picks one — local-dir wins if both sets of env vars are present, so the prod box short-circuits to direct disk reads automatically. * Lifespan hook in qemu_manager.py pre-warms the cache on container boot so first-time user requests don't pay the 30-60 s download + decompress latency. Adding a future board kind (Pi 4 / Pi 5) is now: upload assets via upload-binary.sh, append an entry to manifest.json, register a lifespan pre-warm in the new board's service module. Zero edits to provider.py / downloader.py. Manifest entries for raspberry-pi-3: kernel8.img 9 695 883 bytes (uncompressed) bcm2710-rpi-3-b.dtb 34 687 bytes (uncompressed) raspios-trixie-armhf.img 5 729 419 264 bytes raw / 1 488 002 803 bytes .zst on wire (zstd -19) source: 2026-04-21 build from raspberrypi.com Tests: 21 new unit tests covering manifest parsing, integrity helpers, both downloaders, and the provider's idempotent / concurrent / integrity / decompression / warmup paths. In-process FakeDownloader keeps the suite under 1 s and httpx-free. Docs: new docs/BOOT_IMAGES.md describes the architecture, on-disk layout, named-volume operation, and the procedure for adding a new image set.
2026-05-16 10:41:01 +07:00
from app.core.hooks import register_lifespan_startup
from app.services.boot_images import (
BootImageError,
BootImageProvider,
get_default_provider,
)
logger = logging.getLogger(__name__)
feat(sim): boot_images module + Pi 3 emulation restored Pi 3 simulation had been broken since at least April 2026 (51 fail-events / 24h per docs/PI3_EMULATION_BROKEN.md). Two distinct defects compounded: 1. qemu_manager.py hard-coded paths for kernel8.img, a device-tree blob (under a DOS 8.3 short name!), and a 5.4 GiB Raspberry Pi OS SD image — none of which shipped in the repo or were pulled at image build. 2. qemu-system-arm + qemu-utils were missing from the Docker image entirely, so even with the boot files in place QEMU couldn't launch. Add both to Dockerfile.standalone (~200 MB). The architecture fix is a new `app.services.boot_images` module: * Manifest-driven (boot_images/manifest.json, versioned in repo, declares SHA256 + size for each file, supports an optional `compressed.{encoding,sha256,size_bytes}` block for assets shipped as .zst). * `BootImageProvider` materialises files lazily, atomically (temp + rename), verifies SHA256 pre- AND post-decompression, caches under /var/cache/velxio/boot-images, serialises concurrent get() calls per image set via asyncio.Lock. * `AssetDownloader` Protocol with two impls: - `LicenseGatedDownloader` — same flow ESP32 / RISC-V QEMU libs use (VELXIO_BINARY_BASE_URL + VELXIO_LICENSE_KEY). - `LocalDirectoryDownloader` — for tests + in-prod use where the licence-module storage is already on the same filesystem (saves the loopback HTTP roundtrip on a 1.4 GiB blob). * `build_downloader_from_env()` picks one — local-dir wins if both sets of env vars are present, so the prod box short-circuits to direct disk reads automatically. * Lifespan hook in qemu_manager.py pre-warms the cache on container boot so first-time user requests don't pay the 30-60 s download + decompress latency. Adding a future board kind (Pi 4 / Pi 5) is now: upload assets via upload-binary.sh, append an entry to manifest.json, register a lifespan pre-warm in the new board's service module. Zero edits to provider.py / downloader.py. Manifest entries for raspberry-pi-3: kernel8.img 9 695 883 bytes (uncompressed) bcm2710-rpi-3-b.dtb 34 687 bytes (uncompressed) raspios-trixie-armhf.img 5 729 419 264 bytes raw / 1 488 002 803 bytes .zst on wire (zstd -19) source: 2026-04-21 build from raspberrypi.com Tests: 21 new unit tests covering manifest parsing, integrity helpers, both downloaders, and the provider's idempotent / concurrent / integrity / decompression / warmup paths. In-process FakeDownloader keeps the suite under 1 s and httpx-free. Docs: new docs/BOOT_IMAGES.md describes the architecture, on-disk layout, named-volume operation, and the procedure for adding a new image set.
2026-05-16 10:41:01 +07:00
# Image-set id (matches a key in `boot_images/manifest.json`).
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
PI3_IMAGE_SET = 'raspberry-pi-3-virt'
feat(sim): boot_images module + Pi 3 emulation restored Pi 3 simulation had been broken since at least April 2026 (51 fail-events / 24h per docs/PI3_EMULATION_BROKEN.md). Two distinct defects compounded: 1. qemu_manager.py hard-coded paths for kernel8.img, a device-tree blob (under a DOS 8.3 short name!), and a 5.4 GiB Raspberry Pi OS SD image — none of which shipped in the repo or were pulled at image build. 2. qemu-system-arm + qemu-utils were missing from the Docker image entirely, so even with the boot files in place QEMU couldn't launch. Add both to Dockerfile.standalone (~200 MB). The architecture fix is a new `app.services.boot_images` module: * Manifest-driven (boot_images/manifest.json, versioned in repo, declares SHA256 + size for each file, supports an optional `compressed.{encoding,sha256,size_bytes}` block for assets shipped as .zst). * `BootImageProvider` materialises files lazily, atomically (temp + rename), verifies SHA256 pre- AND post-decompression, caches under /var/cache/velxio/boot-images, serialises concurrent get() calls per image set via asyncio.Lock. * `AssetDownloader` Protocol with two impls: - `LicenseGatedDownloader` — same flow ESP32 / RISC-V QEMU libs use (VELXIO_BINARY_BASE_URL + VELXIO_LICENSE_KEY). - `LocalDirectoryDownloader` — for tests + in-prod use where the licence-module storage is already on the same filesystem (saves the loopback HTTP roundtrip on a 1.4 GiB blob). * `build_downloader_from_env()` picks one — local-dir wins if both sets of env vars are present, so the prod box short-circuits to direct disk reads automatically. * Lifespan hook in qemu_manager.py pre-warms the cache on container boot so first-time user requests don't pay the 30-60 s download + decompress latency. Adding a future board kind (Pi 4 / Pi 5) is now: upload assets via upload-binary.sh, append an entry to manifest.json, register a lifespan pre-warm in the new board's service module. Zero edits to provider.py / downloader.py. Manifest entries for raspberry-pi-3: kernel8.img 9 695 883 bytes (uncompressed) bcm2710-rpi-3-b.dtb 34 687 bytes (uncompressed) raspios-trixie-armhf.img 5 729 419 264 bytes raw / 1 488 002 803 bytes .zst on wire (zstd -19) source: 2026-04-21 build from raspberrypi.com Tests: 21 new unit tests covering manifest parsing, integrity helpers, both downloaders, and the provider's idempotent / concurrent / integrity / decompression / warmup paths. In-process FakeDownloader keeps the suite under 1 s and httpx-free. Docs: new docs/BOOT_IMAGES.md describes the architecture, on-disk layout, named-volume operation, and the procedure for adding a new image set.
2026-05-16 10:41:01 +07:00
# Filenames the provider materialises (must match `name` fields in the
# manifest entry for PI3_IMAGE_SET).
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
PI3_KERNEL_NAME = 'velxio-kernel-arm64'
PI3_INITRAMFS_NAME = 'velxio-initramfs-arm64.cpio.gz'
PI3_ROOTFS_NAME = 'velxio-pi-rootfs-arm64.ext4'
def _find_free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('127.0.0.1', 0))
return s.getsockname()[1]
EventCallback = Callable[[str, dict], Awaitable[None]]
class PiInstance:
"""State for one running Pi board."""
def __init__(self, client_id: str, callback: EventCallback):
self.client_id = client_id
self.callback = callback
# Runtime state
self.process: subprocess.Popen | None = None
self.overlay_path: str | None = None
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
self.serial_port: int = 0 # virtio-console TCP port (/dev/hvc0)
self.gpio_port: int = 0 # virtio-serial protocol TCP port (/dev/vport0p2)
self._serial_writer: asyncio.StreamWriter | None = None
self._gpio_writer: asyncio.StreamWriter | None = None
self._tasks: list[asyncio.Task] = []
self.running = False
async def emit(self, event_type: str, data: dict) -> None:
try:
await self.callback(event_type, data)
except Exception as e:
logger.error('emit(%s): %s', event_type, e)
class QemuManager:
feat(sim): boot_images module + Pi 3 emulation restored Pi 3 simulation had been broken since at least April 2026 (51 fail-events / 24h per docs/PI3_EMULATION_BROKEN.md). Two distinct defects compounded: 1. qemu_manager.py hard-coded paths for kernel8.img, a device-tree blob (under a DOS 8.3 short name!), and a 5.4 GiB Raspberry Pi OS SD image — none of which shipped in the repo or were pulled at image build. 2. qemu-system-arm + qemu-utils were missing from the Docker image entirely, so even with the boot files in place QEMU couldn't launch. Add both to Dockerfile.standalone (~200 MB). The architecture fix is a new `app.services.boot_images` module: * Manifest-driven (boot_images/manifest.json, versioned in repo, declares SHA256 + size for each file, supports an optional `compressed.{encoding,sha256,size_bytes}` block for assets shipped as .zst). * `BootImageProvider` materialises files lazily, atomically (temp + rename), verifies SHA256 pre- AND post-decompression, caches under /var/cache/velxio/boot-images, serialises concurrent get() calls per image set via asyncio.Lock. * `AssetDownloader` Protocol with two impls: - `LicenseGatedDownloader` — same flow ESP32 / RISC-V QEMU libs use (VELXIO_BINARY_BASE_URL + VELXIO_LICENSE_KEY). - `LocalDirectoryDownloader` — for tests + in-prod use where the licence-module storage is already on the same filesystem (saves the loopback HTTP roundtrip on a 1.4 GiB blob). * `build_downloader_from_env()` picks one — local-dir wins if both sets of env vars are present, so the prod box short-circuits to direct disk reads automatically. * Lifespan hook in qemu_manager.py pre-warms the cache on container boot so first-time user requests don't pay the 30-60 s download + decompress latency. Adding a future board kind (Pi 4 / Pi 5) is now: upload assets via upload-binary.sh, append an entry to manifest.json, register a lifespan pre-warm in the new board's service module. Zero edits to provider.py / downloader.py. Manifest entries for raspberry-pi-3: kernel8.img 9 695 883 bytes (uncompressed) bcm2710-rpi-3-b.dtb 34 687 bytes (uncompressed) raspios-trixie-armhf.img 5 729 419 264 bytes raw / 1 488 002 803 bytes .zst on wire (zstd -19) source: 2026-04-21 build from raspberrypi.com Tests: 21 new unit tests covering manifest parsing, integrity helpers, both downloaders, and the provider's idempotent / concurrent / integrity / decompression / warmup paths. In-process FakeDownloader keeps the suite under 1 s and httpx-free. Docs: new docs/BOOT_IMAGES.md describes the architecture, on-disk layout, named-volume operation, and the procedure for adding a new image set.
2026-05-16 10:41:01 +07:00
def __init__(self, provider: BootImageProvider | None = None):
self._instances: dict[str, PiInstance] = {}
feat(sim): boot_images module + Pi 3 emulation restored Pi 3 simulation had been broken since at least April 2026 (51 fail-events / 24h per docs/PI3_EMULATION_BROKEN.md). Two distinct defects compounded: 1. qemu_manager.py hard-coded paths for kernel8.img, a device-tree blob (under a DOS 8.3 short name!), and a 5.4 GiB Raspberry Pi OS SD image — none of which shipped in the repo or were pulled at image build. 2. qemu-system-arm + qemu-utils were missing from the Docker image entirely, so even with the boot files in place QEMU couldn't launch. Add both to Dockerfile.standalone (~200 MB). The architecture fix is a new `app.services.boot_images` module: * Manifest-driven (boot_images/manifest.json, versioned in repo, declares SHA256 + size for each file, supports an optional `compressed.{encoding,sha256,size_bytes}` block for assets shipped as .zst). * `BootImageProvider` materialises files lazily, atomically (temp + rename), verifies SHA256 pre- AND post-decompression, caches under /var/cache/velxio/boot-images, serialises concurrent get() calls per image set via asyncio.Lock. * `AssetDownloader` Protocol with two impls: - `LicenseGatedDownloader` — same flow ESP32 / RISC-V QEMU libs use (VELXIO_BINARY_BASE_URL + VELXIO_LICENSE_KEY). - `LocalDirectoryDownloader` — for tests + in-prod use where the licence-module storage is already on the same filesystem (saves the loopback HTTP roundtrip on a 1.4 GiB blob). * `build_downloader_from_env()` picks one — local-dir wins if both sets of env vars are present, so the prod box short-circuits to direct disk reads automatically. * Lifespan hook in qemu_manager.py pre-warms the cache on container boot so first-time user requests don't pay the 30-60 s download + decompress latency. Adding a future board kind (Pi 4 / Pi 5) is now: upload assets via upload-binary.sh, append an entry to manifest.json, register a lifespan pre-warm in the new board's service module. Zero edits to provider.py / downloader.py. Manifest entries for raspberry-pi-3: kernel8.img 9 695 883 bytes (uncompressed) bcm2710-rpi-3-b.dtb 34 687 bytes (uncompressed) raspios-trixie-armhf.img 5 729 419 264 bytes raw / 1 488 002 803 bytes .zst on wire (zstd -19) source: 2026-04-21 build from raspberrypi.com Tests: 21 new unit tests covering manifest parsing, integrity helpers, both downloaders, and the provider's idempotent / concurrent / integrity / decompression / warmup paths. In-process FakeDownloader keeps the suite under 1 s and httpx-free. Docs: new docs/BOOT_IMAGES.md describes the architecture, on-disk layout, named-volume operation, and the procedure for adding a new image set.
2026-05-16 10:41:01 +07:00
# The provider is resolved lazily on first boot so importing
# this module never triggers a download or even a manifest
# parse. Injected via the constructor for tests; production
# uses `get_default_provider()` on first use.
self._provider = provider
# ── Public API ────────────────────────────────────────────────────────────
def start_instance(self, client_id: str, board_type: str, # noqa: ARG002
callback: EventCallback) -> None:
if client_id in self._instances:
logger.warning('start_instance: %s already running', client_id)
return
inst = PiInstance(client_id, callback)
self._instances[client_id] = inst
asyncio.create_task(self._boot(inst))
def stop_instance(self, client_id: str) -> None:
inst = self._instances.pop(client_id, None)
if inst:
asyncio.create_task(self._shutdown(inst))
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:
asyncio.create_task(self._send_gpio(inst, int(pin), bool(state)))
async def send_serial_bytes(self, client_id: str, data: bytes) -> None:
inst = self._instances.get(client_id)
if not inst:
logger.warning('send_serial_bytes: no instance for client_id=%s', client_id)
return
if not inst._serial_writer:
logger.warning('send_serial_bytes: %s has no serial writer (qemu not connected yet?)', client_id)
return
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
logger.info('send_serial_bytes: %s sending %d bytes: %r',
client_id, len(data), bytes(data[:32]))
inst._serial_writer.write(data)
try:
await inst._serial_writer.drain()
except Exception as e:
logger.warning('send_serial_bytes drain: %s', e)
# ── Boot sequence ─────────────────────────────────────────────────────────
async def _boot(self, inst: PiInstance) -> None:
feat(sim): boot_images module + Pi 3 emulation restored Pi 3 simulation had been broken since at least April 2026 (51 fail-events / 24h per docs/PI3_EMULATION_BROKEN.md). Two distinct defects compounded: 1. qemu_manager.py hard-coded paths for kernel8.img, a device-tree blob (under a DOS 8.3 short name!), and a 5.4 GiB Raspberry Pi OS SD image — none of which shipped in the repo or were pulled at image build. 2. qemu-system-arm + qemu-utils were missing from the Docker image entirely, so even with the boot files in place QEMU couldn't launch. Add both to Dockerfile.standalone (~200 MB). The architecture fix is a new `app.services.boot_images` module: * Manifest-driven (boot_images/manifest.json, versioned in repo, declares SHA256 + size for each file, supports an optional `compressed.{encoding,sha256,size_bytes}` block for assets shipped as .zst). * `BootImageProvider` materialises files lazily, atomically (temp + rename), verifies SHA256 pre- AND post-decompression, caches under /var/cache/velxio/boot-images, serialises concurrent get() calls per image set via asyncio.Lock. * `AssetDownloader` Protocol with two impls: - `LicenseGatedDownloader` — same flow ESP32 / RISC-V QEMU libs use (VELXIO_BINARY_BASE_URL + VELXIO_LICENSE_KEY). - `LocalDirectoryDownloader` — for tests + in-prod use where the licence-module storage is already on the same filesystem (saves the loopback HTTP roundtrip on a 1.4 GiB blob). * `build_downloader_from_env()` picks one — local-dir wins if both sets of env vars are present, so the prod box short-circuits to direct disk reads automatically. * Lifespan hook in qemu_manager.py pre-warms the cache on container boot so first-time user requests don't pay the 30-60 s download + decompress latency. Adding a future board kind (Pi 4 / Pi 5) is now: upload assets via upload-binary.sh, append an entry to manifest.json, register a lifespan pre-warm in the new board's service module. Zero edits to provider.py / downloader.py. Manifest entries for raspberry-pi-3: kernel8.img 9 695 883 bytes (uncompressed) bcm2710-rpi-3-b.dtb 34 687 bytes (uncompressed) raspios-trixie-armhf.img 5 729 419 264 bytes raw / 1 488 002 803 bytes .zst on wire (zstd -19) source: 2026-04-21 build from raspberrypi.com Tests: 21 new unit tests covering manifest parsing, integrity helpers, both downloaders, and the provider's idempotent / concurrent / integrity / decompression / warmup paths. In-process FakeDownloader keeps the suite under 1 s and httpx-free. Docs: new docs/BOOT_IMAGES.md describes the architecture, on-disk layout, named-volume operation, and the procedure for adding a new image set.
2026-05-16 10:41:01 +07:00
# Resolve boot files via the provider (downloads + verifies on
# first call; cache hit on subsequent calls thanks to the
# lifespan pre-warm at module load).
try:
images = await self._get_provider().get(PI3_IMAGE_SET)
except BootImageError as exc:
logger.error('[pi3] boot-image provisioning failed: %s', exc)
await inst.emit('error', {
'message': f'Raspberry Pi 3 boot files unavailable: {exc}',
})
self._instances.pop(inst.client_id, None)
return
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
kernel_path: Path = images[PI3_KERNEL_NAME]
initramfs_path: Path = images[PI3_INITRAMFS_NAME]
rootfs_base: Path = images[PI3_ROOTFS_NAME]
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
# Allocate TCP ports for the two chardevs.
inst.serial_port = _find_free_port()
inst.gpio_port = _find_free_port()
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
# Create overlay qcow2 backed by the velxio rootfs ext4. Each
# session gets its own writable layer; reads cascade down to
# the shared base, writes go into the per-session overlay
# which is deleted on stop.
overlay = tempfile.NamedTemporaryFile(suffix='.qcow2', delete=False)
overlay.close()
inst.overlay_path = overlay.name
try:
subprocess.run(
['qemu-img', 'create', '-f', 'qcow2',
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
'-b', str(rootfs_base), '-F', 'raw',
inst.overlay_path],
check=True, capture_output=True,
)
except subprocess.CalledProcessError as e:
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
await inst.emit('error',
{'message': f'qemu-img create failed: '
f'{e.stderr.decode()}'})
self._instances.pop(inst.client_id, None)
return
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
# Build QEMU command for -M virt + virtio devices.
#
# Why virt: see project/pi-emulation/decisions.md (D1). Short
# version: raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12,
# virtio-console isn't.
#
# Why no -dtb: virt machine generates its own DTB on the fly
# from the runtime device list, so we don't ship one.
cmd = [
'qemu-system-aarch64',
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
'-M', 'virt',
'-cpu', 'cortex-a53',
'-smp', '4',
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
'-m', '1G',
'-kernel', str(kernel_path),
'-initrd', str(initramfs_path),
# Root filesystem via virtio-blk over PCI. virt machine
# uses PCI as the primary virtio transport, so we use
# `virtio-blk-pci` (not `virtio-blk-device`, which is for
# mmio and silently leaves /dev/vda unregistered).
'-drive', f'if=none,file={inst.overlay_path},format=qcow2,id=rootfs',
'-device', 'virtio-blk-pci,drive=rootfs',
# No default network / display / monitor / serial — we add
# exactly the two chardev-backed virtio-serial ports we
# need. -nographic auto-binds -serial mon:stdio which
# collides with our explicit -chardev IDs.
'-nic', 'none',
'-display', 'none',
'-monitor', 'none',
'-serial', 'none',
# Console: TCP chardev → virtio-console → /dev/hvc0 inside
# the guest. The frontend serial WebSocket connects to this
# port (replaces the old ttyAMA0 path).
'-chardev', f'socket,id=cons,host=127.0.0.1,port={inst.serial_port},'
f'server=on,wait=off',
'-device', 'virtio-serial-pci,id=virtio-serial0',
'-device', 'virtconsole,chardev=cons',
# Protocol channel: second virtserialport on the SAME
# virtio-serial controller. Inside the guest this is
# /dev/vport0p2. Phase 2 hooks this up to pi_protocol_mux
# for GPIO/I2C/SPI/UART/PWM.
'-chardev', f'socket,id=proto,host=127.0.0.1,port={inst.gpio_port},'
f'server=on,wait=off',
'-device', 'virtserialport,chardev=proto,name=velxio-protocol',
# Kernel cmdline:
# console=hvc0 — the virtio-console is the user terminal.
# root=/dev/vda — the virtio-blk overlay is the rootfs.
# rw — userspace can write (overlay catches writes).
# quiet — suppress most printk so the user sees the shell
# banner cleanly.
# panic=10 — auto-reboot 10 s after a panic instead of
# hanging forever (defensive against bad user
# rootfs uploads in Phase 4).
'-append', 'console=hvc0 root=/dev/vda rw quiet panic=10',
]
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
logger.info('Launching QEMU for %s: %s',
inst.client_id, ' '.join(cmd))
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
# Use subprocess.Popen via executor — asyncio.create_subprocess_exec
# requires ProactorEventLoop on Windows but uvicorn may use
# SelectorEventLoop.
loop = asyncio.get_running_loop()
try:
inst.process = await loop.run_in_executor(
None,
lambda: subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
stdin=subprocess.DEVNULL,
),
)
except FileNotFoundError:
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
await inst.emit('error',
{'message': 'qemu-system-aarch64 not found in PATH'})
self._instances.pop(inst.client_id, None)
return
inst.running = True
await inst.emit('system', {'event': 'booting'})
# Give QEMU a moment to open its TCP sockets
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
await asyncio.sleep(1.0)
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
# Connect to the two chardev TCP ports.
inst._tasks.append(asyncio.create_task(self._connect_serial(inst)))
inst._tasks.append(asyncio.create_task(self._connect_gpio(inst)))
inst._tasks.append(asyncio.create_task(self._watch_stderr(inst)))
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
# ── Console (virtio-console / /dev/hvc0) ──────────────────────────────────
async def _connect_serial(self, inst: PiInstance) -> None:
for attempt in range(10):
try:
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
reader, writer = await asyncio.open_connection(
'127.0.0.1', inst.serial_port,
)
inst._serial_writer = writer
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
logger.info('%s: serial connected on port %d',
inst.client_id, inst.serial_port)
await inst.emit('system', {'event': 'booted'})
await self._read_serial(inst, reader)
return
except (ConnectionRefusedError, OSError):
await asyncio.sleep(1.0 * (attempt + 1))
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
await inst.emit('error',
{'message': 'Could not connect to QEMU console port'})
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
async def _read_serial(self, inst: PiInstance,
reader: asyncio.StreamReader) -> None:
buf = bytearray()
while inst.running:
try:
chunk = await asyncio.wait_for(reader.read(256), timeout=0.1)
if not chunk:
break
buf.extend(chunk)
text = buf.decode('utf-8', errors='replace')
buf.clear()
await inst.emit('serial_output', {'data': text})
except asyncio.TimeoutError:
continue
except Exception as e:
logger.warning('%s serial read: %s', inst.client_id, e)
break
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
# ── Protocol channel (virtio-serial / /dev/vport0p2) ──────────────────────
# Methods keep the historical "_gpio" naming so the rest of the
# codebase (simulation route, GPIO event bus) doesn't have to
# rename. Phase 2 swaps the line parser for the full multi-protocol
# mux while keeping this connect/read/write plumbing identical.
async def _connect_gpio(self, inst: PiInstance) -> None:
for attempt in range(10):
try:
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
reader, writer = await asyncio.open_connection(
'127.0.0.1', inst.gpio_port,
)
inst._gpio_writer = writer
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
logger.info('%s: protocol channel connected on port %d',
inst.client_id, inst.gpio_port)
await self._read_gpio(inst, reader)
return
except (ConnectionRefusedError, OSError):
await asyncio.sleep(1.0 * (attempt + 1))
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
logger.warning('%s: protocol channel connection failed',
inst.client_id)
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
async def _read_gpio(self, inst: PiInstance,
reader: asyncio.StreamReader) -> None:
"""Parse text-protocol lines from the Pi shim layer.
Phase 1 understands GPIO only (existing protocol). Phase 2
extends this to dispatch I2C/SPI/UART/PWM as well via
pi_protocol_mux.
"""
linebuf = b''
while inst.running:
try:
chunk = await asyncio.wait_for(reader.read(256), timeout=0.1)
if not chunk:
break
linebuf += chunk
while b'\n' in linebuf:
line, linebuf = linebuf.split(b'\n', 1)
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
await self._handle_gpio_line(
inst, line.decode('ascii', 'ignore').strip(),
)
except asyncio.TimeoutError:
continue
except Exception as e:
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
logger.warning('%s protocol read: %s', inst.client_id, e)
break
async def _handle_gpio_line(self, inst: PiInstance, line: str) -> None:
# Expected: "GPIO <bcm_pin> <0|1>"
parts = line.split()
if len(parts) == 3 and parts[0] == 'GPIO':
try:
pin = int(parts[1])
state = int(parts[2])
await inst.emit('gpio_change', {'pin': pin, 'state': state})
except ValueError:
pass
async def _send_gpio(self, inst: PiInstance, pin: int, state: bool) -> None:
if inst._gpio_writer:
msg = f'SET {pin} {1 if state else 0}\n'.encode()
inst._gpio_writer.write(msg)
try:
await inst._gpio_writer.drain()
except Exception as e:
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
logger.warning('%s protocol send: %s', inst.client_id, e)
# ── QEMU stderr watcher ───────────────────────────────────────────────────
async def _watch_stderr(self, inst: PiInstance) -> None:
if not inst.process or not inst.process.stderr:
return
loop = asyncio.get_running_loop()
try:
while inst.running:
line = await loop.run_in_executor(None, inst.process.stderr.readline)
if not line:
break
text = line.decode('utf-8', errors='replace').rstrip()
if text:
logger.warning('QEMU[%s] %s', inst.client_id, text)
except Exception:
pass
logger.info('QEMU[%s] process exited', inst.client_id)
inst.running = False
await inst.emit('system', {'event': 'exited'})
# ── Shutdown ──────────────────────────────────────────────────────────────
async def _shutdown(self, inst: PiInstance) -> None:
inst.running = False
for task in inst._tasks:
task.cancel()
inst._tasks.clear()
if inst._gpio_writer:
try:
inst._gpio_writer.close()
except Exception:
pass
inst._gpio_writer = None
if inst._serial_writer:
try:
inst._serial_writer.close()
except Exception:
pass
inst._serial_writer = None
if inst.process:
loop = asyncio.get_running_loop()
try:
inst.process.terminate()
await asyncio.wait_for(
loop.run_in_executor(None, inst.process.wait),
timeout=5.0,
)
except Exception:
try:
inst.process.kill()
except Exception:
pass
inst.process = None
# Delete overlay
if inst.overlay_path and os.path.exists(inst.overlay_path):
try:
os.unlink(inst.overlay_path)
except Exception:
pass
inst.overlay_path = None
logger.info('PiInstance %s shut down', inst.client_id)
# ── Helpers ───────────────────────────────────────────────────────────────
feat(sim): boot_images module + Pi 3 emulation restored Pi 3 simulation had been broken since at least April 2026 (51 fail-events / 24h per docs/PI3_EMULATION_BROKEN.md). Two distinct defects compounded: 1. qemu_manager.py hard-coded paths for kernel8.img, a device-tree blob (under a DOS 8.3 short name!), and a 5.4 GiB Raspberry Pi OS SD image — none of which shipped in the repo or were pulled at image build. 2. qemu-system-arm + qemu-utils were missing from the Docker image entirely, so even with the boot files in place QEMU couldn't launch. Add both to Dockerfile.standalone (~200 MB). The architecture fix is a new `app.services.boot_images` module: * Manifest-driven (boot_images/manifest.json, versioned in repo, declares SHA256 + size for each file, supports an optional `compressed.{encoding,sha256,size_bytes}` block for assets shipped as .zst). * `BootImageProvider` materialises files lazily, atomically (temp + rename), verifies SHA256 pre- AND post-decompression, caches under /var/cache/velxio/boot-images, serialises concurrent get() calls per image set via asyncio.Lock. * `AssetDownloader` Protocol with two impls: - `LicenseGatedDownloader` — same flow ESP32 / RISC-V QEMU libs use (VELXIO_BINARY_BASE_URL + VELXIO_LICENSE_KEY). - `LocalDirectoryDownloader` — for tests + in-prod use where the licence-module storage is already on the same filesystem (saves the loopback HTTP roundtrip on a 1.4 GiB blob). * `build_downloader_from_env()` picks one — local-dir wins if both sets of env vars are present, so the prod box short-circuits to direct disk reads automatically. * Lifespan hook in qemu_manager.py pre-warms the cache on container boot so first-time user requests don't pay the 30-60 s download + decompress latency. Adding a future board kind (Pi 4 / Pi 5) is now: upload assets via upload-binary.sh, append an entry to manifest.json, register a lifespan pre-warm in the new board's service module. Zero edits to provider.py / downloader.py. Manifest entries for raspberry-pi-3: kernel8.img 9 695 883 bytes (uncompressed) bcm2710-rpi-3-b.dtb 34 687 bytes (uncompressed) raspios-trixie-armhf.img 5 729 419 264 bytes raw / 1 488 002 803 bytes .zst on wire (zstd -19) source: 2026-04-21 build from raspberrypi.com Tests: 21 new unit tests covering manifest parsing, integrity helpers, both downloaders, and the provider's idempotent / concurrent / integrity / decompression / warmup paths. In-process FakeDownloader keeps the suite under 1 s and httpx-free. Docs: new docs/BOOT_IMAGES.md describes the architecture, on-disk layout, named-volume operation, and the procedure for adding a new image set.
2026-05-16 10:41:01 +07:00
def _get_provider(self) -> BootImageProvider:
"""Lazily resolve the boot-image provider on first boot.
Building the provider eagerly at module import would try to
read the env vars at import time, which races with .env loading
in some entrypoints. Lazy resolution keeps that hazard local
to the boot path.
"""
if self._provider is None:
self._provider = get_default_provider()
return self._provider
# ── Lifespan pre-warm ────────────────────────────────────────────────────────
async def _prewarm_pi3_boot_images() -> None:
feat(pi3): switch from raspi3b to virt + virtio (Phase 1) raspi3b pl011 RX is broken in QEMU 10 + kernel 6.12 — see project/pi-emulation/decisions.md for the full debugging trail. This commit lands Phase 1 of the rebuild: switch the QEMU machine to virt + cortex-a53, boot the velxio kernel/initramfs/rootfs over virtio-blk-pci, and expose the user shell on /dev/hvc0 via virtio-serial-pci + virtconsole. End-to-end smoke verified: boot → agetty autologin → bash prompt → echo round-trip returns the typed token. Tested inside the prod container with QEMU 10.0.8 and our cloud-derived kernel 6.12.88. What changed: backend/app/services/qemu_manager.py PI3_IMAGE_SET -> raspberry-pi-3-virt PI3_KERNEL_NAME / PI3_INITRAMFS_NAME / PI3_ROOTFS_NAME new QEMU cmd rewritten end-to-end: -M virt -cpu cortex-a53 -smp 4 -m 1G -kernel <velxio-kernel-arm64> -initrd <velxio-initramfs-arm64.cpio.gz> -drive ... -device virtio-blk-pci (NOT virtio-blk-device — mmio variant left /dev/vda unregistered) -nic none -display none -monitor none -serial none -chardev socket... -device virtio-serial-pci -device virtconsole (user console -> /dev/hvc0) -chardev socket... -device virtserialport,name=velxio-protocol (Phase 2 channel -> /dev/vport0p2) No -dtb (virt generates its own), no -append init=... (kernel runs our initramfs which then switch_root to rootfs and exec's its /sbin/init — Alpine OpenRC). backend/app/services/boot_images/manifest.json New image set raspberry-pi-3-virt with three assets uploaded via the existing license-endpoint pipeline. Old raspberry-pi-3 entry flagged deprecated:true and kept for one release for rollback. test/pi3_console_boot/test_pi3_console_boot.py Updated QEMU argv to match qemu_manager exactly. Markers now look for the Velxio Pi Simulator MOTD + 'login on hvc0' (autologin proof). Round-trip echo still required to pass.
2026-05-18 10:36:02 +07:00
"""Lifespan hook: download + cache the Pi 3 virt boot files in the
feat(sim): boot_images module + Pi 3 emulation restored Pi 3 simulation had been broken since at least April 2026 (51 fail-events / 24h per docs/PI3_EMULATION_BROKEN.md). Two distinct defects compounded: 1. qemu_manager.py hard-coded paths for kernel8.img, a device-tree blob (under a DOS 8.3 short name!), and a 5.4 GiB Raspberry Pi OS SD image — none of which shipped in the repo or were pulled at image build. 2. qemu-system-arm + qemu-utils were missing from the Docker image entirely, so even with the boot files in place QEMU couldn't launch. Add both to Dockerfile.standalone (~200 MB). The architecture fix is a new `app.services.boot_images` module: * Manifest-driven (boot_images/manifest.json, versioned in repo, declares SHA256 + size for each file, supports an optional `compressed.{encoding,sha256,size_bytes}` block for assets shipped as .zst). * `BootImageProvider` materialises files lazily, atomically (temp + rename), verifies SHA256 pre- AND post-decompression, caches under /var/cache/velxio/boot-images, serialises concurrent get() calls per image set via asyncio.Lock. * `AssetDownloader` Protocol with two impls: - `LicenseGatedDownloader` — same flow ESP32 / RISC-V QEMU libs use (VELXIO_BINARY_BASE_URL + VELXIO_LICENSE_KEY). - `LocalDirectoryDownloader` — for tests + in-prod use where the licence-module storage is already on the same filesystem (saves the loopback HTTP roundtrip on a 1.4 GiB blob). * `build_downloader_from_env()` picks one — local-dir wins if both sets of env vars are present, so the prod box short-circuits to direct disk reads automatically. * Lifespan hook in qemu_manager.py pre-warms the cache on container boot so first-time user requests don't pay the 30-60 s download + decompress latency. Adding a future board kind (Pi 4 / Pi 5) is now: upload assets via upload-binary.sh, append an entry to manifest.json, register a lifespan pre-warm in the new board's service module. Zero edits to provider.py / downloader.py. Manifest entries for raspberry-pi-3: kernel8.img 9 695 883 bytes (uncompressed) bcm2710-rpi-3-b.dtb 34 687 bytes (uncompressed) raspios-trixie-armhf.img 5 729 419 264 bytes raw / 1 488 002 803 bytes .zst on wire (zstd -19) source: 2026-04-21 build from raspberrypi.com Tests: 21 new unit tests covering manifest parsing, integrity helpers, both downloaders, and the provider's idempotent / concurrent / integrity / decompression / warmup paths. In-process FakeDownloader keeps the suite under 1 s and httpx-free. Docs: new docs/BOOT_IMAGES.md describes the architecture, on-disk layout, named-volume operation, and the procedure for adding a new image set.
2026-05-16 10:41:01 +07:00
background at process start.
The cache check is cheap when files are already on disk (named
docker volume), so this is a no-op for warm containers and a one-
time pay-on-first-boot for fresh hosts. Failures are logged but
never block startup a missing licence key or a velxio.dev outage
just means the first user request gets an error with a useful
message, instead of the whole backend refusing to start.
"""
try:
provider = get_default_provider()
except Exception as exc: # noqa: BLE001 - log + continue at startup
logger.warning(
'[pi3] cannot build boot-image provider, skipping pre-warm: %s',
exc,
)
return
asyncio.create_task(provider.warmup(PI3_IMAGE_SET))
register_lifespan_startup(_prewarm_pi3_boot_images)
qemu_manager = QemuManager()