feat(boards): expose Raspberry Pi 4 and Pi 5 in the picker (UI + pin wiring)

The BoardKind type and the QEMU backend already supported
raspberry-pi-4 (Cortex-A72) and raspberry-pi-5 (Cortex-A76) by reusing
the Pi 3 arm64 image set, but the frontend had no way to actually
select either: the board picker, the canvas renderer, the serial
monitor, the oscilloscope channel list, and the editor toolbar all
hard-coded "raspberry-pi-3" as the only Pi entry.  ComponentRegistry
even registered Pi 4 / Pi 5 metadata pointing at the velxio-raspberry-pi-3
custom-element tag — a placeholder that meant both boards rendered as
a Pi 3 in the picker thumbnail and on the canvas.

Add dedicated boards top-to-bottom:

  * `RaspberryPi4Element.ts` / `RaspberryPi5Element.ts` — Velxio-style
    schematic SVG (authored from scratch, not traced).  Pi 4 is the
    green PCB with BCM2711 SoC, 4× USB-A, USB-C power, dual µHDMI;
    Pi 5 is the darker green PCB with BCM2712 + RP1 southbridge,
    2.5 GbE, USB-C 5V/5A, PCIe FFC connector, dedicated power
    button.  Both carry a small "velxio" mark in the corner.

  * `pi40PinHeader.ts` — shared `buildPi40PinHeader()` helper that
    returns the 40-pin BCM layout.  Every Pi from the 1B+ onwards
    uses the same physical pin positions and same BCM GPIO
    assignment, so Pi 3 / Pi 4 / Pi 5 elements all consume this
    helper and example wires drawn against one model transfer to
    the others without re-routing.

  * React wrappers `RaspberryPi4.tsx` / `RaspberryPi5.tsx` render the
    custom elements at absolute positions (mirrors how
    RaspberryPi3.tsx handles the Pi 3 illustration).

  * Wire-up across the editor surface:
      - BoardOnCanvas: BOARD_SIZE entry + switch case.
      - BoardPickerModal: description, icon, kinds list.
      - ComponentPickerModal: thumbnails now instantiate the dedicated
        custom element (was velxio-raspberry-pi-3 fallback).
      - SerialMonitor / EditorToolbar: pill labels, icons, colours.
      - Oscilloscope: GPIO channel list (28 BCM pins).
      - SimulatorCanvas: remote-boards filter for run/stop sync.
      - SPICE boardPinGroups: same 5V / 3V3 / GND as Pi 3.
      - boardPinToNumber: accepts physical pin numbers ("1"-"40"),
        BCM names ("GPIO14") and power labels for any Pi 3/4/5 id.
      - ComponentRegistry: dedicated tagNames + per-board thumbnails
        (green for Pi 4, darker green for Pi 5).

  * EditorToolbar's Pi 3 special cases (Linux/Python compile path,
    Run/Stop routing) now use `isPiBoardKind()` so Pi 4 and Pi 5
    inherit the same behaviour automatically, and any future Pi
    family member (Zero / 1 / 2) lands in the right code paths the
    moment its backend boots.

QEMU backend was already wired (qemu_manager.py:71/82 + manifest entry
'raspberry-pi-3-virt' shared across arm64 Pis), so this commit makes
both boards selectable end-to-end without any backend follow-up.
This commit is contained in:
David Montero 2026-05-23 04:46:59 +02:00
parent 2146b09c29
commit f619a2cc7e
15 changed files with 466 additions and 15 deletions

View File

