From 18a582455ceb39c41ad87451272d938c96b01658 Mon Sep 17 00:00:00 2001 From: davidmonterocrespo24 Date: Mon, 18 May 2026 23:23:48 +0200 Subject: [PATCH] =?UTF-8?q?feat(pi):=20Phase=203.3=20=E2=80=94=20Pi=20Zero?= =?UTF-8?q?=20/=20Pi=201=20/=20Pi=202=20armhf=20simulators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../app/services/boot_images/manifest.json | 31 +++++++ backend/app/services/qemu_manager.py | 82 +++++++++++++++++-- .../__tests__/board-kinds-coverage.test.ts | 7 +- frontend/src/services/ComponentRegistry.ts | 39 +++++++++ frontend/src/simulation/Interconnect.ts | 9 +- frontend/src/store/useSimulatorStore.ts | 24 +++--- frontend/src/types/board.ts | 16 ++++ frontend/src/utils/boardProtocols.ts | 11 +-- frontend/src/utils/loadExample.ts | 9 +- 9 files changed, 186 insertions(+), 42 deletions(-) diff --git a/backend/app/services/boot_images/manifest.json b/backend/app/services/boot_images/manifest.json index 7f1240d8..2d3eca1c 100644 --- a/backend/app/services/boot_images/manifest.json +++ b/backend/app/services/boot_images/manifest.json @@ -33,6 +33,37 @@ } ] }, + "raspberry-pi-armhf": { + "description": "Raspberry Pi Zero / 1 / 2 simulator on QEMU -M virt + cortex-a7. Velxio-built armhf kernel (Debian armmp) + initramfs (bundled ext4 + jbd2 + mbcache + crc16 + crc32c_generic + virtio_blk + virtio_mmio) + Alpine-based armhf rootfs. arm-32 virt PCI is broken (missing reg DT prop) so the QEMU command uses the mmio virtio transport; Pi Zero / Pi 1 share this image set with Pi 2 because the real ARMv6 silicon they have isn't supported by any QEMU-virt-capable upstream kernel.", + "images": [ + { + "name": "velxio-kernel-armhf", + "asset_id": "velxio-kernel-armhf", + "sha256": "9d7377c33b9a5631e99ab109c8d3d9d89f99331b53eb8cc755cd76e775c9d0d5", + "size_bytes": 6033920, + "version": "2026.05+v6.12.88-armmp+ext4mods" + }, + { + "name": "velxio-initramfs-armhf.cpio.gz", + "asset_id": "velxio-initramfs-armhf", + "sha256": "611eb3d219cb5d71a534d417dea564ed6eadbd2d1b40e33d28bb4a5a5f5829f5", + "size_bytes": 1390924, + "version": "2026.05+ext4mods" + }, + { + "name": "velxio-pi-rootfs-armhf.ext4", + "asset_id": "velxio-pi-rootfs-armhf-zst", + "sha256": "5f2eafc0e76acc9ecc79da42313c1ffaacd577f42df5a58934d97ea167fab4ee", + "size_bytes": 629145600, + "version": "2026.05+phase2-shims-armhf", + "compressed": { + "encoding": "zstd", + "sha256": "ce9037b371aa1e847f2ce16bf6b0067c4ef51d48b66fe4c2d79941cbdff37b30", + "size_bytes": 23322299 + } + } + ] + }, "raspberry-pi-3": { "description": "[DEPRECATED \u2014 kept for one release to ease rollback] Original raspi3b setup. Replaced by 'raspberry-pi-3-virt' (see above). qemu_manager.py no longer references this entry; do not add new clients here.", "deprecated": true, diff --git a/backend/app/services/qemu_manager.py b/backend/app/services/qemu_manager.py index 68c776f0..36df300c 100644 --- a/backend/app/services/qemu_manager.py +++ b/backend/app/services/qemu_manager.py @@ -66,6 +66,7 @@ PI_CONFIGS: dict[str, dict] = { 'kernel': 'velxio-kernel-arm64', 'initramfs': 'velxio-initramfs-arm64.cpio.gz', 'rootfs': 'velxio-pi-rootfs-arm64.ext4', + 'bus': 'pci', }, 'raspberry-pi-4': { 'qemu': 'qemu-system-aarch64', @@ -76,6 +77,7 @@ PI_CONFIGS: dict[str, dict] = { 'kernel': 'velxio-kernel-arm64', 'initramfs': 'velxio-initramfs-arm64.cpio.gz', 'rootfs': 'velxio-pi-rootfs-arm64.ext4', + 'bus': 'pci', }, 'raspberry-pi-5': { 'qemu': 'qemu-system-aarch64', @@ -86,8 +88,60 @@ PI_CONFIGS: dict[str, dict] = { 'kernel': 'velxio-kernel-arm64', 'initramfs': 'velxio-initramfs-arm64.cpio.gz', 'rootfs': 'velxio-pi-rootfs-arm64.ext4', + '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', }, - # Pi Zero/1/2 require armhf — added in Phase 3.3. } # Default board if the client doesn't specify one. Kept for clients @@ -315,6 +369,20 @@ class QemuManager: # # Why no -dtb: virt machine generates its own DTB on the fly # from the runtime device list, so we don't ship one. + # 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 = [ cfg['qemu'], '-M', 'virt', @@ -323,12 +391,12 @@ class QemuManager: '-m', cfg['memory'], '-kernel', str(kernel_path), '-initrd', str(initramfs_path), - # Root filesystem via virtio-blk over PCI. virt machine - # uses PCI as the primary virtio transport, so we use - # `virtio-blk-pci` (not `virtio-blk-device`, which is for - # mmio and silently leaves /dev/vda unregistered). + # 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. '-drive', f'if=none,file={inst.overlay_path},format=qcow2,id=rootfs', - '-device', 'virtio-blk-pci,drive=rootfs', + '-device', blk_dev, # 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 @@ -342,7 +410,7 @@ class QemuManager: # port (replaces the old ttyAMA0 path). '-chardev', f'socket,id=cons,host=127.0.0.1,port={inst.serial_port},' f'server=on,wait=off', - '-device', 'virtio-serial-pci,id=virtio-serial0', + '-device', serial_dev, '-device', 'virtconsole,chardev=cons', # Protocol channel: pipe (FIFO pair) instead of a socket # chardev. virtserialport on socket chardev has a known diff --git a/frontend/src/__tests__/board-kinds-coverage.test.ts b/frontend/src/__tests__/board-kinds-coverage.test.ts index 69b02fb7..183d9078 100644 --- a/frontend/src/__tests__/board-kinds-coverage.test.ts +++ b/frontend/src/__tests__/board-kinds-coverage.test.ts @@ -41,8 +41,11 @@ const ALL_EXAMPLES: ExampleProject[] = [ * coverage gap visible at code-review time. */ const ACCEPTED_UNCOVERED: ReadonlySet = new Set([ - // Pi 3/4/5 run on the backend (QEMU ARM64) — no in-browser canvas - // example because they boot a full Linux image. + // Pi Zero / 1 / 2 / 3 / 4 / 5 run on the backend (QEMU ARM/ARM64) — no + // in-browser canvas example because they boot a full Linux image. + 'raspberry-pi-zero', + 'raspberry-pi-1', + 'raspberry-pi-2', 'raspberry-pi-3', 'raspberry-pi-4', 'raspberry-pi-5', diff --git a/frontend/src/services/ComponentRegistry.ts b/frontend/src/services/ComponentRegistry.ts index 698b9f29..a836b365 100644 --- a/frontend/src/services/ComponentRegistry.ts +++ b/frontend/src/services/ComponentRegistry.ts @@ -67,6 +67,45 @@ export class ComponentRegistry { // Inject Raspberry Pi 3 / 4 / 5 metadata. All three share the // same 40-pin GPIO header; the simulator backend picks a // different QEMU CPU model per board (Cortex-A53/A72/A76). + data.components.push({ + id: 'raspberry-pi-zero', + tagName: 'velxio-raspberry-pi-3', // reuse 40-pin board art + name: 'Raspberry Pi Zero', + category: 'boards', + description: 'Raspberry Pi Zero with 40-pin GPIO. QEMU virt + Cortex-A7 (armhf) backend; presents the Pi Zero memory/SMP profile (1 core, 512 MB).', + thumbnail: + 'RPi0', + properties: [], + defaultValues: {}, + pinCount: 40, + tags: ['raspberry', 'pi', 'pi-zero', 'board', 'qemu', 'linux'], + }); + data.components.push({ + id: 'raspberry-pi-1', + tagName: 'velxio-raspberry-pi-3', // reuse 40-pin board art + name: 'Raspberry Pi 1', + category: 'boards', + description: 'Raspberry Pi 1 Model B+ with 40-pin GPIO. QEMU virt + Cortex-A7 (armhf) backend; 1 core / 512 MB profile.', + thumbnail: + 'RPi1', + properties: [], + defaultValues: {}, + pinCount: 40, + tags: ['raspberry', 'pi', 'rp1', 'board', 'qemu', 'linux'], + }); + data.components.push({ + id: 'raspberry-pi-2', + tagName: 'velxio-raspberry-pi-3', + name: 'Raspberry Pi 2', + category: 'boards', + description: 'Raspberry Pi 2 Model B with 40-pin GPIO. QEMU virt + Cortex-A7 (armhf) backend; 4 cores / 1 GB.', + thumbnail: + 'RPi2', + properties: [], + defaultValues: {}, + pinCount: 40, + tags: ['raspberry', 'pi', 'rp2', 'board', 'qemu', 'linux'], + }); data.components.push({ id: 'raspberry-pi-3', tagName: 'velxio-raspberry-pi-3', diff --git a/frontend/src/simulation/Interconnect.ts b/frontend/src/simulation/Interconnect.ts index 8f54eb91..667aa765 100644 --- a/frontend/src/simulation/Interconnect.ts +++ b/frontend/src/simulation/Interconnect.ts @@ -127,12 +127,9 @@ function isEsp32Bridge(boardKind: string): boolean { } function isPi3Bridge(boardKind: string): boolean { - // Pi 3 / 4 / 5 all use the same backend bridge (QEMU virt + virtio-serial). - return ( - boardKind === 'raspberry-pi-3' || - boardKind === 'raspberry-pi-4' || - boardKind === 'raspberry-pi-5' - ); + // Pi Zero / 1 / 2 / 3 / 4 / 5 all use the same backend bridge + // (QEMU virt + virtio-serial). Exclude raspberry-pi-pico (RP2040). + return boardKind.startsWith('raspberry-pi-') && boardKind !== 'raspberry-pi-pico'; } /** Resolve `(componentId, pinName)` to a `(boardId, pinNumber)` pair. */ diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index ddf65b28..e2939206 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -18,7 +18,7 @@ import type { I2CDevice } from '../simulation/I2CBusManager'; import type { RP2040I2CDevice } from '../simulation/RP2040Simulator'; import type { Wire, WireInProgress, WireEndpoint } from '../types/wire'; import type { BoardKind, BoardInstance, LanguageMode } from '../types/board'; -import { BOARD_SUPPORTS_MICROPYTHON } from '../types/board'; +import { BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind } from '../types/board'; import { calculatePinPosition } from '../utils/pinPositionCalculator'; import { useOscilloscopeStore } from './useOscilloscopeStore'; import { RaspberryPi3Bridge } from '../simulation/RaspberryPi3Bridge'; @@ -954,11 +954,7 @@ export const useSimulatorStore = create((set, get) => { const serialCallback = (ch: string) => appendSerial(id, ch); - if ( - boardKind === 'raspberry-pi-3' || - boardKind === 'raspberry-pi-4' || - boardKind === 'raspberry-pi-5' - ) { + if (isPiBoardKind(boardKind)) { const bridge = new RaspberryPi3Bridge(id, boardKind); bridge.onSerialData = (ch: string) => { serialCallback(ch); @@ -1084,7 +1080,7 @@ export const useSimulatorStore = create((set, get) => { // Create the editor file group for this board useEditorStore.getState().createFileGroup(`group-${id}`); // Init VFS for Raspberry Pi 3 boards - if (boardKind === 'raspberry-pi-3') { + if (isPiBoardKind(boardKind)) { useVfsStore.getState().initBoardVfs(id); } // ── Interconnect: register the board and rebuild routes ────────── @@ -1200,7 +1196,7 @@ export const useSimulatorStore = create((set, get) => { set({ activeBoardId: boardId, // Sync legacy flat fields to this board's values - boardType: (board.boardKind === 'raspberry-pi-3' + boardType: (isPiBoardKind(board.boardKind) ? 'arduino-uno' : board.boardKind) as BoardType, boardPosition: { x: board.x, y: board.y }, @@ -1247,7 +1243,7 @@ export const useSimulatorStore = create((set, get) => { } } else { const sim = getBoardSimulator(boardId); - if (sim && board.boardKind !== 'raspberry-pi-3') { + if (sim && !isPiBoardKind(board.boardKind)) { try { if (sim instanceof AVRSimulator) { sim.loadHex(program); @@ -1369,7 +1365,7 @@ export const useSimulatorStore = create((set, get) => { const board = get().boards.find((b) => b.id === boardId); if (!board) return; - if (board.boardKind === 'raspberry-pi-3') { + if (isPiBoardKind(board.boardKind)) { getBoardBridge(boardId)?.connect(); } else if (isEsp32Kind(board.boardKind)) { // Pre-register sensors connected to this board so the QEMU worker @@ -1532,7 +1528,7 @@ export const useSimulatorStore = create((set, get) => { const board = get().boards.find((b) => b.id === boardId); if (!board) return; - if (board.boardKind === 'raspberry-pi-3') { + if (isPiBoardKind(board.boardKind)) { getBoardBridge(boardId)?.disconnect(); } else if (isEsp32Kind(board.boardKind)) { getEsp32Bridge(boardId)?.disconnect(); @@ -1558,7 +1554,7 @@ export const useSimulatorStore = create((set, get) => { esp32Bridge.disconnect(); setTimeout(() => esp32Bridge.connect(), 500); } - } else if (board.boardKind !== 'raspberry-pi-3') { + } else if (!isPiBoardKind(board.boardKind)) { const sim = getBoardSimulator(boardId); if (sim) { sim.reset(); @@ -2395,7 +2391,7 @@ export const useSimulatorStore = create((set, get) => { const board = get().boards.find((b) => b.id === boardId); if (!board) return; - if (board.boardKind === 'raspberry-pi-3') { + if (isPiBoardKind(board.boardKind)) { const bridge = getBoardBridge(boardId); if (bridge) { for (let i = 0; i < text.length; i++) { @@ -2424,7 +2420,7 @@ export const useSimulatorStore = create((set, get) => { serialWriteToBoard: (boardId: string, text: string) => { const board = get().boards.find((b) => b.id === boardId); if (!board) return; - if (board.boardKind === 'raspberry-pi-3') { + if (isPiBoardKind(board.boardKind)) { const bridge = getBoardBridge(boardId); if (bridge) { for (let i = 0; i < text.length; i++) { diff --git a/frontend/src/types/board.ts b/frontend/src/types/board.ts index e399cf9d..574f0433 100644 --- a/frontend/src/types/board.ts +++ b/frontend/src/types/board.ts @@ -4,6 +4,9 @@ export type BoardKind = | 'arduino-mega' | 'raspberry-pi-pico' // RP2040, browser emulation | 'pi-pico-w' // RP2040 + WiFi, browser emulation (WiFi ignored) + | 'raspberry-pi-zero' // QEMU virt + Cortex-A7 (armhf), backend — looks-like Pi Zero + | 'raspberry-pi-1' // QEMU virt + Cortex-A7 (armhf), backend — looks-like Pi 1 + | 'raspberry-pi-2' // QEMU virt + Cortex-A7 (armhf), backend | 'raspberry-pi-3' // QEMU virt + Cortex-A53, backend | 'raspberry-pi-4' // QEMU virt + Cortex-A72, backend | 'raspberry-pi-5' // QEMU virt + Cortex-A76, backend @@ -21,6 +24,13 @@ export type BoardKind = export type LanguageMode = 'arduino' | 'micropython'; +/** True for every Raspberry Pi backed by the QEMU bridge (Zero, 1, 2, 3, 4, 5). + * Excludes the Pico boards (RP2040, browser emulation). */ +export function isPiBoardKind(kind: BoardKind | string): boolean { + return typeof kind === 'string' && kind.startsWith('raspberry-pi-') + && kind !== 'raspberry-pi-pico'; +} + export const BOARD_SUPPORTS_MICROPYTHON = new Set([ 'raspberry-pi-pico', 'pi-pico-w', @@ -72,6 +82,9 @@ export const BOARD_KIND_LABELS: Record = { 'arduino-mega': 'Arduino Mega 2560', 'raspberry-pi-pico': 'Raspberry Pi Pico', 'pi-pico-w': 'Raspberry Pi Pico W', + 'raspberry-pi-zero': 'Raspberry Pi Zero', + 'raspberry-pi-1': 'Raspberry Pi 1B+', + 'raspberry-pi-2': 'Raspberry Pi 2B', 'raspberry-pi-3': 'Raspberry Pi 3B', 'raspberry-pi-4': 'Raspberry Pi 4B', 'raspberry-pi-5': 'Raspberry Pi 5', @@ -94,6 +107,9 @@ export const BOARD_KIND_FQBN: Record = { 'arduino-mega': 'arduino:avr:mega', 'raspberry-pi-pico': 'rp2040:rp2040:rpipico', 'pi-pico-w': 'rp2040:rp2040:rpipicow', + 'raspberry-pi-zero': null, + 'raspberry-pi-1': null, + 'raspberry-pi-2': null, 'raspberry-pi-3': null, 'raspberry-pi-4': null, 'raspberry-pi-5': null, diff --git a/frontend/src/utils/boardProtocols.ts b/frontend/src/utils/boardProtocols.ts index 39ee3956..993d4dad 100644 --- a/frontend/src/utils/boardProtocols.ts +++ b/frontend/src/utils/boardProtocols.ts @@ -144,15 +144,8 @@ function tableFor(boardKind: BoardKind | string): RoleTable | null { if (boardKind === 'raspberry-pi-pico' || boardKind === 'pi-pico-w') return RP2040_DEFAULT; if (boardKind === 'esp32-c3' || (boardKind as string).startsWith('esp32-c3')) return ESP32_C3_DEFAULT; if (boardKind === 'esp32' || (boardKind as string).startsWith('esp32')) return ESP32_DEFAULT; - // Pi 3/4/5 all share the same 40-pin GPIO header → same BCM table. - if ( - boardKind === 'raspberry-pi-3' || - boardKind === 'raspberry-pi-4' || - boardKind === 'raspberry-pi-5' || - (boardKind as string).startsWith('raspberry-pi-3') || - (boardKind as string).startsWith('raspberry-pi-4') || - (boardKind as string).startsWith('raspberry-pi-5') - ) + // Pi Zero/1/2/3/4/5 all share the same 40-pin GPIO header → same BCM table. + if ((boardKind as string).startsWith('raspberry-pi-')) return PI3_BCM; return ARDUINO_NANO; // default fallback: treat unknown as arduino-uno-like } diff --git a/frontend/src/utils/loadExample.ts b/frontend/src/utils/loadExample.ts index 2417f83e..41dc649e 100644 --- a/frontend/src/utils/loadExample.ts +++ b/frontend/src/utils/loadExample.ts @@ -5,6 +5,7 @@ import type { ExampleProject } from '../data/examples'; import type { BoardKind } from '../types/board'; +import { isPiBoardKind } from '../types/board'; import { useEditorStore } from '../store/useEditorStore'; import { useSimulatorStore, DEFAULT_BOARD_POSITION } from '../store/useSimulatorStore'; import { useElectricalStore } from '../store/useElectricalStore'; @@ -129,12 +130,12 @@ export async function loadExample( // Arduino-style boards (AVR, RP2040, ESP32, …) all need the `.ino` // extension so arduino-cli auto-includes . Only the Pi 3B // uses a different toolchain (Python via VFS or g++ for `.cpp`). - const filename = (eb.boardKind === 'raspberry-pi-3' || eb.boardKind === 'raspberry-pi-4' || eb.boardKind === 'raspberry-pi-5') ? 'main.cpp' : 'sketch.ino'; + const filename = isPiBoardKind(eb.boardKind) ? 'main.cpp' : 'sketch.ino'; useEditorStore.getState().setActiveGroup(board.activeFileGroupId); useEditorStore.getState().loadFiles([{ name: filename, content: eb.code }]); } - if (eb.vfsFiles && (eb.boardKind === 'raspberry-pi-3' || eb.boardKind === 'raspberry-pi-4' || eb.boardKind === 'raspberry-pi-5')) { + if (eb.vfsFiles && isPiBoardKind(eb.boardKind)) { const vfsState = useVfsStore.getState(); const tree = vfsState.getTree(boardId); for (const [nodeId, node] of Object.entries(tree)) { @@ -147,7 +148,7 @@ export async function loadExample( const firstArduinoIdx = example.boards.findIndex( (eb) => - eb.boardKind !== 'raspberry-pi-3' && eb.boardKind !== 'raspberry-pi-4' && eb.boardKind !== 'raspberry-pi-5' && + !isPiBoardKind(eb.boardKind) && eb.boardKind !== 'esp32' && eb.boardKind !== 'esp32-s3' && eb.boardKind !== 'esp32-c3', @@ -242,7 +243,7 @@ export async function loadExample( // appear blank. (Regression test: load-example-transitions.test.ts.) const editorStore = useEditorStore.getState(); editorStore.setActiveGroup(liveBoard.activeFileGroupId); - const filename = (liveBoard.boardKind === 'raspberry-pi-3' || liveBoard.boardKind === 'raspberry-pi-4' || liveBoard.boardKind === 'raspberry-pi-5') ? 'main.cpp' : 'sketch.ino'; + const filename = isPiBoardKind(liveBoard.boardKind) ? 'main.cpp' : 'sketch.ino'; editorStore.loadFiles([{ name: filename, content: example.code }]); } else { // Truly board-less: write the placeholder code to whatever the editor