feat(examples): add single-board Raspberry Pi 3/4/5 GPIO examples
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 <noreply@anthropic.com>
This commit is contained in:
parent
daa46c6724
commit
909c94c5fb
|
|
@ -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<string>([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');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue