De puzzel heeft een rooster van lichtgevende knoppen en elke knop kan ingedrukt worden om hem aan/uit te zetten. De puzzel is opgelost wanneer de speler alle vakjes op het rooster laat oplichten.
De moeilijkheid zit hem in het feit dat elke knop niet enkel zichzelf aan/uit zet, maar tegelijk verschillende andere. De speler moet dus een strategie vinden om alle lichten aan te krijgen.
Bij correcte oplossing wordt het volgende cijfer van de ontmantelingscode zichtbaar.
Software
Deze puzzel is een versie van het bekende Lights Out game. Deze code is gebaseerd op een Typescript implementatie van Raymond Tana. Deze versie kan je hier online spelen.
Voor dit project vertaalde ik dus deze Typescript code naar Rust. Daarvoor vertaalde ik het project eerst naar een GUI versie gebruik makende van het Rust egui framework (simulator genaamd in de code). Daarna heb ik de game logic geïsoleerd (game in de code) zodat die herbruikbaar is voor het hardware project (firmware in de code).
Voor een eerste prototype gebruikte ik een ESP32 S3 Plus, maar ik wisselde uiteindelijk toch naar de Raspberry Pi Pico 2 Wireless omdat de ESP32 maximaal een 3x3 rooster toeliet, waardoor die versie te makkelijk oplosbaar was.
Bij het opstarten wordt de puzzel gegenereerd door te starten van een volledig verlicht rooster, en dan een aantal random mutaties te doen. Dit aantal is gedefinieerd in game/src/config.rs als PUZZLE_DIFFICULTY. Hoe hoger deze constante, hoe moeilijker de puzzel. De oplossing is dan eigenlijk die gebruikte sequentie mutaties in omgekeerde volgorde (maar kan uiteraard korter als een groot aantal wordt gebruikt).
Project structure
Het project maakt gebruik van een workspace met 3 aparte “crates” (Rust libraries):
| Crate | Doel |
|---|---|
game | Gedeelde game logica (no_std compatibel, d.w.z. de Rust standard library wordt niet gebruikt voor embbeded use) |
firmware | Bare-metal firmware voor de Raspberry Pi Pico 2 Wireless |
simulator | Desktop GUI (egui) om het spel te spelen zonder hardware |
Om de GUI versie lokaal te runnen, compile en run je dus vanuit het simulator project:
cd simulator
cargo run --release
Hardware (Raspberry Pi Pico 2 Wireless)
Flashing setup
1. Installeer Rust
Er wordt gebruik gemaakt van de standaard stable Rust toolchain. Als Rust nog niet geinstalleerd is, kan je makkelijk installeren via Rustup
2. Pico target toevoegen
Voeg het correct Rust target toe voor de Raspberry Pi Pico’s ARM Cortex-M33 architectuur:
rustup target add thumbv8m.main-none-eabihf
Dit is dus niet project-specifiek, maar installeert het target in de globale Rustup home directory (C:\Users\YourName\.rustup\).
3. picotool installeren
Download het pre-built Windows binary van
pico-sdk-tools releases. Gebruik picotool-<version>-a4-x64-win.zip, unzip het, en voeg de picotool/
folder toe aan je PATH.
4. Compileer en flash
Houd de BOOTSEL knop (de kleine knop naast de USB poort op de Pico) terwijl je de USB kabel aansluit, en laat maar los als hij volledig aangesloten is. Dan zal de Pico verschijnen als een USB apparaat in file explorer.
Als de Pico correct verbonden is (je kan hem zien in file explorer) kan je compileren vanuit het firmware project:
cd firmware
cargo run --release
picotool zal dan de resulterende binary naar de Pico uploaden en hem automatisch rebooten in die firmware. Als dit correct gebeurd is, verdwijnt de Pico als USB apparaat uit de file explorer. De BOOTSEL knop stap moet dus herhaald worden alvorens opnieuw te flashen.
Componenten
- Raspberry Pi Pico 2 Wireless
- Dubbelzijdig gaatjesbord
- Lithium Ion polymer battery 3,7V 500mAh - Met JST-PHR-2
- Adafruit Micro USB charger for Lithium Ion / Lithium Polymer batteries
- Schuifschakelaar SPDT ON-ON 0,5A - 12V
- 16x Tuozhan LED SMD 0805 - Groen
- 16 Akko Crystal switches + dark transparent keycaps
- 150 Ω SMD weerstanden, één per LED, dus 16 totaal
Bedrading
Het concept van de bedrading is gebruik te maken van gedeelde rijen voor de LEDs en switches. Elke cel in het 4×4 grid heeft dus één LED én één switch die beide dezelfde rijdraad delen:
Cel (rij, kolom):
LED (+) ──┐
├── Rijdraad (gedeeld)
Switch A ──┘
LED (-) ──── LED-kolomdraad
Switch B ──── Switch-kolomdraad
De firmware wisselt snel tussen twee fases per rij:
- LED-fase (1800 µs): rij = HIGH → LEDs in die rij kunnen oplichten
- Switch-fase (200 µs): rij = LOW → Switches in die rij worden gescand
De switch stuurt de LED dus niet rechtstreeks aan — dat doet de firmware. Ze zijn alleen elektrisch verbonden via de rijdraad, maar werken in aparte tijdsfasen:
Fase 1 - LED (1800µs):
- Rij = HIGH, LED-kolom = LOW → LED brandt (als de spelstatus dat zegt)
- De switch doet niets in deze fase
Fase 2 - Switch (200µs):
- Rij = LOW, LED-kolom = HIGH → alle LEDs uit
- Firmware leest BTN_COL: LOW = Switch ingedrukt (want SW_A trekt BTN_COL naar GND via de rij)
- De LED doet niets in deze fase
De switch en LED wisselen elkaar dus af in de tijd, zo snel (~2ms per rij) dat je oog het niet ziet. De rijdraad is gewoon een gedeelde geleider - de volgorde van de componenten maakt niet uit, want ze staan alle drie (GP0, LED+, SW_A) altijd op hetzelfde spanning.
Pin-overzicht
| Signaal | GP-pin | Fysieke pin | Richting |
|---|---|---|---|
| Rij 0 | GP0 | Pin 1 | Output |
| Rij 1 | GP1 | Pin 2 | Output |
| Rij 2 | GP2 | Pin 4 | Output |
| Rij 3 | GP3 | Pin 5 | Output |
| LED kolom 0 | GP4 | Pin 6 | Output |
| LED kolom 1 | GP5 | Pin 7 | Output |
| LED kolom 2 | GP6 | Pin 9 | Output |
| LED kolom 3 | GP7 | Pin 10 | Output |
| Switch kolom 0 | GP8 | Pin 11 | Input (pull-up) |
| Switch kolom 1 | GP9 | Pin 12 | Input (pull-up) |
| Switch kolom 2 | GP10 | Pin 14 | Input (pull-up) |
| Switch kolom 3 | GP11 | Pin 15 | Input (pull-up) |
| Solved signaal | GP12 | Pin 16 | Output |
| Solved GND | — | Pin 18 | GND |
Voeding: de Pico wordt gevoed via USB ofwel via batterij met pin 39 (VSYS) en pin 38 (GND).
Solved signaal: GP12 gaat HIGH zodra de puzzel opgelost is. Dit is uiteindelijk vervangen door draadloos door te sturen naar de Heltec Wifi Kit 8.
LED-matrix (rijen × LED-kolommen)
GP0/GP1/GP2/GP3 zijn rijdraden — horizontale draden die in beide matrices voorkomen.
GP4 GP5 GP6 GP7
│ │ │ │
GP0 ──[LED(0,0)]──[LED(0,1)]──[LED(0,2)]──[LED(0,3)]
GP1 ──[LED(1,0)]──[LED(1,1)]──[LED(1,2)]──[LED(1,3)]
GP2 ──[LED(2,0)]──[LED(2,1)]──[LED(2,2)]──[LED(2,3)]
GP3 ──[LED(3,0)]──[LED(3,1)]──[LED(3,2)]──[LED(3,3)]
LED-polariteit:
+anode → rijdraad (GP0–GP3)−kathode → LED-kolomdraad via 150Ω weerstand (GP4–GP7)
De LED kolommen hierboven bestaan dus eigenlijk uit 4 parallelle takken van telkens een 150Ω weerstand in serie met een LED. Voorbeeld voor kolom 0 (GP5–GP7 zijn identiek):
GP4
│
├── 150Ω ──[LED(0,0)]── GP0
│ (−) (+)
├── 150Ω ──[LED(1,0)]── GP1
│ (−) (+)
├── 150Ω ──[LED(2,0)]── GP2
│ (−) (+)
└── 150Ω ──[LED(3,0)]── GP3
(−) (+)
Stroomrichting bij LEDs aan:
GP0 (HIGH) → LED(+) → LED(−) → 150Ω → GP4/5/6/7 (LOW)
Dus per LED:
- Eén uiteinde van de 150Ω → naar de kolomdraad (GP4)
- Ander uiteinde van de 150Ω → naar de kathode (-) van die LED
- De kathode zit aan één uiteinde van de weerstand, en de kathodes van opeenvolgende LEDs zijn niet direct met elkaar verbonden
- Elke weerstand verbindt precies één LED-kathode met de bijhorende kolomdraad.
Switch-matrix (dezelfde rijen × Switch-kolommen)
GP0/GP1/GP2/GP3 zijn rijdraden — horizontale draden die in beide matrices voorkomen.
GP8 GP9 GP10 GP11
│ │ │ │
GP0 ──[SW(0,0)] ──[SW(0,1)] ──[SW(0,2)] ──[SW(0,3)]
GP1 ──[SW(1,0)] ──[SW(1,1)] ──[SW(1,2)] ──[SW(1,3)]
GP2 ──[SW(2,0)] ──[SW(2,1)] ──[SW(2,2)] ──[SW(2,3)]
GP3 ──[SW(3,0)] ──[SW(3,1)] ──[SW(3,2)] ──[SW(3,3)]
GP8–GP11 zijn Switch-kolommen — los van de LED-kolommen.
In de praktijk loopt elke rijdraad dus langs zowel de LEDs (+ pool) als de Switches (A pool) in die rij:
GP0───[SW(0,0)]──+[LED(0,0)]+───[SW(0,1)]──+[LED(0,1)]+───[SW(0,2)]──+[LED(0,2)]+───[SW(0,3)]──+[LED(0,3)]
GP1───[SW(1,0)]──+[LED(1,0)]+───[SW(1,1)]──+[LED(1,1)]+───[SW(1,2)]──+[LED(1,2)]+───[SW(1,3)]──+[LED(1,3)]
GP2───[SW(2,0)]──+[LED(2,0)]+───[SW(2,1)]──+[LED(2,1)]+───[SW(2,2)]──+[LED(2,2)]+───[SW(2,3)]──+[LED(2,3)]
GP3───[SW(3,0)]──+[LED(3,0)]+───[SW(3,1)]──+[LED(3,1)]+───[SW(3,2)]──+[LED(3,2)]+───[SW(3,3)]──+[LED(3,3)]