From 909c94c5fb6ece3e56d8fee44d87166db96a3069 Mon Sep 17 00:00:00 2001 From: David Montero Date: Tue, 2 Jun 2026 04:36:58 +0200 Subject: [PATCH] feat(examples): add single-board Raspberry Pi 3/4/5 GPIO examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six gpiozero (Python) examples to exercise the Pi 3/4/5 QEMU Linux boards with different sensors/actuators. All strictly digital — the Pi has no ADC and PWM is not simulated, so this covers the GPIO in/out paths that work: - [Pi 3] Blink an LED - [Pi 3] Running Lights (5 LEDs) - [Pi 4] Button Toggles LED - [Pi 4] RGB LED Color Cycle (digital, 7 colors, pwm=False) - [Pi 5] PIR Motion Alarm - [Pi 5] Traffic Light Structure mirrors the existing Pi example (boards[] + vfsFiles['script.py'], run via 'python3 /home/pi/script.py'); LEDs wired directly like nano-button-led. gpiozero is used because it works across Pi 3/4/5 (RPi.GPIO doesn't on Pi 5). Adds a smoke test loading all six (board kind, components, wiring consistency, gpiozero script present in the VFS). Co-Authored-By: Claude Opus 4.8 --- .../pi-single-board-examples.test.ts | 65 ++++ frontend/src/data/examples.ts | 327 ++++++++++++++++++ 2 files changed, 392 insertions(+) create mode 100644 frontend/src/__tests__/pi-single-board-examples.test.ts diff --git a/frontend/src/__tests__/pi-single-board-examples.test.ts b/frontend/src/__tests__/pi-single-board-examples.test.ts new file mode 100644 index 00000000..d25cf600 --- /dev/null +++ b/frontend/src/__tests__/pi-single-board-examples.test.ts @@ -0,0 +1,65 @@ +// @vitest-environment jsdom +/** + * Smoke test for the single-board Raspberry Pi 3/4/5 examples: each must + * load into the stores with the right board kind, the expected components, + * and a non-empty Python script in the board's VFS (script.py). + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useSimulatorStore } from '../store/useSimulatorStore'; +import { useVfsStore } from '../store/useVfsStore'; +import { useElectricalStore } from '../store/useElectricalStore'; +import { loadExample } from '../utils/loadExample'; +import { exampleProjects } from '../data/examples'; + +function resetStores() { + const sim = useSimulatorStore.getState(); + for (const id of sim.boards.map((b) => b.id)) sim.removeBoard(id); + useElectricalStore.getState().setPaused(false); +} + +const CASES: { id: string; kind: string; minComponents: number }[] = [ + { id: 'pi3-blink-led', kind: 'raspberry-pi-3', minComponents: 1 }, + { id: 'pi3-running-lights', kind: 'raspberry-pi-3', minComponents: 5 }, + { id: 'pi4-button-led', kind: 'raspberry-pi-4', minComponents: 2 }, + { id: 'pi4-rgb-color-cycle', kind: 'raspberry-pi-4', minComponents: 1 }, + { id: 'pi5-pir-motion-alarm', kind: 'raspberry-pi-5', minComponents: 2 }, + { id: 'pi5-traffic-light', kind: 'raspberry-pi-5', minComponents: 3 }, +]; + +describe('Raspberry Pi 3/4/5 single-board examples', () => { + beforeEach(() => { + resetStores(); + }); + + for (const c of CASES) { + it(`loads ${c.id} (single ${c.kind}, gpiozero script, components wired)`, async () => { + const example = exampleProjects.find((e) => e.id === c.id); + expect(example, `example ${c.id} exists`).toBeDefined(); + + await loadExample(example!); + + const sim = useSimulatorStore.getState(); + // Exactly one board, of the expected Pi kind. + expect(sim.boards).toHaveLength(1); + expect(sim.boards[0].boardKind).toBe(c.kind); + + // Components placed. + expect(sim.components.length).toBeGreaterThanOrEqual(c.minComponents); + + // Every wire endpoint references either the board or a placed component. + const ids = new Set([sim.boards[0].id, ...sim.components.map((comp) => comp.id)]); + for (const w of sim.wires) { + expect(ids.has(w.start.componentId)).toBe(true); + expect(ids.has(w.end.componentId)).toBe(true); + } + + // The Python script landed in the board's VFS (script.py, non-empty). + const tree = useVfsStore.getState().getTree(sim.boards[0].id); + const scriptNode = Object.values(tree).find( + (n) => n.type === 'file' && n.name === 'script.py', + ); + expect(scriptNode, 'script.py present in VFS').toBeTruthy(); + expect(scriptNode!.content ?? '').toContain('gpiozero'); + }); + } +}); diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts index 8b0456ad..a4764f29 100644 --- a/frontend/src/data/examples.ts +++ b/frontend/src/data/examples.ts @@ -9108,6 +9108,333 @@ void loop() { }, ], }, + + // ── Raspberry Pi 3 / 4 / 5 — single-board GPIO examples ────────────────── + // These run a Python (gpiozero) script on the QEMU Linux board. gpiozero is + // board-agnostic (works on Pi 3/4/5). Only DIGITAL GPIO is wired — output + // (LEDs, RGB) and input (button, PIR) — since the Pi has no ADC and PWM is + // not simulated. To run: Start the Pi, click Upload in the File System + // panel, then `python3 /home/pi/script.py`. + { + id: 'pi3-blink-led', + title: '[Pi 3] Blink an LED', + description: + 'Raspberry Pi 3 blinks a red LED on GPIO17 with gpiozero. Start the Pi, click Upload in the File System panel, then run: python3 /home/pi/script.py', + category: 'basics', + difficulty: 'beginner', + code: '', + boards: [ + { + boardKind: 'raspberry-pi-3', + x: 80, + y: 80, + vfsFiles: { + 'script.py': `#!/usr/bin/env python3 +# Raspberry Pi 3 - Blink an LED +# Wiring: GPIO17 -> LED(+) ; LED(-) -> GND (add a 220 ohm series +# resistor on real hardware; the simulator's LED needs none). +from gpiozero import LED +from time import sleep + +led = LED(17) +print("Blinking LED on GPIO17 - Ctrl-C to stop") +try: + while True: + led.on() + print("LED ON") + sleep(1) + led.off() + print("LED OFF") + sleep(1) +except KeyboardInterrupt: + led.off() + print("Stopped") +`, + }, + }, + ], + components: [ + { type: 'wokwi-led', id: 'pi3b-led', x: 540, y: 160, properties: { color: 'red' } }, + ], + wires: [ + { id: 'w-sig', start: { componentId: 'raspberry-pi-3', pinName: 'GPIO17' }, end: { componentId: 'pi3b-led', pinName: 'A' }, color: '#e74c3c' }, + { id: 'w-gnd', start: { componentId: 'pi3b-led', pinName: 'C' }, end: { componentId: 'raspberry-pi-3', pinName: 'GND' }, color: '#000000' }, + ], + }, + { + id: 'pi3-running-lights', + title: '[Pi 3] Running Lights (5 LEDs)', + description: + 'Raspberry Pi 3 sweeps a Knight-Rider pattern across 5 LEDs on GPIO17/27/22/5/6 with gpiozero. Start the Pi, Upload, then run: python3 /home/pi/script.py', + category: 'basics', + difficulty: 'intermediate', + code: '', + boards: [ + { + boardKind: 'raspberry-pi-3', + x: 80, + y: 80, + vfsFiles: { + 'script.py': `#!/usr/bin/env python3 +# Raspberry Pi 3 - Knight Rider / running lights across 5 LEDs +from gpiozero import LED +from time import sleep + +pins = [17, 27, 22, 5, 6] +leds = [LED(p) for p in pins] +print("Running lights on GPIO", pins, "- Ctrl-C to stop") +try: + while True: + for i in range(len(leds)): + leds[i].on() + print("LED", pins[i], "ON") + sleep(0.12) + leds[i].off() + for i in range(len(leds) - 2, 0, -1): + leds[i].on() + print("LED", pins[i], "ON") + sleep(0.12) + leds[i].off() +except KeyboardInterrupt: + for led in leds: + led.off() + print("Stopped") +`, + }, + }, + ], + components: [ + { type: 'wokwi-led', id: 'pi3r-led1', x: 540, y: 60, properties: { color: 'red' } }, + { type: 'wokwi-led', id: 'pi3r-led2', x: 540, y: 140, properties: { color: 'orange' } }, + { type: 'wokwi-led', id: 'pi3r-led3', x: 540, y: 220, properties: { color: 'yellow' } }, + { type: 'wokwi-led', id: 'pi3r-led4', x: 540, y: 300, properties: { color: 'green' } }, + { type: 'wokwi-led', id: 'pi3r-led5', x: 540, y: 380, properties: { color: 'blue' } }, + ], + wires: [ + { id: 'w1', start: { componentId: 'raspberry-pi-3', pinName: 'GPIO17' }, end: { componentId: 'pi3r-led1', pinName: 'A' }, color: '#e74c3c' }, + { id: 'w2', start: { componentId: 'raspberry-pi-3', pinName: 'GPIO27' }, end: { componentId: 'pi3r-led2', pinName: 'A' }, color: '#e67e22' }, + { id: 'w3', start: { componentId: 'raspberry-pi-3', pinName: 'GPIO22' }, end: { componentId: 'pi3r-led3', pinName: 'A' }, color: '#f1c40f' }, + { id: 'w4', start: { componentId: 'raspberry-pi-3', pinName: 'GPIO5' }, end: { componentId: 'pi3r-led4', pinName: 'A' }, color: '#2ecc71' }, + { id: 'w5', start: { componentId: 'raspberry-pi-3', pinName: 'GPIO6' }, end: { componentId: 'pi3r-led5', pinName: 'A' }, color: '#3498db' }, + { id: 'g1', start: { componentId: 'pi3r-led1', pinName: 'C' }, end: { componentId: 'raspberry-pi-3', pinName: 'GND' }, color: '#000000' }, + { id: 'g2', start: { componentId: 'pi3r-led2', pinName: 'C' }, end: { componentId: 'raspberry-pi-3', pinName: 'GND' }, color: '#000000' }, + { id: 'g3', start: { componentId: 'pi3r-led3', pinName: 'C' }, end: { componentId: 'raspberry-pi-3', pinName: 'GND' }, color: '#000000' }, + { id: 'g4', start: { componentId: 'pi3r-led4', pinName: 'C' }, end: { componentId: 'raspberry-pi-3', pinName: 'GND' }, color: '#000000' }, + { id: 'g5', start: { componentId: 'pi3r-led5', pinName: 'C' }, end: { componentId: 'raspberry-pi-3', pinName: 'GND' }, color: '#000000' }, + ], + }, + { + id: 'pi4-button-led', + title: '[Pi 4] Button Toggles LED', + description: + 'Raspberry Pi 4 reads a push button on GPIO2 and toggles an LED on GPIO17 with gpiozero. Start the Pi, Upload, run python3 /home/pi/script.py, then click the button.', + category: 'basics', + difficulty: 'beginner', + code: '', + boards: [ + { + boardKind: 'raspberry-pi-4', + x: 80, + y: 80, + vfsFiles: { + 'script.py': `#!/usr/bin/env python3 +# Raspberry Pi 4 - Button toggles an LED +# Wiring: button GPIO2 <-> GND (internal pull-up; pressed reads LOW). +# LED on GPIO17 -> LED(+) ; LED(-) -> GND +from gpiozero import Button, LED +from time import sleep + +button = Button(2) # pull_up=True by default +led = LED(17) +print("Press the button to toggle the LED - Ctrl-C to stop") +state = False +try: + while True: + button.wait_for_press() + state = not state + led.value = state + print("LED", "ON" if state else "OFF") + sleep(0.3) # simple debounce +except KeyboardInterrupt: + led.off() + print("Stopped") +`, + }, + }, + ], + components: [ + { type: 'wokwi-pushbutton', id: 'pi4-btn', x: 540, y: 120, properties: { color: 'green' } }, + { type: 'wokwi-led', id: 'pi4-led', x: 540, y: 300, properties: { color: 'blue' } }, + ], + wires: [ + { id: 'w-btn', start: { componentId: 'raspberry-pi-4', pinName: 'GPIO2' }, end: { componentId: 'pi4-btn', pinName: '1.l' }, color: '#00aaff' }, + { id: 'w-btn-gnd', start: { componentId: 'pi4-btn', pinName: '2.l' }, end: { componentId: 'raspberry-pi-4', pinName: 'GND' }, color: '#000000' }, + { id: 'w-led', start: { componentId: 'raspberry-pi-4', pinName: 'GPIO17' }, end: { componentId: 'pi4-led', pinName: 'A' }, color: '#3498db' }, + { id: 'w-led-gnd', start: { componentId: 'pi4-led', pinName: 'C' }, end: { componentId: 'raspberry-pi-4', pinName: 'GND' }, color: '#000000' }, + ], + }, + { + id: 'pi4-rgb-color-cycle', + title: '[Pi 4] RGB LED Color Cycle', + description: + 'Raspberry Pi 4 cycles a common-cathode RGB LED through 7 colors using digital on/off on GPIO17/27/22 (gpiozero RGBLED, pwm=False). Start the Pi, Upload, run python3 /home/pi/script.py', + category: 'basics', + difficulty: 'intermediate', + code: '', + boards: [ + { + boardKind: 'raspberry-pi-4', + x: 80, + y: 80, + vfsFiles: { + 'script.py': `#!/usr/bin/env python3 +# Raspberry Pi 4 - RGB LED color cycle (digital, 7 colors) +# Wiring: R->GPIO17, G->GPIO27, B->GPIO22, COM->GND (common cathode). +# pwm=False -> each channel is plain on/off, so no PWM is needed. +from gpiozero import RGBLED +from time import sleep + +rgb = RGBLED(red=17, green=27, blue=22, pwm=False) +colors = [ + ("red", (1, 0, 0)), + ("green", (0, 1, 0)), + ("blue", (0, 0, 1)), + ("yellow", (1, 1, 0)), + ("cyan", (0, 1, 1)), + ("magenta", (1, 0, 1)), + ("white", (1, 1, 1)), +] +print("Cycling RGB colors - Ctrl-C to stop") +try: + while True: + for name, value in colors: + rgb.color = value + print(name) + sleep(1) +except KeyboardInterrupt: + rgb.off() + print("Stopped") +`, + }, + }, + ], + components: [ + { type: 'wokwi-rgb-led', id: 'pi4-rgb', x: 560, y: 160, properties: {} }, + ], + wires: [ + { id: 'w-r', start: { componentId: 'raspberry-pi-4', pinName: 'GPIO17' }, end: { componentId: 'pi4-rgb', pinName: 'R' }, color: '#e74c3c' }, + { id: 'w-g', start: { componentId: 'raspberry-pi-4', pinName: 'GPIO27' }, end: { componentId: 'pi4-rgb', pinName: 'G' }, color: '#2ecc71' }, + { id: 'w-b', start: { componentId: 'raspberry-pi-4', pinName: 'GPIO22' }, end: { componentId: 'pi4-rgb', pinName: 'B' }, color: '#3498db' }, + { id: 'w-com', start: { componentId: 'pi4-rgb', pinName: 'COM' }, end: { componentId: 'raspberry-pi-4', pinName: 'GND' }, color: '#000000' }, + ], + }, + { + id: 'pi5-pir-motion-alarm', + title: '[Pi 5] PIR Motion Alarm', + description: + 'Raspberry Pi 5 lights an LED on GPIO17 whenever a PIR motion sensor on GPIO4 detects movement (gpiozero MotionSensor). Start the Pi, Upload, run python3 /home/pi/script.py, then trigger the PIR.', + category: 'sensors', + difficulty: 'intermediate', + code: '', + boards: [ + { + boardKind: 'raspberry-pi-5', + x: 80, + y: 80, + vfsFiles: { + 'script.py': `#!/usr/bin/env python3 +# Raspberry Pi 5 - PIR motion alarm +# Wiring: PIR VCC->5V, GND->GND, OUT->GPIO4 ; LED on GPIO17. +from gpiozero import MotionSensor, LED +from time import sleep + +pir = MotionSensor(4) +led = LED(17) +print("Motion alarm armed - trigger the PIR sensor (Ctrl-C to stop)") +try: + while True: + if pir.motion_detected: + led.on() + print("Motion detected!") + else: + led.off() + sleep(0.2) +except KeyboardInterrupt: + led.off() + print("Stopped") +`, + }, + }, + ], + components: [ + { type: 'wokwi-pir-motion-sensor', id: 'pi5-pir', x: 540, y: 120, properties: {} }, + { type: 'wokwi-led', id: 'pi5-led', x: 560, y: 320, properties: { color: 'red' } }, + ], + wires: [ + { id: 'w-vcc', start: { componentId: 'raspberry-pi-5', pinName: '5V' }, end: { componentId: 'pi5-pir', pinName: 'VCC' }, color: '#e74c3c' }, + { id: 'w-pir-gnd', start: { componentId: 'pi5-pir', pinName: 'GND' }, end: { componentId: 'raspberry-pi-5', pinName: 'GND' }, color: '#000000' }, + { id: 'w-out', start: { componentId: 'pi5-pir', pinName: 'OUT' }, end: { componentId: 'raspberry-pi-5', pinName: 'GPIO4' }, color: '#f1c40f' }, + { id: 'w-led', start: { componentId: 'raspberry-pi-5', pinName: 'GPIO17' }, end: { componentId: 'pi5-led', pinName: 'A' }, color: '#e74c3c' }, + { id: 'w-led-gnd', start: { componentId: 'pi5-led', pinName: 'C' }, end: { componentId: 'raspberry-pi-5', pinName: 'GND' }, color: '#000000' }, + ], + }, + { + id: 'pi5-traffic-light', + title: '[Pi 5] Traffic Light', + description: + 'Raspberry Pi 5 runs a red/yellow/green traffic-light state machine on GPIO17/27/22 with gpiozero. Start the Pi, Upload, then run: python3 /home/pi/script.py', + category: 'basics', + difficulty: 'intermediate', + code: '', + boards: [ + { + boardKind: 'raspberry-pi-5', + x: 80, + y: 80, + vfsFiles: { + 'script.py': `#!/usr/bin/env python3 +# Raspberry Pi 5 - Traffic light state machine +# Wiring: red->GPIO17, yellow->GPIO27, green->GPIO22 (each LED- -> GND). +from gpiozero import LED +from time import sleep + +red = LED(17) +yellow = LED(27) +green = LED(22) +print("Traffic light running - Ctrl-C to stop") +try: + while True: + red.on(); yellow.off(); green.off() + print("RED") + sleep(3) + red.off(); green.on() + print("GREEN") + sleep(3) + green.off(); yellow.on() + print("YELLOW") + sleep(1) + yellow.off() +except KeyboardInterrupt: + red.off(); yellow.off(); green.off() + print("Stopped") +`, + }, + }, + ], + components: [ + { type: 'wokwi-led', id: 'pi5-red', x: 540, y: 80, properties: { color: 'red' } }, + { type: 'wokwi-led', id: 'pi5-yellow', x: 540, y: 200, properties: { color: 'yellow' } }, + { type: 'wokwi-led', id: 'pi5-green', x: 540, y: 320, properties: { color: 'green' } }, + ], + wires: [ + { id: 'w-r', start: { componentId: 'raspberry-pi-5', pinName: 'GPIO17' }, end: { componentId: 'pi5-red', pinName: 'A' }, color: '#e74c3c' }, + { id: 'w-y', start: { componentId: 'raspberry-pi-5', pinName: 'GPIO27' }, end: { componentId: 'pi5-yellow', pinName: 'A' }, color: '#f1c40f' }, + { id: 'w-g', start: { componentId: 'raspberry-pi-5', pinName: 'GPIO22' }, end: { componentId: 'pi5-green', pinName: 'A' }, color: '#2ecc71' }, + { id: 'g-r', start: { componentId: 'pi5-red', pinName: 'C' }, end: { componentId: 'raspberry-pi-5', pinName: 'GND' }, color: '#000000' }, + { id: 'g-y', start: { componentId: 'pi5-yellow', pinName: 'C' }, end: { componentId: 'raspberry-pi-5', pinName: 'GND' }, color: '#000000' }, + { id: 'g-g', start: { componentId: 'pi5-green', pinName: 'C' }, end: { componentId: 'raspberry-pi-5', pinName: 'GND' }, color: '#000000' }, + ], + }, ]; // Merge legacy examples with circuit-focused examples (analog, digital gates,