From 3989f28eb168b1a3f66cca424b6fb94556a1b137 Mon Sep 17 00:00:00 2001 From: David Montero Date: Wed, 26 Aug 2026 05:06:28 +0200 Subject: [PATCH] feat(boards): the Zero, the 1B+ and the 2B stop pretending to be a Pi 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four Raspberry Pi kinds shipped one picture. BoardOnCanvas routed raspberry-pi-zero, -1 and -2 straight to , and the picker card did the same, both with a comment explaining that the 40-pin header is "the canvas identity" so the art could be shared. It cannot, for the Zero. That board is 65 x 30 mm against the Pi 3's 85 x 56, it ships its header UNPOPULATED, and it has neither Ethernet nor a full-size USB port. Drawing it as a Pi 3 tells a student the opposite of what is in their hand, and it is a teaching simulator. The 1B+ and the 2B really do share the Pi 3's outline and connector positions, but not the silicon and not the silkscreen, and those are exactly what someone reads a board photo for. Three new illustrations, flat-vector in the house style: the palette is lifted verbatim from Raspberry_Pi_3_illustration.svg (PCB #037e3f, gold pads #90814e, mount rings #f9e494, HDMI #e8914f, steel #e5e5e3) and the raspberry silk is the same path the Pico art uses, so they sit next to the Pi 3 without a seam. Zero 2 W 650 x 314 units — bare plated header holes, RP3A0-AU package, RF can, microSD cage, mini-HDMI, two micro-USB, CSI on the edge 1 Model B+ 884 x 578 — BCM2835 / ARM1176JZF-S, "Model B+ V1.2" silk 2 Model B 884 x 578 — BCM2836 / Cortex-A7, "Pi 2 Model B V1.1" silk buildPi40PinHeader() was already in the tree, already carrying the BCM name map, and used by nothing — its geometry was hardcoded to a 250x160 footprint. It now takes the header origin, pitch and row offsets, so each board passes the numbers ITS OWN art is drawn from and the pin tips cannot drift off the pads. Verified by overlaying the computed positions on the rendered art: all 40 land on the drawn holes on all three boards. Not touched: ComponentRegistry still gives these kinds tagName velxio-raspberry-pi-3. That tag only feeds PartInspectorDialog's preview, and the element behind it draws a crude red rectangle for the Pi 3 too — a separate wart, and one that wants its own fix rather than three more copies. tsc --noEmit clean across the frontend, build:docker green, and the 76 tests in board-kinds-coverage + multi-board-integration pass. --- .../assets/Raspberry_Pi_1_illustration.svg | 180 ++++++++++++++++++ .../assets/Raspberry_Pi_2_illustration.svg | 180 ++++++++++++++++++ .../assets/Raspberry_Pi_Zero_illustration.svg | 149 +++++++++++++++ .../src/components/ComponentPickerModal.tsx | 22 ++- .../components/simulator/BoardOnCanvas.tsx | 15 +- .../velxio-components/RaspberryPi1.tsx | 66 +++++++ .../velxio-components/RaspberryPi2.tsx | 66 +++++++ .../velxio-components/RaspberryPiZero.tsx | 74 +++++++ .../velxio-components/pi40PinHeader.ts | 42 ++-- 9 files changed, 772 insertions(+), 22 deletions(-) create mode 100644 frontend/src/assets/Raspberry_Pi_1_illustration.svg create mode 100644 frontend/src/assets/Raspberry_Pi_2_illustration.svg create mode 100644 frontend/src/assets/Raspberry_Pi_Zero_illustration.svg create mode 100644 frontend/src/components/velxio-components/RaspberryPi1.tsx create mode 100644 frontend/src/components/velxio-components/RaspberryPi2.tsx create mode 100644 frontend/src/components/velxio-components/RaspberryPiZero.tsx diff --git a/frontend/src/assets/Raspberry_Pi_1_illustration.svg b/frontend/src/assets/Raspberry_Pi_1_illustration.svg new file mode 100644 index 00000000..b87e05bd --- /dev/null +++ b/frontend/src/assets/Raspberry_Pi_1_illustration.svg @@ -0,0 +1,180 @@ + + Raspberry Pi 1 Model B+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GPIO + + + Raspberry Pi Model B+ V1.2 + (c) Raspberry Pi 2014 + + C E + FCC + + + + + BROADCOM + BCM2835 + ARM1176JZF-S + + + + + LAN9514 + + + + + + DISPLAY + + + + + CAMERA + + + + + + + + + + + + + + + + ETHERNET + + + + + PWR IN + + + + + HDMI + + + + A/V + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/Raspberry_Pi_2_illustration.svg b/frontend/src/assets/Raspberry_Pi_2_illustration.svg new file mode 100644 index 00000000..7b93a59b --- /dev/null +++ b/frontend/src/assets/Raspberry_Pi_2_illustration.svg @@ -0,0 +1,180 @@ + + Raspberry Pi 2 Model B + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GPIO + + + Raspberry Pi 2 Model B V1.1 + (c) Raspberry Pi 2014 + + C E + FCC + + + + + BROADCOM + BCM2836 + ARM Cortex-A7 + + + + + LAN9514 + + + + + + DISPLAY + + + + + CAMERA + + + + + + + + + + + + + + + + ETHERNET + + + + + PWR IN + + + + + HDMI + + + + A/V + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/Raspberry_Pi_Zero_illustration.svg b/frontend/src/assets/Raspberry_Pi_Zero_illustration.svg new file mode 100644 index 00000000..44852ad8 --- /dev/null +++ b/frontend/src/assets/Raspberry_Pi_Zero_illustration.svg @@ -0,0 +1,149 @@ + + Raspberry Pi Zero 2 W + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GPIO + + + + + + + + + + + + RP3A0-AU + 2041 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + USB + + + + PWR IN + + + diff --git a/frontend/src/components/ComponentPickerModal.tsx b/frontend/src/components/ComponentPickerModal.tsx index 45f8c233..7a6262f5 100644 --- a/frontend/src/components/ComponentPickerModal.tsx +++ b/frontend/src/components/ComponentPickerModal.tsx @@ -47,6 +47,9 @@ import { type OnlineOnlyBoardAd, type OnlineOnlyComponentAd, } from '../lib/onlineOnlyBoards'; +import raspberryPiZeroSvg from '../assets/Raspberry_Pi_Zero_illustration.svg'; +import raspberryPi1Svg from '../assets/Raspberry_Pi_1_illustration.svg'; +import raspberryPi2Svg from '../assets/Raspberry_Pi_2_illustration.svg'; import raspberryPi3Svg from '../assets/Raspberry_Pi_3_illustration.svg'; import raspberryPi4Png from '../assets/raspberry-pi-4-board.png'; import raspberryPi5Png from '../assets/raspberry-pi-5-board.png'; @@ -821,15 +824,18 @@ const BoardCard: React.FC = ({ kind, onSelect, hoverApi }) => { }; }, [kind]); - const reactThumbnail = - // Zero/1/2 render on canvas through the Pi-3 element (same 40-pin art), - // so their cards reuse the same illustration. - kind === 'raspberry-pi-3' || - kind === 'raspberry-pi-zero' || - kind === 'raspberry-pi-1' || - kind === 'raspberry-pi-2' ? ( + // Every Pi shows its own board. The cards used to share the Pi 3's picture, + // which made the picker claim a Zero looks like a Pi 3. + const piArt: Partial> = { + 'raspberry-pi-zero': raspberryPiZeroSvg, + 'raspberry-pi-1': raspberryPi1Svg, + 'raspberry-pi-2': raspberryPi2Svg, + 'raspberry-pi-3': raspberryPi3Svg, + }; + + const reactThumbnail = piArt[kind] ? ( {BOARD_KIND_LABELS[kind]} diff --git a/frontend/src/components/simulator/BoardOnCanvas.tsx b/frontend/src/components/simulator/BoardOnCanvas.tsx index a7db42eb..bd7c61fe 100644 --- a/frontend/src/components/simulator/BoardOnCanvas.tsx +++ b/frontend/src/components/simulator/BoardOnCanvas.tsx @@ -10,6 +10,9 @@ import { ArduinoMega } from '../velxio-components/ArduinoMega'; // boardKind by mistake — kept the import out so future contributors don't // 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 { RaspberryPiZero } from '../velxio-components/RaspberryPiZero'; +import { RaspberryPi1 } from '../velxio-components/RaspberryPi1'; +import { RaspberryPi2 } from '../velxio-components/RaspberryPi2'; import { RaspberryPi3 } from '../velxio-components/RaspberryPi3'; import { RaspberryPi4 } from '../velxio-components/RaspberryPi4'; import { RaspberryPi5 } from '../velxio-components/RaspberryPi5'; @@ -167,12 +170,18 @@ export const BoardOnCanvas = ({ case 'raspberry-pi-pico': case 'pi-pico-w': return ; - // Zero/1/2 share the Pi-3's 40-pin board art (their canvas identity is - // the header, and the ComponentRegistry metadata reuses the same tag); - // only the backend QEMU profile differs per kind. + // Each Pi draws as itself. Zero/1/2 used to borrow the Pi 3's picture + // "because the header is the canvas identity" — but the Zero is 65x30 mm + // against the Pi 3's 85x56, ships its header unpopulated and has neither + // Ethernet nor full-size USB, so that shortcut drew a board the student + // was not holding. 1B+ and 2B do share the Pi 3's outline; their silicon + // and silkscreen do not. case 'raspberry-pi-zero': + return ; case 'raspberry-pi-1': + return ; case 'raspberry-pi-2': + return ; case 'raspberry-pi-3': return ; case 'raspberry-pi-4': diff --git a/frontend/src/components/velxio-components/RaspberryPi1.tsx b/frontend/src/components/velxio-components/RaspberryPi1.tsx new file mode 100644 index 00000000..b0ca43f5 --- /dev/null +++ b/frontend/src/components/velxio-components/RaspberryPi1.tsx @@ -0,0 +1,66 @@ +import { useEffect, useRef } from 'react'; +import boardSvg from '../../assets/Raspberry_Pi_1_illustration.svg'; +import { buildPi40PinHeader } from './pi40PinHeader'; + +/** + * Raspberry Pi 1 Model B+ — Velxio board art. + * + * Shares the 85 x 56 mm "B+ form factor" with the Pi 3, which is why this + * board used to be drawn AS a Pi 3. The outline and the connector positions + * really are identical; the silicon is not — BCM2835 with the RAM stacked on top and no separate Elpida package — and neither is the + * silkscreen, so the picture now says which board the student is holding. + * + * Geometry is in the SVG's own units (10 per mm), the same numbers the art is + * drawn from, so the pin tips land on the drawn pads. Pitch is a true 2.54 mm. + */ + +const SVG_VB_W = 884; // 85 mm of PCB + the USB/Ethernet overhang on the right +const SVG_VB_H = 578; // 56 mm of PCB + the connector overhang at the bottom +const HDR_X0 = 72; +const HDR_STEP = 25.4; +const HDR_Y_TOP = 42; +const HDR_Y_BOT = 67.4; + +const DISPLAY_W = 330; +const SCALE = DISPLAY_W / SVG_VB_W; +const DISPLAY_H = Math.round(SVG_VB_H * SCALE); + +interface Props { + id?: string; + x?: number; + y?: number; +} + +export const RaspberryPi1 = ({ id = 'raspberry-pi-1', x = 0, y = 0 }: Props) => { + const imgRef = useRef(null); + + useEffect(() => { + if (imgRef.current) { + (imgRef.current as unknown as { pinInfo: unknown }).pinInfo = buildPi40PinHeader({ + xStart: HDR_X0 * SCALE, + xStep: HDR_STEP * SCALE, + yTop: HDR_Y_TOP * SCALE, + yBot: HDR_Y_BOT * SCALE, + }); + } + }, []); + + return ( + Raspberry Pi 1 Model B+ + ); +}; diff --git a/frontend/src/components/velxio-components/RaspberryPi2.tsx b/frontend/src/components/velxio-components/RaspberryPi2.tsx new file mode 100644 index 00000000..7f018a00 --- /dev/null +++ b/frontend/src/components/velxio-components/RaspberryPi2.tsx @@ -0,0 +1,66 @@ +import { useEffect, useRef } from 'react'; +import boardSvg from '../../assets/Raspberry_Pi_2_illustration.svg'; +import { buildPi40PinHeader } from './pi40PinHeader'; + +/** + * Raspberry Pi 2 Model B — Velxio board art. + * + * Shares the 85 x 56 mm "B+ form factor" with the Pi 3, which is why this + * board used to be drawn AS a Pi 3. The outline and the connector positions + * really are identical; the silicon is not — BCM2836, the first quad-core, with its RAM moved to the underside — and neither is the + * silkscreen, so the picture now says which board the student is holding. + * + * Geometry is in the SVG's own units (10 per mm), the same numbers the art is + * drawn from, so the pin tips land on the drawn pads. Pitch is a true 2.54 mm. + */ + +const SVG_VB_W = 884; // 85 mm of PCB + the USB/Ethernet overhang on the right +const SVG_VB_H = 578; // 56 mm of PCB + the connector overhang at the bottom +const HDR_X0 = 72; +const HDR_STEP = 25.4; +const HDR_Y_TOP = 42; +const HDR_Y_BOT = 67.4; + +const DISPLAY_W = 330; +const SCALE = DISPLAY_W / SVG_VB_W; +const DISPLAY_H = Math.round(SVG_VB_H * SCALE); + +interface Props { + id?: string; + x?: number; + y?: number; +} + +export const RaspberryPi2 = ({ id = 'raspberry-pi-2', x = 0, y = 0 }: Props) => { + const imgRef = useRef(null); + + useEffect(() => { + if (imgRef.current) { + (imgRef.current as unknown as { pinInfo: unknown }).pinInfo = buildPi40PinHeader({ + xStart: HDR_X0 * SCALE, + xStep: HDR_STEP * SCALE, + yTop: HDR_Y_TOP * SCALE, + yBot: HDR_Y_BOT * SCALE, + }); + } + }, []); + + return ( + Raspberry Pi 2 Model B + ); +}; diff --git a/frontend/src/components/velxio-components/RaspberryPiZero.tsx b/frontend/src/components/velxio-components/RaspberryPiZero.tsx new file mode 100644 index 00000000..c19bfa9a --- /dev/null +++ b/frontend/src/components/velxio-components/RaspberryPiZero.tsx @@ -0,0 +1,74 @@ +import { useEffect, useRef } from 'react'; +import raspberryPiZeroSvg from '../../assets/Raspberry_Pi_Zero_illustration.svg'; +import { buildPi40PinHeader } from './pi40PinHeader'; + +/** + * Raspberry Pi Zero 2 W — Velxio board art. + * + * The Zero used to borrow the Pi 3's picture. That is wrong in the one way a + * board picture can be wrong: it is a different SIZE. The Zero is 65 x 30 mm + * against the Pi 3's 85 x 56 mm, its header ships unpopulated, and it has no + * Ethernet and no full-size USB. A canvas that draws it as a Pi 3 tells the + * student the opposite of what the hardware looks like. + * + * Geometry below is in the SVG's own units (10 per mm) and is the SAME set of + * numbers the artwork is drawn from, so the pin tips cannot drift off the + * pads. Header pitch is a true 2.54 mm. + */ + +const SVG_W = 650; // 65 mm +const SVG_VB_H = 314; // 30 mm of PCB + the connector overhang at the bottom +const HDR_X0 = 82; +const HDR_STEP = 25.4; +const HDR_Y_TOP = 31; +const HDR_Y_BOT = 56.4; + +// 65 mm next to the Pi 3's 85 mm at 320 px: 320 * 65 / 85 ~ 245. Round to 250 +// so the Zero reads as the small board it is when both sit on one canvas. +const DISPLAY_W = 250; +const SCALE = DISPLAY_W / SVG_W; +const DISPLAY_H = Math.round(SVG_VB_H * SCALE); + +interface RaspberryPiZeroProps { + id?: string; + x?: number; + y?: number; +} + +export const RaspberryPiZero = ({ + id = 'raspberry-pi-zero', + x = 0, + y = 0, +}: RaspberryPiZeroProps) => { + const imgRef = useRef(null); + + useEffect(() => { + if (imgRef.current) { + (imgRef.current as unknown as { pinInfo: unknown }).pinInfo = buildPi40PinHeader({ + xStart: HDR_X0 * SCALE, + xStep: HDR_STEP * SCALE, + yTop: HDR_Y_TOP * SCALE, + yBot: HDR_Y_BOT * SCALE, + }); + } + }, []); + + return ( + Raspberry Pi Zero 2 W + ); +}; diff --git a/frontend/src/components/velxio-components/pi40PinHeader.ts b/frontend/src/components/velxio-components/pi40PinHeader.ts index 8b7954de..8e2e63e4 100644 --- a/frontend/src/components/velxio-components/pi40PinHeader.ts +++ b/frontend/src/components/velxio-components/pi40PinHeader.ts @@ -4,9 +4,12 @@ * 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. + * Coordinates are in CSS pixels relative to the board element's top-left. + * Every Pi draws its header at a different place on its own artwork, so the + * geometry is a parameter: pass the header origin, the column pitch and the + * two row offsets AS MEASURED ON THE ART, and get pin tips that land on the + * drawn pads. 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 @@ -20,10 +23,24 @@ export interface PinInfo { signals: string[]; } -const PIN_X_START = 20; -const PIN_X_STEP = 10; -const PIN_Y_TOP = 10; -const PIN_Y_BOT = 20; +export interface Pi40HeaderGeometry { + /** x of pin 1 / pin 2 (column 0), in CSS pixels. */ + xStart: number; + /** Column pitch in CSS pixels — 2.54 mm scaled by the art's own scale. */ + xStep: number; + /** y of the odd (top) row. */ + yTop: number; + /** y of the even (bottom) row. */ + yBot: number; +} + +/** The 250x160 footprint the first Velxio Pi element was drawn at. */ +const DEFAULT_GEOMETRY: Pi40HeaderGeometry = { + xStart: 20, + xStep: 10, + yTop: 10, + yBot: 20, +}; const PI_PIN_NAMES: Record = { 1: '3V3', 2: '5V', @@ -48,22 +65,25 @@ const PI_PIN_NAMES: Record = { 39: 'GND', 40: 'GPIO21', }; -export function buildPi40PinHeader(): PinInfo[] { +export function buildPi40PinHeader( + geometry: Pi40HeaderGeometry = DEFAULT_GEOMETRY, +): PinInfo[] { + const { xStart, xStep, yTop, yBot } = geometry; 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; + const px = xStart + col * xStep; pins.push({ name: PI_PIN_NAMES[oddPin] ?? `P${oddPin}`, x: px, - y: PIN_Y_TOP, + y: yTop, signals: [], }); pins.push({ name: PI_PIN_NAMES[evenPin] ?? `P${evenPin}`, x: px, - y: PIN_Y_BOT, + y: yBot, signals: [], }); }