velxio/backend/app/services/qemu_manager.py

869 lines
36 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(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
# Per-board configuration. Pi 3/4/5 share the same arm64 image set;
# Pi Zero/1/2 (Phase 3 deliverable) will share an armhf image set.
# Only -cpu, -smp, -m, qemu binary and which image-set the provider
# fetches vary per model.
#
# CPU choice notes:
# raspberry-pi-3 → Cortex-A53 (BCM2837, ARMv8 64-bit)
# raspberry-pi-4 → Cortex-A72 (BCM2711, ARMv8 64-bit)
# raspberry-pi-5 → Cortex-A76 (BCM2712, ARMv8 64-bit)
PI_CONFIGS: dict[str, dict] = {
'raspberry-pi-3': {
'qemu': 'qemu-system-aarch64',
'cpu': 'cortex-a53',
'smp': '4',
'memory': '1G',
'image_set': 'raspberry-pi-3-virt',
'kernel': 'velxio-kernel-arm64',
'initramfs': 'velxio-initramfs-arm64.cpio.gz',
'rootfs': 'velxio-pi-rootfs-arm64.ext4',
feat(pi): Phase 3.3 — Pi Zero / Pi 1 / Pi 2 armhf simulators Closes the deferred Phase 3.3. Root-causes the Pi 2 "Attempted to kill init" panic as `mount /dev/vda` failing with EINVAL — Debian armmp does not have ext4 builtin (only fuseblk in /proc/filesystems). - qemu_manager: PI_CONFIGS gains raspberry-pi-zero / -1 / -2 entries. All three use the armmp armhf kernel + Cortex-A7 CPU + the mmio virtio transport (arm-32 virt PCI fails -75 due to missing reg DT property). Pi Zero / Pi 1 get the small 1-core / 512 MB profile; Pi 2 gets 4-core / 1 GB. QEMU command builder branches on cfg.bus for virtio-blk-pci vs virtio-blk-device (and serial likewise). - manifest.json: new `raspberry-pi-armhf` image_set wiring three assets (kernel + initramfs + zstd rootfs). - Frontend BoardKind gains the three new kinds + an isPiBoardKind() helper. Replaces the eight scattered `=== 'raspberry-pi-3' || === 'raspberry-pi-4' || === 'raspberry-pi-5'` branches in useSimulatorStore, Interconnect, loadExample, boardProtocols. ComponentRegistry gets three new picker entries. - board-kinds-coverage test: ACCEPTED_UNCOVERED gains the new kinds (backend boards have no canvas examples). The matching armhf build-pi-kernel.sh / build-pi-rootfs.sh changes live in velxio-prod's scripts/ (private overlay) — the upstream kernel build script only knows about arm64; armhf is built in the private repo because the assets ship through the license endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 04:23:48 +07:00
'bus': 'pci',
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
},
'raspberry-pi-4': {
'qemu': 'qemu-system-aarch64',
'cpu': 'cortex-a72',
'smp': '4',
'memory': '2G',
'image_set': 'raspberry-pi-3-virt', # same arm64 image set as Pi 3
'kernel': 'velxio-kernel-arm64',
'initramfs': 'velxio-initramfs-arm64.cpio.gz',
'rootfs': 'velxio-pi-rootfs-arm64.ext4',
feat(pi): Phase 3.3 — Pi Zero / Pi 1 / Pi 2 armhf simulators Closes the deferred Phase 3.3. Root-causes the Pi 2 "Attempted to kill init" panic as `mount /dev/vda` failing with EINVAL — Debian armmp does not have ext4 builtin (only fuseblk in /proc/filesystems). - qemu_manager: PI_CONFIGS gains raspberry-pi-zero / -1 / -2 entries. All three use the armmp armhf kernel + Cortex-A7 CPU + the mmio virtio transport (arm-32 virt PCI fails -75 due to missing reg DT property). Pi Zero / Pi 1 get the small 1-core / 512 MB profile; Pi 2 gets 4-core / 1 GB. QEMU command builder branches on cfg.bus for virtio-blk-pci vs virtio-blk-device (and serial likewise). - manifest.json: new `raspberry-pi-armhf` image_set wiring three assets (kernel + initramfs + zstd rootfs). - Frontend BoardKind gains the three new kinds + an isPiBoardKind() helper. Replaces the eight scattered `=== 'raspberry-pi-3' || === 'raspberry-pi-4' || === 'raspberry-pi-5'` branches in useSimulatorStore, Interconnect, loadExample, boardProtocols. ComponentRegistry gets three new picker entries. - board-kinds-coverage test: ACCEPTED_UNCOVERED gains the new kinds (backend boards have no canvas examples). The matching armhf build-pi-kernel.sh / build-pi-rootfs.sh changes live in velxio-prod's scripts/ (private overlay) — the upstream kernel build script only knows about arm64; armhf is built in the private repo because the assets ship through the license endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 04:23:48 +07:00
'bus': 'pci',
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
},
'raspberry-pi-5': {
'qemu': 'qemu-system-aarch64',
'cpu': 'cortex-a76',
'smp': '4',
'memory': '2G',
'image_set': 'raspberry-pi-3-virt', # same arm64 image set
'kernel': 'velxio-kernel-arm64',
'initramfs': 'velxio-initramfs-arm64.cpio.gz',
'rootfs': 'velxio-pi-rootfs-arm64.ext4',
feat(pi): Phase 3.3 — Pi Zero / Pi 1 / Pi 2 armhf simulators Closes the deferred Phase 3.3. Root-causes the Pi 2 "Attempted to kill init" panic as `mount /dev/vda` failing with EINVAL — Debian armmp does not have ext4 builtin (only fuseblk in /proc/filesystems). - qemu_manager: PI_CONFIGS gains raspberry-pi-zero / -1 / -2 entries. All three use the armmp armhf kernel + Cortex-A7 CPU + the mmio virtio transport (arm-32 virt PCI fails -75 due to missing reg DT property). Pi Zero / Pi 1 get the small 1-core / 512 MB profile; Pi 2 gets 4-core / 1 GB. QEMU command builder branches on cfg.bus for virtio-blk-pci vs virtio-blk-device (and serial likewise). - manifest.json: new `raspberry-pi-armhf` image_set wiring three assets (kernel + initramfs + zstd rootfs). - Frontend BoardKind gains the three new kinds + an isPiBoardKind() helper. Replaces the eight scattered `=== 'raspberry-pi-3' || === 'raspberry-pi-4' || === 'raspberry-pi-5'` branches in useSimulatorStore, Interconnect, loadExample, boardProtocols. ComponentRegistry gets three new picker entries. - board-kinds-coverage test: ACCEPTED_UNCOVERED gains the new kinds (backend boards have no canvas examples). The matching armhf build-pi-kernel.sh / build-pi-rootfs.sh changes live in velxio-prod's scripts/ (private overlay) — the upstream kernel build script only knows about arm64; armhf is built in the private repo because the assets ship through the license endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 04:23:48 +07:00
'bus': 'pci',
},
# ── armhf (32-bit ARM, Pi Zero / 1 / 2) ─────────────────────────────
# QEMU virt for arm-32 does not probe PCI cleanly (the pci-host-generic
# node is missing the "reg" DT property and probe fails -75), so we
# use the MMIO virtio transport instead: -device virtio-blk-device /
# virtio-serial-device. The kernel here is linux-image-armmp (Debian
# generic ARMv7) which ships ext4 + virtio_mmio as MODULES — the
# armhf initramfs bundles ext4.ko + jbd2.ko + mbcache.ko + crc16.ko +
# crc32c_generic.ko and insmods them in dep order before /dev/vda
# mount. Pi Zero / Pi 1 are ARMv6 which the Debian armmp kernel does
# not target; Phase 3.3b tracks sourcing an ARMv6 kernel for those.
'raspberry-pi-2': {
'qemu': 'qemu-system-arm',
'cpu': 'cortex-a7',
'smp': '4',
'memory': '1G',
'image_set': 'raspberry-pi-armhf',
'kernel': 'velxio-kernel-armhf',
'initramfs': 'velxio-initramfs-armhf.cpio.gz',
'rootfs': 'velxio-pi-rootfs-armhf.ext4',
'bus': 'mmio',
},
# Pi 1 / Pi Zero are ARMv6 on real silicon (arm1176, BCM2835). Debian
# dropped the ARMv6 kernel and Alpine's linux-rpi kernel is wired for
# the actual BCM2835 hardware (no virtio_blk, no ext4 module) so it
# cannot boot on QEMU virt. We follow the project's "looks-like-a-Pi
# but isn't-exactly-a-Pi" architecture rule: serve them off the same
# ARMv7 armmp kernel as Pi 2, with the smaller RAM / SMP profile of
# the real boards. User code that uses RPi.GPIO / smbus2 / spidev
# behaves identically. We do not advertise ARMv6 anywhere in the
# rootfs (no /proc/cpuinfo lying).
'raspberry-pi-1': {
'qemu': 'qemu-system-arm',
'cpu': 'cortex-a7',
'smp': '1',
'memory': '512M',
'image_set': 'raspberry-pi-armhf',
'kernel': 'velxio-kernel-armhf',
'initramfs': 'velxio-initramfs-armhf.cpio.gz',
'rootfs': 'velxio-pi-rootfs-armhf.ext4',
'bus': 'mmio',
},
'raspberry-pi-zero': {
'qemu': 'qemu-system-arm',
'cpu': 'cortex-a7',
'smp': '1',
'memory': '512M',
'image_set': 'raspberry-pi-armhf',
'kernel': 'velxio-kernel-armhf',
'initramfs': 'velxio-initramfs-armhf.cpio.gz',
'rootfs': 'velxio-pi-rootfs-armhf.ext4',
'bus': 'mmio',
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
},
}
# Default board if the client doesn't specify one. Kept for clients
# that still send the legacy "start_pi" message without a board field.
DEFAULT_PI_BOARD = 'raspberry-pi-3'
# ── Pluggable I2C/SPI/UART dispatcher ────────────────────────────────────
#
# When the pro overlay loads, it can call
# ``set_pi_protocol_dispatcher(fn)`` to register a coroutine that
# receives the raw protocol tokens for I2C/SPI/UART frames. If the
# dispatcher returns a non-None string, that string is written back
# to the guest as a reply line. Returning None falls through to the
# default no-slave stubs.
#
# Signature: async def(client_id: str, tokens: list[str]) -> str | None
#
# This keeps the upstream OSS code unaware of the pro slave models
# (BME280, MCP23017, ...) while letting the overlay attach real
# behaviour at register_pro() time.
import typing as _typing
_ProtocolDispatcher = _typing.Callable[
[str, list[str]],
_typing.Awaitable[_typing.Optional[str]],
]
_PI_PROTOCOL_DISPATCHER: _ProtocolDispatcher | None = None
def set_pi_protocol_dispatcher(fn: _ProtocolDispatcher | None) -> None:
"""Install (or clear) the pro overlay's I2C/SPI/UART dispatcher."""
global _PI_PROTOCOL_DISPATCHER
_PI_PROTOCOL_DISPATCHER = fn
# Pro overlay can also register a handler for attach/detach WebSocket
# messages from the canvas (e.g. when the user wires a BME280 to the Pi).
# Signature: async def(client_id: str, action: str, data: dict) -> None
# where action is 'attach' or 'detach'.
_SlaveHandler = _typing.Callable[
[str, str, dict],
_typing.Awaitable[None],
]
_PI_SLAVE_HANDLER: _SlaveHandler | None = None
def set_pi_slave_handler(fn: _SlaveHandler | None) -> None:
"""Install (or clear) the pro overlay's slave attach/detach handler."""
global _PI_SLAVE_HANDLER
_PI_SLAVE_HANDLER = fn
def get_pi_slave_handler() -> _SlaveHandler | None:
return _PI_SLAVE_HANDLER
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."""
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
def __init__(self, client_id: str, callback: EventCallback,
board_type: str = DEFAULT_PI_BOARD):
self.client_id = client_id
self.callback = callback
self.board_type = board_type
# 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)
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
self.gpio_port: int = 0 # legacy field (kept for log compat)
self.proto_pipe_base: str | None = None # pipe chardev FIFO basename
self._serial_writer: asyncio.StreamWriter | None = None
self._gpio_writer: asyncio.StreamWriter | None = None
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
# File descriptors for the proto pipe pair (host side).
self._proto_in_fd: int | None = None # we write here → guest reads
self._proto_out_fd: int | None = None # we read here ← guest writes
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 ────────────────────────────────────────────────────────────
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
def start_instance(self, client_id: str, board_type: str,
callback: EventCallback) -> None:
if client_id in self._instances:
logger.warning('start_instance: %s already running', client_id)
return
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
if board_type not in PI_CONFIGS:
logger.warning(
'start_instance: unknown board %r, falling back to %s',
board_type, DEFAULT_PI_BOARD,
)
board_type = DEFAULT_PI_BOARD
inst = PiInstance(client_id, callback, board_type=board_type)
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(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
# Per-board configuration drives the QEMU command, image-set,
# CPU type, RAM, and SMP count. See PI_CONFIGS at the top of
# this module.
cfg = PI_CONFIGS[inst.board_type]
logger.info('[%s] booting %s (cpu=%s mem=%s)',
inst.client_id, inst.board_type, cfg['cpu'], cfg['memory'])
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:
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
images = await self._get_provider().get(cfg['image_set'])
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
except BootImageError as exc:
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
logger.error('[%s] boot-image provisioning failed: %s',
inst.client_id, exc)
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
await inst.emit('error', {
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
'message': f'{inst.board_type} boot files unavailable: {exc}',
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
})
self._instances.pop(inst.client_id, None)
return
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
kernel_path: Path = images[cfg['kernel']]
initramfs_path: Path = images[cfg['initramfs']]
rootfs_base: Path = images[cfg['rootfs']]
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
# Allocate transport endpoints for the two chardevs.
#
# User console: TCP socket — virtconsole on top of a socket
# works bidirectionally (proven by Phase 1).
#
# Protocol channel: pipe (FIFO pair on disk) — `virtserialport`
# on a socket chardev has a guest→host flow bug in QEMU 10
# (data is silently dropped). Pipe chardev sidesteps that;
# see project/pi-emulation/decisions.md D9.
inst.serial_port = _find_free_port()
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
inst.gpio_port = _find_free_port() # kept for legacy log fields
# Make a fresh FIFO pair per session. QEMU's pipe chardev
# appends ".in" (host writes / guest reads) and ".out"
# (guest writes / host reads) to the path.
inst.proto_pipe_base = tempfile.mktemp(prefix="velxio-pi-proto-")
for suffix in (".in", ".out"):
path = inst.proto_pipe_base + suffix
try:
os.mkfifo(path, 0o600)
except FileExistsError:
pass
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.
feat(pi): Phase 3.3 — Pi Zero / Pi 1 / Pi 2 armhf simulators Closes the deferred Phase 3.3. Root-causes the Pi 2 "Attempted to kill init" panic as `mount /dev/vda` failing with EINVAL — Debian armmp does not have ext4 builtin (only fuseblk in /proc/filesystems). - qemu_manager: PI_CONFIGS gains raspberry-pi-zero / -1 / -2 entries. All three use the armmp armhf kernel + Cortex-A7 CPU + the mmio virtio transport (arm-32 virt PCI fails -75 due to missing reg DT property). Pi Zero / Pi 1 get the small 1-core / 512 MB profile; Pi 2 gets 4-core / 1 GB. QEMU command builder branches on cfg.bus for virtio-blk-pci vs virtio-blk-device (and serial likewise). - manifest.json: new `raspberry-pi-armhf` image_set wiring three assets (kernel + initramfs + zstd rootfs). - Frontend BoardKind gains the three new kinds + an isPiBoardKind() helper. Replaces the eight scattered `=== 'raspberry-pi-3' || === 'raspberry-pi-4' || === 'raspberry-pi-5'` branches in useSimulatorStore, Interconnect, loadExample, boardProtocols. ComponentRegistry gets three new picker entries. - board-kinds-coverage test: ACCEPTED_UNCOVERED gains the new kinds (backend boards have no canvas examples). The matching armhf build-pi-kernel.sh / build-pi-rootfs.sh changes live in velxio-prod's scripts/ (private overlay) — the upstream kernel build script only knows about arm64; armhf is built in the private repo because the assets ship through the license endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 04:23:48 +07:00
# Virtio device suffix depends on the bus. arm64 virt has
# working PCI so we use the -pci variants there. arm-32 virt's
# pci-host-generic node has a broken DT (missing "reg" property)
# so the bus never enumerates — we have to fall back to the
# mmio variants. The same device names work for both blk and
# serial; only the suffix changes.
bus = cfg.get('bus', 'pci')
if bus == 'mmio':
blk_dev = 'virtio-blk-device,drive=rootfs'
serial_dev = 'virtio-serial-device,id=virtio-serial0'
else:
blk_dev = 'virtio-blk-pci,drive=rootfs'
serial_dev = 'virtio-serial-pci,id=virtio-serial0'
cmd = [
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
cfg['qemu'],
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',
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
'-cpu', cfg['cpu'],
'-smp', cfg['smp'],
'-m', cfg['memory'],
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', str(kernel_path),
'-initrd', str(initramfs_path),
feat(pi): Phase 3.3 — Pi Zero / Pi 1 / Pi 2 armhf simulators Closes the deferred Phase 3.3. Root-causes the Pi 2 "Attempted to kill init" panic as `mount /dev/vda` failing with EINVAL — Debian armmp does not have ext4 builtin (only fuseblk in /proc/filesystems). - qemu_manager: PI_CONFIGS gains raspberry-pi-zero / -1 / -2 entries. All three use the armmp armhf kernel + Cortex-A7 CPU + the mmio virtio transport (arm-32 virt PCI fails -75 due to missing reg DT property). Pi Zero / Pi 1 get the small 1-core / 512 MB profile; Pi 2 gets 4-core / 1 GB. QEMU command builder branches on cfg.bus for virtio-blk-pci vs virtio-blk-device (and serial likewise). - manifest.json: new `raspberry-pi-armhf` image_set wiring three assets (kernel + initramfs + zstd rootfs). - Frontend BoardKind gains the three new kinds + an isPiBoardKind() helper. Replaces the eight scattered `=== 'raspberry-pi-3' || === 'raspberry-pi-4' || === 'raspberry-pi-5'` branches in useSimulatorStore, Interconnect, loadExample, boardProtocols. ComponentRegistry gets three new picker entries. - board-kinds-coverage test: ACCEPTED_UNCOVERED gains the new kinds (backend boards have no canvas examples). The matching armhf build-pi-kernel.sh / build-pi-rootfs.sh changes live in velxio-prod's scripts/ (private overlay) — the upstream kernel build script only knows about arm64; armhf is built in the private repo because the assets ship through the license endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 04:23:48 +07:00
# Root filesystem via virtio-blk. arm64 uses the pci
# transport (works out of the box on virt-aarch64); armhf
# uses the mmio transport because PCI is broken on
# virt-arm32.
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
'-drive', f'if=none,file={inst.overlay_path},format=qcow2,id=rootfs',
feat(pi): Phase 3.3 — Pi Zero / Pi 1 / Pi 2 armhf simulators Closes the deferred Phase 3.3. Root-causes the Pi 2 "Attempted to kill init" panic as `mount /dev/vda` failing with EINVAL — Debian armmp does not have ext4 builtin (only fuseblk in /proc/filesystems). - qemu_manager: PI_CONFIGS gains raspberry-pi-zero / -1 / -2 entries. All three use the armmp armhf kernel + Cortex-A7 CPU + the mmio virtio transport (arm-32 virt PCI fails -75 due to missing reg DT property). Pi Zero / Pi 1 get the small 1-core / 512 MB profile; Pi 2 gets 4-core / 1 GB. QEMU command builder branches on cfg.bus for virtio-blk-pci vs virtio-blk-device (and serial likewise). - manifest.json: new `raspberry-pi-armhf` image_set wiring three assets (kernel + initramfs + zstd rootfs). - Frontend BoardKind gains the three new kinds + an isPiBoardKind() helper. Replaces the eight scattered `=== 'raspberry-pi-3' || === 'raspberry-pi-4' || === 'raspberry-pi-5'` branches in useSimulatorStore, Interconnect, loadExample, boardProtocols. ComponentRegistry gets three new picker entries. - board-kinds-coverage test: ACCEPTED_UNCOVERED gains the new kinds (backend boards have no canvas examples). The matching armhf build-pi-kernel.sh / build-pi-rootfs.sh changes live in velxio-prod's scripts/ (private overlay) — the upstream kernel build script only knows about arm64; armhf is built in the private repo because the assets ship through the license endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 04:23:48 +07:00
'-device', blk_dev,
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
# 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',
feat(pi): Phase 3.3 — Pi Zero / Pi 1 / Pi 2 armhf simulators Closes the deferred Phase 3.3. Root-causes the Pi 2 "Attempted to kill init" panic as `mount /dev/vda` failing with EINVAL — Debian armmp does not have ext4 builtin (only fuseblk in /proc/filesystems). - qemu_manager: PI_CONFIGS gains raspberry-pi-zero / -1 / -2 entries. All three use the armmp armhf kernel + Cortex-A7 CPU + the mmio virtio transport (arm-32 virt PCI fails -75 due to missing reg DT property). Pi Zero / Pi 1 get the small 1-core / 512 MB profile; Pi 2 gets 4-core / 1 GB. QEMU command builder branches on cfg.bus for virtio-blk-pci vs virtio-blk-device (and serial likewise). - manifest.json: new `raspberry-pi-armhf` image_set wiring three assets (kernel + initramfs + zstd rootfs). - Frontend BoardKind gains the three new kinds + an isPiBoardKind() helper. Replaces the eight scattered `=== 'raspberry-pi-3' || === 'raspberry-pi-4' || === 'raspberry-pi-5'` branches in useSimulatorStore, Interconnect, loadExample, boardProtocols. ComponentRegistry gets three new picker entries. - board-kinds-coverage test: ACCEPTED_UNCOVERED gains the new kinds (backend boards have no canvas examples). The matching armhf build-pi-kernel.sh / build-pi-rootfs.sh changes live in velxio-prod's scripts/ (private overlay) — the upstream kernel build script only knows about arm64; armhf is built in the private repo because the assets ship through the license endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 04:23:48 +07:00
'-device', serial_dev,
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
'-device', 'virtconsole,chardev=cons',
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
# Protocol channel: pipe (FIFO pair) instead of a socket
# chardev. virtserialport on socket chardev has a known
# guest→host flow bug in QEMU 10 (see decisions.md D9).
# Pipe creates <path>.in (host→guest) + <path>.out
# (guest→host) as named FIFOs that QEMU opens lazily.
# Inside the guest this still appears as /dev/vport<N>p<M>
# with the name "velxio-protocol".
'-chardev', f'pipe,id=proto,path={inst.proto_pipe_base}',
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
'-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:
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
"""Open the proto pipe pair. QEMU's `pipe` chardev creates
two FIFOs: <base>.in (we write guest reads via /dev/vport<N>)
and <base>.out (guest writes we read).
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
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
We open both ends non-blocking so opening the not-yet-written
side doesn't deadlock, then register the read FIFO with the
asyncio loop so incoming bytes wake the line parser.
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
"""
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
if not inst.proto_pipe_base:
return
in_path = inst.proto_pipe_base + '.in'
out_path = inst.proto_pipe_base + '.out'
# Wait for the FIFOs to exist (created by _boot before this
# task is spawned). If QEMU failed to start they may never
# appear — bail after a few seconds.
for _ in range(50):
if os.path.exists(in_path) and os.path.exists(out_path):
break
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
await asyncio.sleep(0.1)
else:
logger.warning('%s: proto pipe FIFOs missing; aborting',
inst.client_id)
return
try:
# O_RDWR keeps both ends opened on the host side so the
# FIFOs never reach an EOF state, even if QEMU briefly
# disconnects (e.g. between booting and the guest opening
# /dev/vport<N>). Non-blocking so we can integrate with
# the asyncio loop via add_reader/add_writer.
inst._proto_in_fd = os.open(in_path,
os.O_RDWR | os.O_NONBLOCK)
inst._proto_out_fd = os.open(out_path,
os.O_RDWR | os.O_NONBLOCK)
except OSError as exc:
logger.warning('%s: open proto pipes: %s',
inst.client_id, exc)
return
logger.info('%s: protocol channel opened (pipe %s.{in,out})',
inst.client_id, inst.proto_pipe_base)
loop = asyncio.get_running_loop()
# Use add_reader to integrate the readable FIFO with the loop.
linebuf = bytearray()
def _on_readable() -> None:
fd = inst._proto_out_fd
if fd is None:
return
try:
data = os.read(fd, 4096)
except BlockingIOError:
return
except OSError:
return
if not data:
return
linebuf.extend(data)
while b'\n' in linebuf:
line, _, rest = linebuf.partition(b'\n')
linebuf[:] = rest
asyncio.create_task(self._handle_gpio_line(
inst, line.decode('ascii', 'ignore').strip(),
))
loop.add_reader(inst._proto_out_fd, _on_readable)
# Keep this coroutine alive while inst is running so the loop
# doesn't garbage-collect the reader registration.
while inst.running:
await asyncio.sleep(1.0)
try:
loop.remove_reader(inst._proto_out_fd)
except Exception:
pass
async def _handle_gpio_line(self, inst: PiInstance, line: str) -> None:
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
"""Dispatch a single text-protocol line from the Pi shim layer.
Phase 2 supports the velxio Pi shim wire format:
GPIO <bcm> <0|1> gpio_change event
GPIO_SETUP <bcm> <in|out> <pud> noted (no canvas effect)
GPIO_IN <bcm> reply VAL <bcm> <0|1>
PWM_START <bcm> <freq> <duty> gpio_pwm event
PWM_CHANGE <bcm> <freq> <duty> gpio_pwm event
PWM_STOP <bcm> gpio_pwm stop
I2C / SPI / UART reply I2C_ERR / SPI_DATA empty
(Phase 2.5 wires real bridges)
"""
parts = line.split()
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
if not parts:
return
op = parts[0]
if op == 'GPIO' and len(parts) == 3:
try:
pin = int(parts[1])
state = int(parts[2])
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
await inst.emit('gpio_change',
{'pin': pin, 'state': state})
except ValueError:
pass
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
return
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
if op == 'GPIO_SETUP' and len(parts) >= 3:
# Setup is informational; emit so the frontend can show
# pin direction badges if it wants. No canvas-level wiring
# needed today.
try:
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
pin = int(parts[1])
direction = parts[2]
await inst.emit(
'gpio_setup',
{'pin': pin, 'direction': direction,
'pull': parts[3] if len(parts) > 3 else 'pud_off'},
)
except ValueError:
pass
return
if op == 'GPIO_IN' and len(parts) == 2:
# Reply with the last known state of the pin. For Phase 2
# we just echo 0 — the canvas-side input wiring fans in
# through SET commands which the shim caches on the guest.
# When canvas-driven inputs land in Phase 2.5 this will
# query the gpio event bus' last-state map.
try:
pin = int(parts[1])
await self._reply_gpio(inst, f'VAL {pin} 0')
except ValueError:
pass
return
if op in ('PWM_START', 'PWM_CHANGE') and len(parts) == 4:
try:
pin = int(parts[1])
freq = float(parts[2])
duty = float(parts[3])
await inst.emit('gpio_pwm',
{'pin': pin, 'frequency': freq,
'duty_cycle': duty,
'event': 'start' if op == 'PWM_START' else 'change'})
except ValueError:
pass
return
if op == 'PWM_STOP' and len(parts) == 2:
try:
pin = int(parts[1])
await inst.emit('gpio_pwm',
{'pin': pin, 'event': 'stop'})
except ValueError:
pass
return
# I2C / SPI / UART — if a pro overlay has registered a slave
# dispatcher, route the frame to it; otherwise reply with
# stubs (Phase 2 behaviour) so user code gets a deterministic
# answer instead of hanging.
if op in ('I2C', 'SPI', 'UART'):
disp = _PI_PROTOCOL_DISPATCHER
if disp is not None:
try:
reply = await disp(inst.client_id, parts)
except Exception:
logger.exception('pi-protocol dispatcher crashed')
reply = None
if reply is not None:
await self._reply_gpio(inst, reply)
return
# Fall through to default stubs
if op == 'I2C' and len(parts) >= 4:
sub = parts[3]
if sub in ('R', 'RR'):
await self._reply_gpio(
inst, f'I2C_ERR {parts[1]} {parts[2]} no-slave')
return
if op == 'SPI' and len(parts) >= 4 and parts[3] == 'X':
try:
req_hex = parts[4] if len(parts) > 4 else ''
length = len(bytes.fromhex(req_hex))
except ValueError:
length = 0
await self._reply_gpio(
inst, f'SPI_DATA {parts[1]} {parts[2]} {"00" * length}')
return
if op == 'UART' and len(parts) >= 3 and parts[2] == 'RX_REQ':
await self._reply_gpio(inst, f'UART_RX {parts[1]}')
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
return
# Unknown — log at debug level (not a hot path)
logger.debug('pi-protocol: unhandled line: %r', line)
async def _reply_gpio(self, inst: PiInstance, line: str) -> None:
"""Send a reply frame back to the guest over the protocol
chardev (pipe-backed). Used for any op that the shim layer
waits on."""
if inst._proto_in_fd is None:
return
try:
os.write(inst._proto_in_fd, (line + '\n').encode('ascii'))
except OSError as e:
logger.warning('%s protocol reply: %s', inst.client_id, e)
async def _send_gpio(self, inst: PiInstance, pin: int,
state: bool) -> None:
if inst._proto_in_fd is None:
return
msg = f'SET {pin} {1 if state else 0}\n'.encode()
try:
os.write(inst._proto_in_fd, msg)
except OSError as e:
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
fix(pi3): pipe chardev for proto channel + Phase 2 protocol mux QEMU 10's virtserialport on a socket chardev (server=on,wait=off) silently drops guest→host bytes. Reproduced cleanly: writes from inside the guest to /dev/vport<N>p<M> succeed (no errno) but the connected client socket receives 0 bytes. Same bug whether the client is a single recv loop, multiple threads, TCP or UNIX socket, or whether QEMU runs as server vs client. virtconsole on the same socket works fine — only virtserialport is broken. Workaround: use `pipe` chardev (a pair of named FIFOs created beforehand by qemu_manager). guest→host through .out flows reliably in QEMU 10 — verified with manual test: 'echo PIPE_TEST > /dev/vport1p1' in the guest produces 'PIPE_TEST\n' immediately on the host side. Changes: - qemu_manager._boot: allocate a temp basename, mkfifo .in + .out, pass to QEMU as 'pipe,path=<base>'. - qemu_manager._connect_gpio: open both FIFOs O_RDWR | O_NONBLOCK on host side (O_RDWR keeps the FIFOs open even when guest hasn't opened its side yet), wire .out into asyncio via loop.add_reader. - qemu_manager._reply_gpio / _send_gpio: write to .in fd via os.write. - qemu_manager._handle_gpio_line: extended Phase 1 GPIO-only parser into a full Phase 2 mux: GPIO/GPIO_SETUP/GPIO_IN/PWM_*/I2C/SPI/UART with appropriate replies. - qemu_manager._shutdown: close FDs + unlink the FIFOs. - manifest.json: bump raspberry-pi-3-virt rootfs to 2026.05+phase2-shims (the new rootfs ships the velxio shim Python modules under /usr/lib/velxio-shims/).
2026-05-18 11:51:23 +07:00
# Close the proto pipe FDs and unlink the FIFOs.
for attr in ('_proto_in_fd', '_proto_out_fd'):
fd = getattr(inst, attr, None)
if fd is not None:
try:
os.close(fd)
except OSError:
pass
setattr(inst, attr, None)
if inst.proto_pipe_base:
for suffix in ('.in', '.out'):
p = inst.proto_pipe_base + suffix
try:
os.unlink(p)
except OSError:
pass
inst.proto_pipe_base = 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
# Notify pro overlay (if any) so it can drop its slave registry
# entry for this client. OSS image has no handler → no-op.
if _PI_SLAVE_HANDLER is not None:
try:
await _PI_SLAVE_HANDLER(inst.client_id, 'shutdown', {})
except Exception:
logger.exception('pi-slave shutdown hook crashed')
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 ────────────────────────────────────────────────────────
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
async def _prewarm_pi_boot_images() -> None:
"""Lifespan hook: download + cache the Pi 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.
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
Pre-warms every unique ``image_set`` referenced by PI_CONFIGS
exactly once (Pi 3/4/5 share the arm64 set; Pi Zero/1/2 will add
an armhf set in Phase 3.3). The cache check is cheap when files
are already on disk (named docker volume), so this is a no-op
for warm containers. Failures are logged but never block startup.
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
"""
try:
provider = get_default_provider()
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
except Exception as exc: # noqa: BLE001
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
logger.warning(
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
'[pi] cannot build boot-image provider, skipping pre-warm: %s',
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
exc,
)
return
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
seen: set[str] = set()
for cfg in PI_CONFIGS.values():
if cfg['image_set'] in seen:
continue
seen.add(cfg['image_set'])
asyncio.create_task(provider.warmup(cfg['image_set']))
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
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
register_lifespan_startup(_prewarm_pi_boot_images)
qemu_manager = QemuManager()