@ -41,6 +41,8 @@ const BOARD_DESCRIPTIONS: Record<BoardKind, string> = {
'raspberry-pi-pico': 'RP2040 dual-core Cortex-M0+',
'pi-pico-w': 'RP2040 + WiFi/BT, same emulator as Pico',
'raspberry-pi-3': 'ARM64 Cortex-A53 quad-core, Linux/Python (QEMU)',
'raspberry-pi-4': 'ARM64 Cortex-A72 quad-core, Linux/Python (QEMU)',
'raspberry-pi-5': 'ARM64 Cortex-A76 quad-core + RP1 I/O, Linux/Python (QEMU)',
esp32: 'Xtensa LX6 dual-core, WiFi+BT, 38 GPIO (QEMU)',
'esp32-devkit-c-v4': 'ESP32 DevKit C V4, official Espressif (QEMU)',
'esp32-cam': 'ESP32 + 2MP camera, microSD (QEMU)',
@ -61,6 +63,8 @@ const ALL_BOARDS: BoardKind[] = [
'raspberry-pi-pico',
'pi-pico-w',
'raspberry-pi-3',
'raspberry-pi-4',
'raspberry-pi-5',
'esp32',
'esp32-devkit-c-v4',
'esp32-cam',
@ -424,8 +428,21 @@ const BoardCard: React.FC<BoardCardProps> = ({ kind, onSelect }) => {
React.useEffect(() => {
if (!thumbnailRef.current) return;
// React-rendered boards and Pi3 handled in JSX below
// React-rendered boards and Pi family handled below: Pi 3 has a custom
// illustration SVG; Pi 4 / Pi 5 instantiate their own velxio-* custom
// element directly because they don't go through BOARD_TAG.
if (kind === 'raspberry-pi-3' || kind === 'attiny85') return;
if (kind === 'raspberry-pi-4' || kind === 'raspberry-pi-5') {
const tagName = kind === 'raspberry-pi-4' ? 'velxio-raspberry-pi-4' : 'velxio-raspberry-pi-5';
const el = document.createElement(tagName) as HTMLElement;
el.style.transform = 'scale(0.35)';
el.style.transformOrigin = 'center center';
thumbnailRef.current.innerHTML = '';
thumbnailRef.current.appendChild(el);
return () => {
if (thumbnailRef.current) thumbnailRef.current.innerHTML = '';
};
}
const tag = BOARD_TAG[kind];
if (!tag) return;

View File

@ -9,7 +9,7 @@ import { BOARD_PIN_GROUPS } from '../../simulation/spice/boardPinGroups';
import { CircuitVerificationModal } from '../simulator/CircuitVerificationModal';
import type { PinSourceState } from '../../simulation/spice/types';
import type { BoardKind, LanguageMode } from '../../types/board';
import { BOARD_KIND_FQBN, BOARD_KIND_LABELS, BOARD_SUPPORTS_MICROPYTHON } from '../../types/board';
import { BOARD_KIND_FQBN, BOARD_KIND_LABELS, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind } from '../../types/board';
import { compileCode } from '../../services/compilation';
import {
compileRom,
@ -61,6 +61,8 @@ const BOARD_PILL_ICON: Record<BoardKind, string> = {
'arduino-mega': '▬',
'raspberry-pi-pico': '◆',
'raspberry-pi-3': '⬛',
'raspberry-pi-4': '⬛',
'raspberry-pi-5': '⬛',
esp32: '⬡',
'esp32-s3': '⬡',
'esp32-c3': '⬡',
@ -72,6 +74,8 @@ const BOARD_PILL_COLOR: Record<BoardKind, string> = {
'arduino-mega': '#4fc3f7',
'raspberry-pi-pico': '#ce93d8',
'raspberry-pi-3': '#ef9a9a',
'raspberry-pi-4': '#ef9a9a',
'raspberry-pi-5': '#ef9a9a',
esp32: '#a5d6a7',
'esp32-s3': '#a5d6a7',
'esp32-c3': '#a5d6a7',
@ -259,7 +263,7 @@ export const EditorToolbar = ({
const kind = activeBoard?.boardKind;
// Raspberry Pi 3B doesn't need arduino-cli compilation
if (kind === 'raspberry-pi-3') {
if (isPiBoardKind(kind)) {
addLog({
timestamp: new Date(),
type: 'info',
@ -566,7 +570,7 @@ export const EditorToolbar = ({
}
const isQemuBoard =
board?.boardKind === 'raspberry-pi-3' ||
board?.boardKind && isPiBoardKind(board.boardKind) ||
board?.boardKind === 'esp32' ||
board?.boardKind === 'esp32-s3' ||
board?.boardKind === 'esp32-cam' ||
@ -713,7 +717,7 @@ export const EditorToolbar = ({
for (const board of boardsList) {
const label = BOARD_KIND_LABELS[board.boardKind] ?? board.boardKind;
if (board.boardKind === 'raspberry-pi-3') {
if (isPiBoardKind(board.boardKind)) {
addLog({
timestamp: new Date(),
type: 'info',
@ -815,7 +819,7 @@ export const EditorToolbar = ({
codeChangedSinceLastCompile ||
boardsList.some(
(b) =>
b.boardKind !== 'raspberry-pi-3' &&
!isPiBoardKind(b.boardKind) &&
b.languageMode !== 'micropython' &&
!b.compiledProgram,
);
@ -830,7 +834,7 @@ export const EditorToolbar = ({
for (const board of refreshed) {
if (board.running) continue;
const isQemu =
board.boardKind === 'raspberry-pi-3' ||
isPiBoardKind(board.boardKind) ||
board.boardKind === 'esp32' ||
board.boardKind === 'esp32-s3';
if (isQemu || board.compiledProgram || board.languageMode === 'micropython') {

View File

@ -8,6 +8,8 @@ import { ArduinoMega } from '../velxio-components/ArduinoMega';
// re-wire it back in. If someone genuinely needs a Nano RP2040 Connect
// board (D2..D13 labels), add a new boardKind 'arduino-nano-rp2040'.
import { RaspberryPi3 } from '../velxio-components/RaspberryPi3';
import { RaspberryPi4 } from '../velxio-components/RaspberryPi4';
import { RaspberryPi5 } from '../velxio-components/RaspberryPi5';
import { Esp32 } from '../velxio-components/Esp32';
import { Attiny85 } from '../velxio-components/Attiny85';
import { PiPicoW } from '../velxio-components/PiPicoW';
@ -29,6 +31,8 @@ const BOARD_SIZE: Record<string, { w: number; h: number }> = {
// the boardKind name.
'raspberry-pi-pico': { w: 105, h: 264 },
'raspberry-pi-3': { w: 250, h: 160 }, // RaspberryPi3Element: PI_WIDTH=250 PI_HEIGHT=160
'raspberry-pi-4': { w: 250, h: 160 }, // RaspberryPi4Element — same 40-pin footprint
'raspberry-pi-5': { w: 250, h: 160 }, // RaspberryPi5Element — same 40-pin footprint
esp32: { w: 141, h: 265 }, // esp32-devkit-v1: 28.2 × 53 mm
'esp32-s3': { w: 128, h: 350 }, // esp32-s3-devkitc-1: 25.5 × 70 mm
'esp32-c3': { w: 127, h: 215 }, // esp32-c3-devkitm-1: 25.4 × 42.9 mm
@ -97,6 +101,10 @@ export const BoardOnCanvas = ({
return <PiPicoW id={id} x={x} y={y} />;
case 'raspberry-pi-3':
return <RaspberryPi3 id={id} x={x} y={y} />;
case 'raspberry-pi-4':
return <RaspberryPi4 id={id} x={x} y={y} />;
case 'raspberry-pi-5':
return <RaspberryPi5 id={id} x={x} y={y} />;
case 'esp32':
case 'esp32-devkit-c-v4':
case 'esp32-cam':

View File

@ -9,6 +9,8 @@ const BOARD_DESCRIPTIONS: Record<BoardKind, string> = {
'arduino-mega': '8-bit AVR, 256KB flash, 54 digital I/O',
'raspberry-pi-pico': 'RP2040 dual-core Cortex-M0+',
'raspberry-pi-3': 'ARM64 Cortex-A53 quad-core, Linux/Python (QEMU)',
'raspberry-pi-4': 'ARM64 Cortex-A72 quad-core, Linux/Python (QEMU)',
'raspberry-pi-5': 'ARM64 Cortex-A76 quad-core + RP1 I/O, Linux/Python (QEMU)',
esp32: 'Xtensa LX6 dual-core, WiFi+BT, 38 GPIO (QEMU)',
'esp32-s3': 'Xtensa LX7 dual-core, WiFi+BT, AI accel (QEMU)',
'esp32-c3': 'RISC-V single-core, WiFi+BLE, 22 GPIO (QEMU)',
@ -20,6 +22,8 @@ const BOARD_ICON: Record<BoardKind, string> = {
'arduino-mega': '▬',
'raspberry-pi-pico': '◆',
'raspberry-pi-3': '⬛',
'raspberry-pi-4': '⬛',
'raspberry-pi-5': '⬛',
esp32: '⬡',
'esp32-s3': '⬡',
'esp32-c3': '⬡',
@ -37,6 +41,8 @@ const BOARDS: BoardKind[] = [
'arduino-mega',
'raspberry-pi-pico',
'raspberry-pi-3',
'raspberry-pi-4',
'raspberry-pi-5',
'esp32',
'esp32-s3',
'esp32-c3',

View File

@ -68,6 +68,8 @@ function getPinsForBoardKind(boardKind: BoardKind): { pin: number; label: string
case 'aitewinrobot-esp32c3-supermini':
return Array.from({ length: 22 }, (_, i) => ({ pin: i, label: `GPIO${i}` }));
case 'raspberry-pi-3':
case 'raspberry-pi-4':
case 'raspberry-pi-5':
return Array.from({ length: 28 }, (_, i) => ({ pin: i, label: `GPIO${i}` }));
default:
// arduino-uno, arduino-nano

View File

@ -18,6 +18,8 @@ const BOARD_SHORT_LABEL: Partial<Record<string, string>> = {
'raspberry-pi-pico': 'Pico',
'pi-pico-w': 'Pico W',
'raspberry-pi-3': 'Pi 3B',
'raspberry-pi-4': 'Pi 4B',
'raspberry-pi-5': 'Pi 5',
esp32: 'ESP32',
'esp32-devkit-c-v4': 'ESP32',
'esp32-cam': 'ESP32-CAM',
@ -38,6 +40,8 @@ const BOARD_ICON: Partial<Record<string, string>> = {
'raspberry-pi-pico': '◆',
'pi-pico-w': '◆',
'raspberry-pi-3': '⬛',
'raspberry-pi-4': '⬛',
'raspberry-pi-5': '⬛',
esp32: '⬡',
'esp32-devkit-c-v4': '⬡',
'esp32-cam': '⬡',
@ -58,6 +62,8 @@ const BOARD_COLOR: Partial<Record<string, string>> = {
'raspberry-pi-pico': '#ce93d8',
'pi-pico-w': '#ce93d8',
'raspberry-pi-3': '#ef9a9a',
'raspberry-pi-4': '#ef9a9a',
'raspberry-pi-5': '#ef9a9a',
esp32: '#a5d6a7',
'esp32-devkit-c-v4': '#a5d6a7',
'esp32-cam': '#a5d6a7',

View File

@ -413,6 +413,8 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
const remoteBoards = boards.filter(
(b) =>
b.boardKind === 'raspberry-pi-3' ||
b.boardKind === 'raspberry-pi-4' ||
b.boardKind === 'raspberry-pi-5' ||
b.boardKind === 'esp32' ||
b.boardKind === 'esp32-s3' ||
b.boardKind === 'esp32-c3',

View File

@ -0,0 +1,36 @@
/**
* Raspberry Pi 4 React wrapper renders the velxio-raspberry-pi-4 custom
* element (defined in RaspberryPi4Element.ts) at an absolute position so
* BoardOnCanvas can drop it on the simulator canvas. The wire system
* reads `pinInfo` directly from the custom element via DOM, so no
* additional pinInfo wiring is needed here.
*/
import './RaspberryPi4Element';
declare global {
namespace JSX {
interface IntrinsicElements {
'velxio-raspberry-pi-4': any; // eslint-disable-line @typescript-eslint/no-explicit-any
}
}
}
interface RaspberryPi4Props {
id?: string;
x?: number;
y?: number;
}
export const RaspberryPi4 = ({ id = 'raspberry-pi-4', x = 0, y = 0 }: RaspberryPi4Props) => (
<velxio-raspberry-pi-4
id={id}
style={{
position: 'absolute',
left: `${x}px`,
top: `${y}px`,
display: 'block',
userSelect: 'none',
pointerEvents: 'none',
}}
/>
);

View File

@ -0,0 +1,120 @@
/**
* Raspberry Pi 4 Model B Velxio schematic board.
*
* Authored from scratch (not traced from RPi Foundation art); the layout
* is the publicly-known PCB topology (BCM2711 SoC centre-right, USB-C
* power on the south edge, dual micro-HDMI mid-south, 4× USB-A and
* gigabit Ethernet stacked on the east edge, 40-pin GPIO header on the
* north edge, microSD on the underside). Rendered at the same
* 250×160 footprint and pin pitch as RaspberryPi3Element so wires from
* existing canvases line up byte-for-byte.
*
* The 40-pin GPIO header is electrically identical to every Pi from
* the 1B+ onwards pin numbers 1-40 map to the same BCM GPIO lines
* the firmware sees, so reusing the same pin-name scheme means Velxio
* routes and example projects transfer between Pi models without
* touching wires.
*/
import { buildPi40PinHeader } from './pi40PinHeader';
const PI_WIDTH = 250;
const PI_HEIGHT = 160;
class RaspberryPi4Element extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
get pinInfo() {
return buildPi40PinHeader();
}
render() {
let pinsSvg = '';
const pins = this.pinInfo;
pins.forEach((pin) => {
pinsSvg += `<rect x="${pin.x - 3}" y="${pin.y - 3}" width="6" height="6" fill="#D4AF37" />`;
pinsSvg += `<circle cx="${pin.x}" cy="${pin.y}" r="2" fill="#000" />`;
});
this.shadowRoot!.innerHTML = `
<style>
:host {
display: block;
width: ${PI_WIDTH}px;
height: ${PI_HEIGHT}px;
position: relative;
}
svg { width: 100%; height: 100%; }
.board {
fill: #006633; /* Pi 4 green PCB */
stroke: #003d1f;
stroke-width: 2;
rx: 8;
}
.cpu { fill: #1a1a1a; stroke: #000; rx: 2; }
.usb { fill: #ccc; stroke: #999; rx: 2; }
.usbc { fill: #888; stroke: #555; rx: 1; }
.eth { fill: #bbb; stroke: #888; rx: 2; }
.hdmi { fill: #555; stroke: #333; rx: 1; }
.gpio-header { fill: #222; }
.rp1 { fill: #2a2a2a; stroke: #000; rx: 2; }
.label { fill: #FFF; font-family: sans-serif; }
</style>
<svg viewBox="0 0 ${PI_WIDTH} ${PI_HEIGHT}">
<!-- PCB -->
<rect class="board" x="2" y="2" width="${PI_WIDTH - 4}" height="${PI_HEIGHT - 4}" />
<!-- BCM2711 SoC (centre-right, larger than Pi 3) -->
<rect class="cpu" x="100" y="55" width="48" height="48" />
<text x="124" y="76" class="label" font-size="7" text-anchor="middle">BCM2711</text>
<text x="124" y="86" class="label" font-size="6" text-anchor="middle" opacity="0.7">A72 ×4</text>
<!-- 4× USB-A (east edge, two stacked banks) -->
<rect class="usb" x="${PI_WIDTH - 38}" y="18" width="36" height="28" />
<text x="${PI_WIDTH - 20}" y="34" class="label" font-size="5" text-anchor="middle" opacity="0.7">USB3</text>
<rect class="usb" x="${PI_WIDTH - 38}" y="50" width="36" height="28" />
<text x="${PI_WIDTH - 20}" y="66" class="label" font-size="5" text-anchor="middle" opacity="0.7">USB2</text>
<!-- Gigabit Ethernet -->
<rect class="eth" x="${PI_WIDTH - 38}" y="82" width="36" height="36" />
<text x="${PI_WIDTH - 20}" y="102" class="label" font-size="5" text-anchor="middle" opacity="0.7">GbE</text>
<!-- USB-C Power (south edge, west side) -->
<rect class="usbc" x="6" y="${PI_HEIGHT - 18}" width="18" height="10" />
<text x="15" y="${PI_HEIGHT - 22}" class="label" font-size="5" text-anchor="middle" opacity="0.7">USB-C 5V</text>
<!-- 2× micro-HDMI (south edge, centre) -->
<rect class="hdmi" x="40" y="${PI_HEIGHT - 16}" width="20" height="10" />
<rect class="hdmi" x="66" y="${PI_HEIGHT - 16}" width="20" height="10" />
<text x="63" y="${PI_HEIGHT - 20}" class="label" font-size="5" text-anchor="middle" opacity="0.7">µHDMI 0 / 1</text>
<!-- Audio jack (south edge, east of HDMI) -->
<circle cx="100" cy="${PI_HEIGHT - 11}" r="5" fill="#222" stroke="#000" />
<!-- GPIO Header base -->
<rect class="gpio-header" x="15" y="5" width="200" height="20" rx="1" />
<!-- Pins -->
${pinsSvg}
<!-- Board name + Velxio mark -->
<text x="60" y="125" class="label" font-size="14" font-weight="bold">Raspberry Pi 4</text>
<text x="60" y="140" class="label" font-size="9" opacity="0.85">Model B · Cortex-A72</text>
<text x="${PI_WIDTH - 8}" y="${PI_HEIGHT - 6}" class="label" font-size="6" text-anchor="end" opacity="0.6">velxio</text>
</svg>
`;
}
}
if (!customElements.get('velxio-raspberry-pi-4')) {
customElements.define('velxio-raspberry-pi-4', RaspberryPi4Element);
}
export {};

View File

@ -0,0 +1,34 @@
/**
* Raspberry Pi 5 React wrapper renders the velxio-raspberry-pi-5 custom
* element at an absolute position. Pin info lives on the custom element
* via its `pinInfo` getter; the wire system reads it from the DOM.
*/
import './RaspberryPi5Element';
declare global {
namespace JSX {
interface IntrinsicElements {
'velxio-raspberry-pi-5': any; // eslint-disable-line @typescript-eslint/no-explicit-any
}
}
}
interface RaspberryPi5Props {
id?: string;
x?: number;
y?: number;
}
export const RaspberryPi5 = ({ id = 'raspberry-pi-5', x = 0, y = 0 }: RaspberryPi5Props) => (
<velxio-raspberry-pi-5
id={id}
style={{
position: 'absolute',
left: `${x}px`,
top: `${y}px`,
display: 'block',
userSelect: 'none',
pointerEvents: 'none',
}}
/>
);

View File

@ -0,0 +1,129 @@
/**
* Raspberry Pi 5 Velxio schematic board.
*
* Authored from scratch (not traced from RPi Foundation art); the layout
* is the publicly-known PCB topology: BCM2712 SoC centre, RP1 southbridge
* to its right driving the I/O, 4× USB-A + 2.5 GbE on the east edge,
* USB-C power + dual micro-HDMI on the south, 40-pin GPIO header on the
* north, dedicated power button on the west, PCIe FFC connector on the
* underside, fan header to the north-east of the SoC.
*
* The 40-pin GPIO header is electrically identical to every Pi from
* the 1B+ onwards, so the pin coords mirror RaspberryPi3Element /
* RaspberryPi4Element byte-for-byte wires snap to the same physical
* positions when a user swaps boards.
*/
import { buildPi40PinHeader } from './pi40PinHeader';
const PI_WIDTH = 250;
const PI_HEIGHT = 160;
class RaspberryPi5Element extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
get pinInfo() {
return buildPi40PinHeader();
}
render() {
let pinsSvg = '';
const pins = this.pinInfo;
pins.forEach((pin) => {
pinsSvg += `<rect x="${pin.x - 3}" y="${pin.y - 3}" width="6" height="6" fill="#D4AF37" />`;
pinsSvg += `<circle cx="${pin.x}" cy="${pin.y}" r="2" fill="#000" />`;
});
this.shadowRoot!.innerHTML = `
<style>
:host {
display: block;
width: ${PI_WIDTH}px;
height: ${PI_HEIGHT}px;
position: relative;
}
svg { width: 100%; height: 100%; }
.board {
fill: #004d27; /* Pi 5 darker green PCB */
stroke: #002b15;
stroke-width: 2;
rx: 8;
}
.cpu { fill: #1a1a1a; stroke: #000; rx: 2; }
.rp1 { fill: #2a2a2a; stroke: #000; rx: 2; }
.usb { fill: #ccc; stroke: #999; rx: 2; }
.usbc { fill: #888; stroke: #555; rx: 1; }
.eth { fill: #bbb; stroke: #888; rx: 2; }
.hdmi { fill: #555; stroke: #333; rx: 1; }
.gpio-header { fill: #222; }
.pcie { fill: #4a4a4a; stroke: #222; rx: 1; }
.pwrbtn { fill: #c00; stroke: #800; rx: 1.5; }
.label { fill: #FFF; font-family: sans-serif; }
</style>
<svg viewBox="0 0 ${PI_WIDTH} ${PI_HEIGHT}">
<!-- PCB -->
<rect class="board" x="2" y="2" width="${PI_WIDTH - 4}" height="${PI_HEIGHT - 4}" />
<!-- BCM2712 SoC (centre) -->
<rect class="cpu" x="90" y="55" width="44" height="44" />
<text x="112" y="76" class="label" font-size="7" text-anchor="middle">BCM2712</text>
<text x="112" y="86" class="label" font-size="6" text-anchor="middle" opacity="0.7">A76 ×4</text>
<!-- RP1 southbridge I/O chip (Raspberry Pi's own silicon, east of SoC) -->
<rect class="rp1" x="146" y="65" width="26" height="26" />
<text x="159" y="80" class="label" font-size="6" text-anchor="middle">RP1</text>
<!-- 4× USB-A (east edge, two stacked banks of 2) -->
<rect class="usb" x="${PI_WIDTH - 38}" y="18" width="36" height="28" />
<text x="${PI_WIDTH - 20}" y="34" class="label" font-size="5" text-anchor="middle" opacity="0.7">USB3</text>
<rect class="usb" x="${PI_WIDTH - 38}" y="50" width="36" height="28" />
<text x="${PI_WIDTH - 20}" y="66" class="label" font-size="5" text-anchor="middle" opacity="0.7">USB3</text>
<!-- 2.5 Gigabit Ethernet -->
<rect class="eth" x="${PI_WIDTH - 38}" y="82" width="36" height="36" />
<text x="${PI_WIDTH - 20}" y="102" class="label" font-size="5" text-anchor="middle" opacity="0.7">2.5GbE</text>
<!-- USB-C power (south edge) -->
<rect class="usbc" x="6" y="${PI_HEIGHT - 18}" width="18" height="10" />
<text x="15" y="${PI_HEIGHT - 22}" class="label" font-size="5" text-anchor="middle" opacity="0.7">USB-C 5V/5A</text>
<!-- Power button (west edge) Pi 5 has a real on/off button -->
<rect class="pwrbtn" x="3" y="80" width="6" height="6" />
<text x="14" y="86" class="label" font-size="5" opacity="0.7">PWR</text>
<!-- 2× micro-HDMI (south, centre) -->
<rect class="hdmi" x="40" y="${PI_HEIGHT - 16}" width="20" height="10" />
<rect class="hdmi" x="66" y="${PI_HEIGHT - 16}" width="20" height="10" />
<text x="63" y="${PI_HEIGHT - 20}" class="label" font-size="5" text-anchor="middle" opacity="0.7">µHDMI 0 / 1</text>
<!-- PCIe FFC connector (south, east of HDMI) -->
<rect class="pcie" x="100" y="${PI_HEIGHT - 14}" width="36" height="6" />
<text x="118" y="${PI_HEIGHT - 18}" class="label" font-size="5" text-anchor="middle" opacity="0.7">PCIe FFC</text>
<!-- GPIO Header base -->
<rect class="gpio-header" x="15" y="5" width="200" height="20" rx="1" />
<!-- Pins -->
${pinsSvg}
<!-- Board name + Velxio mark -->
<text x="60" y="125" class="label" font-size="14" font-weight="bold">Raspberry Pi 5</text>
<text x="60" y="140" class="label" font-size="9" opacity="0.85">Cortex-A76 · BCM2712 · RP1</text>
<text x="${PI_WIDTH - 8}" y="${PI_HEIGHT - 6}" class="label" font-size="6" text-anchor="end" opacity="0.6">velxio</text>
</svg>
`;
}
}
if (!customElements.get('velxio-raspberry-pi-5')) {
customElements.define('velxio-raspberry-pi-5', RaspberryPi5Element);
}
export {};

View File

@ -0,0 +1,71 @@
/**
* Shared 40-pin GPIO header layout used by every Raspberry Pi from the
* 1B+ onwards (Zero W, 1B+, 2, 3, 4, 5). Same physical positions, same
* BCM GPIO assignment, same naming convention so wires drawn against
* a Pi 3 example transfer to a Pi 4 or Pi 5 board without re-routing.
*
* Coordinates are in CSS pixels relative to the board element's top-left,
* sized to fit the 250×160 footprint shared by every Velxio Pi element.
* Pin "1" sits top-left; odd pins on the top row, even on the bottom.
*
* Names follow the Pi 3 wrapper convention (BCM GPIO numbers like
* "GPIO14" plus power/ground labels) so example wires using those names
* resolve to the same physical position on every Pi model.
*/
export interface PinInfo {
name: string;
x: number;
y: number;
signals: string[];
}
const PIN_X_START = 20;
const PIN_X_STEP = 10;
const PIN_Y_TOP = 10;
const PIN_Y_BOT = 20;
const PI_PIN_NAMES: Record<number, string> = {
1: '3V3', 2: '5V',
3: 'GPIO2', 4: '5V',
5: 'GPIO3', 6: 'GND',
7: 'GPIO4', 8: 'GPIO14',
9: 'GND', 10: 'GPIO15',
11: 'GPIO17', 12: 'GPIO18',
13: 'GPIO27', 14: 'GND',
15: 'GPIO22', 16: 'GPIO23',
17: '3V3', 18: 'GPIO24',
19: 'GPIO10', 20: 'GND',
21: 'GPIO9', 22: 'GPIO25',
23: 'GPIO11', 24: 'GPIO8',
25: 'GND', 26: 'GPIO7',
27: 'ID_SD', 28: 'ID_SC',
29: 'GPIO5', 30: 'GND',
31: 'GPIO6', 32: 'GPIO12',
33: 'GPIO13', 34: 'GND',
35: 'GPIO19', 36: 'GPIO16',
37: 'GPIO26', 38: 'GPIO20',
39: 'GND', 40: 'GPIO21',
};
export function buildPi40PinHeader(): PinInfo[] {
const pins: PinInfo[] = [];
for (let col = 0; col < 20; col++) {
const oddPin = col * 2 + 1; // top row
const evenPin = col * 2 + 2; // bottom row
const px = PIN_X_START + col * PIN_X_STEP;
pins.push({
name: PI_PIN_NAMES[oddPin] ?? `P${oddPin}`,
x: px,
y: PIN_Y_TOP,
signals: [],
});
pins.push({
name: PI_PIN_NAMES[evenPin] ?? `P${evenPin}`,
x: px,
y: PIN_Y_BOT,
signals: [],
});
}
return pins;
}

View File

@ -121,12 +121,12 @@ export class ComponentRegistry {
});
data.components.push({
id: 'raspberry-pi-4',
tagName: 'velxio-raspberry-pi-3', // reuse Pi 3 board art (40-pin layout identical)
tagName: 'velxio-raspberry-pi-4',
name: 'Raspberry Pi 4',
category: 'boards',
description: 'Raspberry Pi 4 Model B with 40-pin GPIO. QEMU virt + Cortex-A72 backend.',
thumbnail:
'<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" fill="#83B81A" rx="4"/><text x="50%" y="50%" text-anchor="middle" dy=".3em" font-size="10" fill="#FFF">RPi4</text></svg>',
'<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" fill="#006633" rx="4"/><text x="50%" y="50%" text-anchor="middle" dy=".3em" font-size="10" fill="#FFF">RPi4</text></svg>',
properties: [],
defaultValues: {},
pinCount: 40,
@ -134,12 +134,12 @@ export class ComponentRegistry {
});
data.components.push({
id: 'raspberry-pi-5',
tagName: 'velxio-raspberry-pi-3', // reuse art for now (Phase 3 polish: Pi 5 PCB SVG)
tagName: 'velxio-raspberry-pi-5',
name: 'Raspberry Pi 5',
category: 'boards',
description: 'Raspberry Pi 5 with 40-pin GPIO. QEMU virt + Cortex-A76 backend (no raspi5 machine in QEMU yet).',
description: 'Raspberry Pi 5 with 40-pin GPIO + RP1 southbridge. QEMU virt + Cortex-A76 backend.',
thumbnail:
'<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" fill="#76323F" rx="4"/><text x="50%" y="50%" text-anchor="middle" dy=".3em" font-size="10" fill="#FFF">RPi5</text></svg>',
'<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" fill="#004d27" rx="4"/><text x="50%" y="50%" text-anchor="middle" dy=".3em" font-size="10" fill="#FFF">RPi5</text></svg>',
properties: [],
defaultValues: {},
pinCount: 40,

View File

@ -48,6 +48,8 @@ export const BOARD_PIN_GROUPS: Record<AllBoardKinds, BoardPinGroup> = {
vcc_pins: ['3V3', 'VBUS', 'VSYS'],
},
'raspberry-pi-3': { vcc: 5, gnd: ['GND'], vcc_pins: ['5V', '3V3'] },
'raspberry-pi-4': { vcc: 5, gnd: ['GND'], vcc_pins: ['5V', '3V3'] },
'raspberry-pi-5': { vcc: 5, gnd: ['GND'], vcc_pins: ['5V', '3V3'] },
esp32: { vcc: 3.3, gnd: ['GND', 'GND.1', 'GND.2'], vcc_pins: ['3V3', 'VIN', '5V'] },
'esp32-devkit-c-v4': { vcc: 3.3, gnd: ['GND', 'GND.1', 'GND.2'], vcc_pins: ['3V3', 'VIN', '5V'] },

View File

@ -213,6 +213,8 @@ export const BOARD_COMPONENT_IDS = [
'arduino-mega',
'nano-rp2040',
'raspberry-pi-3',
'raspberry-pi-4',
'raspberry-pi-5',
'raspberry-pi-pico',
'pi-pico-w',
'esp32',
@ -295,9 +297,21 @@ export function boardPinToNumber(boardId: string, pinName: string): number | nul
return null;
}
// Raspberry Pi 3B — pinName is the physical pin number ("1" … "40")
// We return the BCM GPIO number, or -1 for power/GND pins.
if (boardId === 'raspberry-pi-3' || boardId.startsWith('raspberry-pi-3')) {
// Raspberry Pi 3 / 4 / 5 (and any future 40-pin Pi) all share the same
// physical pin layout and BCM GPIO assignment, so the same lookup
// table works. `pinName` may be either the physical pin number
// ("1" … "40") OR a BCM-style name ("GPIO14") emitted by the Pi
// element's pinInfo — power / GND pins return -1.
if (
boardId === 'raspberry-pi-3' || boardId.startsWith('raspberry-pi-3') ||
boardId === 'raspberry-pi-4' || boardId.startsWith('raspberry-pi-4') ||
boardId === 'raspberry-pi-5' || boardId.startsWith('raspberry-pi-5')
) {
if (/^(GND|VCC|3V3|5V|ID_S[DC])/.test(pinName)) return -1;
if (pinName.startsWith('GPIO')) {
const n = parseInt(pinName.substring(4), 10);
if (!isNaN(n)) return n;
}
const physical = parseInt(pinName, 10);
if (!isNaN(physical)) return PI3_PHYSICAL_TO_BCM[physical] ?? null;
return null;