From f619a2cc7e0bafcf8d516b4ed6b17b9126117a39 Mon Sep 17 00:00:00 2001 From: David Montero Date: Sat, 23 May 2026 04:46:59 +0200 Subject: [PATCH] feat(boards): expose Raspberry Pi 4 and Pi 5 in the picker (UI + pin wiring) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/components/ComponentPickerModal.tsx | 19 ++- .../src/components/editor/EditorToolbar.tsx | 16 ++- .../components/simulator/BoardOnCanvas.tsx | 8 ++ .../components/simulator/BoardPickerModal.tsx | 6 + .../src/components/simulator/Oscilloscope.tsx | 2 + .../components/simulator/SerialMonitor.tsx | 6 + .../components/simulator/SimulatorCanvas.tsx | 2 + .../velxio-components/RaspberryPi4.tsx | 36 +++++ .../velxio-components/RaspberryPi4Element.ts | 120 ++++++++++++++++ .../velxio-components/RaspberryPi5.tsx | 34 +++++ .../velxio-components/RaspberryPi5Element.ts | 129 ++++++++++++++++++ .../velxio-components/pi40PinHeader.ts | 71 ++++++++++ frontend/src/services/ComponentRegistry.ts | 10 +- .../src/simulation/spice/boardPinGroups.ts | 2 + frontend/src/utils/boardPinMapping.ts | 20 ++- 15 files changed, 466 insertions(+), 15 deletions(-) create mode 100644 frontend/src/components/velxio-components/RaspberryPi4.tsx create mode 100644 frontend/src/components/velxio-components/RaspberryPi4Element.ts create mode 100644 frontend/src/components/velxio-components/RaspberryPi5.tsx create mode 100644 frontend/src/components/velxio-components/RaspberryPi5Element.ts create mode 100644 frontend/src/components/velxio-components/pi40PinHeader.ts diff --git a/frontend/src/components/ComponentPickerModal.tsx b/frontend/src/components/ComponentPickerModal.tsx index 12381a18..110892a9 100644 --- a/frontend/src/components/ComponentPickerModal.tsx +++ b/frontend/src/components/ComponentPickerModal.tsx @@ -41,6 +41,8 @@ const BOARD_DESCRIPTIONS: Record = { '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 = ({ 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; diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index 7da4f6e9..2959a874 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -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 = { '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 = { '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') { diff --git a/frontend/src/components/simulator/BoardOnCanvas.tsx b/frontend/src/components/simulator/BoardOnCanvas.tsx index e2aac2a5..56f4cdc2 100644 --- a/frontend/src/components/simulator/BoardOnCanvas.tsx +++ b/frontend/src/components/simulator/BoardOnCanvas.tsx @@ -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 = { // 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 ; case 'raspberry-pi-3': return ; + case 'raspberry-pi-4': + return ; + case 'raspberry-pi-5': + return ; case 'esp32': case 'esp32-devkit-c-v4': case 'esp32-cam': diff --git a/frontend/src/components/simulator/BoardPickerModal.tsx b/frontend/src/components/simulator/BoardPickerModal.tsx index 8c8039ed..c3c8680a 100644 --- a/frontend/src/components/simulator/BoardPickerModal.tsx +++ b/frontend/src/components/simulator/BoardPickerModal.tsx @@ -9,6 +9,8 @@ const BOARD_DESCRIPTIONS: Record = { '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 = { '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', diff --git a/frontend/src/components/simulator/Oscilloscope.tsx b/frontend/src/components/simulator/Oscilloscope.tsx index 4a3a73cf..c06122d6 100644 --- a/frontend/src/components/simulator/Oscilloscope.tsx +++ b/frontend/src/components/simulator/Oscilloscope.tsx @@ -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 diff --git a/frontend/src/components/simulator/SerialMonitor.tsx b/frontend/src/components/simulator/SerialMonitor.tsx index c5386679..e77be17f 100644 --- a/frontend/src/components/simulator/SerialMonitor.tsx +++ b/frontend/src/components/simulator/SerialMonitor.tsx @@ -18,6 +18,8 @@ const BOARD_SHORT_LABEL: Partial> = { '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> = { '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> = { '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', diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index d1ae203d..9ac53bb7 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -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', diff --git a/frontend/src/components/velxio-components/RaspberryPi4.tsx b/frontend/src/components/velxio-components/RaspberryPi4.tsx new file mode 100644 index 00000000..625a5402 --- /dev/null +++ b/frontend/src/components/velxio-components/RaspberryPi4.tsx @@ -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) => ( + +); diff --git a/frontend/src/components/velxio-components/RaspberryPi4Element.ts b/frontend/src/components/velxio-components/RaspberryPi4Element.ts new file mode 100644 index 00000000..7fde450f --- /dev/null +++ b/frontend/src/components/velxio-components/RaspberryPi4Element.ts @@ -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 += ``; + pinsSvg += ``; + }); + + this.shadowRoot!.innerHTML = ` + + + + + + + + BCM2711 + A72 ×4 + + + + USB3 + + USB2 + + + + GbE + + + + USB-C 5V + + + + + µHDMI 0 / 1 + + + + + + + + + ${pinsSvg} + + + Raspberry Pi 4 + Model B · Cortex-A72 + velxio + + `; + } +} + +if (!customElements.get('velxio-raspberry-pi-4')) { + customElements.define('velxio-raspberry-pi-4', RaspberryPi4Element); +} + +export {}; diff --git a/frontend/src/components/velxio-components/RaspberryPi5.tsx b/frontend/src/components/velxio-components/RaspberryPi5.tsx new file mode 100644 index 00000000..ca138e10 --- /dev/null +++ b/frontend/src/components/velxio-components/RaspberryPi5.tsx @@ -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) => ( + +); diff --git a/frontend/src/components/velxio-components/RaspberryPi5Element.ts b/frontend/src/components/velxio-components/RaspberryPi5Element.ts new file mode 100644 index 00000000..d66ccfcb --- /dev/null +++ b/frontend/src/components/velxio-components/RaspberryPi5Element.ts @@ -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 += ``; + pinsSvg += ``; + }); + + this.shadowRoot!.innerHTML = ` + + + + + + + + BCM2712 + A76 ×4 + + + + RP1 + + + + USB3 + + USB3 + + + + 2.5GbE + + + + USB-C 5V/5A + + + + PWR + + + + + µHDMI 0 / 1 + + + + PCIe FFC + + + + + + ${pinsSvg} + + + Raspberry Pi 5 + Cortex-A76 · BCM2712 · RP1 + velxio + + `; + } +} + +if (!customElements.get('velxio-raspberry-pi-5')) { + customElements.define('velxio-raspberry-pi-5', RaspberryPi5Element); +} + +export {}; diff --git a/frontend/src/components/velxio-components/pi40PinHeader.ts b/frontend/src/components/velxio-components/pi40PinHeader.ts new file mode 100644 index 00000000..8b7954de --- /dev/null +++ b/frontend/src/components/velxio-components/pi40PinHeader.ts @@ -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 = { + 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; +} diff --git a/frontend/src/services/ComponentRegistry.ts b/frontend/src/services/ComponentRegistry.ts index 714cca8d..9701029b 100644 --- a/frontend/src/services/ComponentRegistry.ts +++ b/frontend/src/services/ComponentRegistry.ts @@ -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: - 'RPi4', + 'RPi4', 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: - 'RPi5', + 'RPi5', properties: [], defaultValues: {}, pinCount: 40, diff --git a/frontend/src/simulation/spice/boardPinGroups.ts b/frontend/src/simulation/spice/boardPinGroups.ts index a5b27e54..c77bad92 100644 --- a/frontend/src/simulation/spice/boardPinGroups.ts +++ b/frontend/src/simulation/spice/boardPinGroups.ts @@ -48,6 +48,8 @@ export const BOARD_PIN_GROUPS: Record = { 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'] }, diff --git a/frontend/src/utils/boardPinMapping.ts b/frontend/src/utils/boardPinMapping.ts index 1181ef02..300da8aa 100644 --- a/frontend/src/utils/boardPinMapping.ts +++ b/frontend/src/utils/boardPinMapping.ts @@ -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;