Autoflasher added

This commit is contained in:
superminaren 2026-05-08 10:20:30 +02:00
parent 1d192c0b7b
commit c9100ea957
9 changed files with 2140 additions and 0 deletions

View File

@ -0,0 +1,2 @@
# Trigger flash when an RP2040 in BOOTSEL mode appears as a block device
SUBSYSTEM=="block", ACTION=="add", ENV{ID_VENDOR_ID}=="2e8a", ENV{ID_MODEL_ID}=="0003", ENV{DEVTYPE}=="disk", TAG+="systemd", ENV{SYSTEMD_WANTS}+="rp2040-flash@%k.service"

175
autoflasher/README.md Normal file
View File

@ -0,0 +1,175 @@
# rp2040-autoflash
Automatically flash any number of RP2040 boards the moment they're plugged in
holding BOOTSEL. Drop a `.uf2` into a folder, connect a board, and it gets
programmed — no host software, no button-mashing, no per-board commands.
Multiple boards connected at the same time are flashed in parallel, each
isolated from the others.
## How it works
1. A **udev rule** matches the RP2040's BOOTSEL USB IDs (`2e8a:0003`) when it
appears as a block device.
2. udev hands the event off to a **systemd template service**
(`rp2040-flash@sdX.service`) so the work runs asynchronously and udev
doesn't time out. Each connected board gets its own service instance.
3. The service runs a **bash script** that mounts the BOOTSEL drive, copies
the UF2 from the firmware directory, and cleans up. The RP2040 reboots
itself the moment the UF2 finishes writing.
## Requirements
- Linux with systemd and udev (Ubuntu, Debian, Fedora, Arch, Raspberry Pi OS, etc.)
- `mount`, `vfat` filesystem support (standard on every desktop distro)
- root access for installation
## Files
| File | Destination |
|---|---|
| `99-rp2040-flash.rules` | `/etc/udev/rules.d/` |
| `rp2040-flash@.service` | `/etc/systemd/system/` |
| `flash-rp2040.sh` | `/usr/local/bin/` |
## Installation
```bash
sudo install -m 755 flash-rp2040.sh /usr/local/bin/flash-rp2040.sh
sudo install -m 644 99-rp2040-flash.rules /etc/udev/rules.d/
sudo install -m 644 rp2040-flash@.service /etc/systemd/system/
sudo mkdir -p /opt/rp2040-firmware
sudo systemctl daemon-reload
sudo udevadm control --reload-rules
sudo udevadm trigger
```
## Usage
1. Place a UF2 file in `/opt/rp2040-firmware/`.
- If a file named `firmware.uf2` exists, it is used.
- Otherwise, the most recently modified `*.uf2` in the directory is used.
2. Connect an RP2040 while holding BOOTSEL (or with `BOOTSEL` already engaged).
3. The board flashes and reboots automatically — typically within a second
or two of being detected.
4. Repeat with as many boards as your USB ports/hubs can supply. They flash
independently and in parallel.
### Updating the firmware
Just replace the file:
```bash
sudo cp new-build.uf2 /opt/rp2040-firmware/firmware.uf2
```
No service restart or udev reload is needed — the script reads the directory
on every flash.
## Configuration
### Firmware directory
Edit `rp2040-flash@.service` and change:
```ini
Environment=RP2040_FIRMWARE_DIR=/opt/rp2040-firmware
```
Then reload:
```bash
sudo systemctl daemon-reload
```
### Supporting RP2350
The default rule matches `ID_MODEL_ID==0003` (RP2040). To also match RP2350,
edit `99-rp2040-flash.rules` and replace the model match with:
```
ENV{ID_MODEL_ID}=="0003|000f"
```
…or add a second rule line for `000f`.
### Avoiding desktop auto-mount races
If you run a desktop environment (GNOME, KDE, etc.) that auto-mounts
removable storage, it may grab the BOOTSEL drive before the script does.
Add this to the udev rule to suppress that:
```
ENV{UDISKS_IGNORE}="1"
```
## Monitoring
Live tail of all flash activity:
```bash
journalctl -t 'rp2040-flash*' -f
```
Or the dedicated log file:
```bash
tail -f /var/log/rp2040-flash.log
```
Status of a specific in-flight flash (replace `sdb`):
```bash
systemctl status 'rp2040-flash@sdb.service'
```
## Troubleshooting
**Nothing happens when I plug in a board.**
Check that the board enumerates as `2e8a:0003`:
```bash
lsusb | grep 2e8a
```
If the VID/PID is correct but no service starts, verify the rule loaded:
```bash
udevadm monitor --udev --subsystem-match=block
```
then plug the board in. You should see an `add` event and a
`SYSTEMD_WANTS=rp2040-flash@...` property.
**The service starts but flashing fails.**
Look at the log:
```bash
tail -n 50 /var/log/rp2040-flash.log
```
Common causes: the firmware directory is empty, the `.uf2` is corrupt, or
the desktop auto-mounter grabbed the drive first (see configuration above).
**`cp` / `sync` errors in the log.**
These are usually harmless. The RP2040 disconnects from USB the instant the
UF2 has been written, so the kernel sees an unclean unmount even though the
flash succeeded. The "Flash sequence finished" line is what matters.
**Board flashes but doesn't run my code.**
That's a firmware problem, not a flashing problem — the same UF2 dropped
manually onto the BOOTSEL drive will fail in the same way. Verify the build.
## Uninstallation
```bash
sudo rm /etc/udev/rules.d/99-rp2040-flash.rules
sudo rm /etc/systemd/system/rp2040-flash@.service
sudo rm /usr/local/bin/flash-rp2040.sh
sudo udevadm control --reload-rules
sudo systemctl daemon-reload
```

View File

@ -0,0 +1,67 @@
#!/bin/bash
# Flash an RP2040 in BOOTSEL mode with a .uf2 from $RP2040_FIRMWARE_DIR.
# Picks firmware.uf2 if present, otherwise the most recently modified .uf2 in the dir.
set -uo pipefail
DEVICE="${1:?Usage: $0 <block-device>}"
FIRMWARE_DIR="${RP2040_FIRMWARE_DIR:-/opt/rp2040-firmware}"
MOUNT_POINT="/run/rp2040-flash/$(basename "$DEVICE")"
LOG_TAG="rp2040-flash[$(basename "$DEVICE")]"
log() {
logger -t "$LOG_TAG" "$*"
echo "$(date '+%F %T') $LOG_TAG $*" >> /var/log/rp2040-flash.log
}
cleanup() {
if mountpoint -q "$MOUNT_POINT" 2>/dev/null; then
umount "$MOUNT_POINT" 2>/dev/null || true
fi
rmdir "$MOUNT_POINT" 2>/dev/null || true
}
trap cleanup EXIT
log "Triggered for $DEVICE"
# Pick a UF2: prefer firmware.uf2, else newest *.uf2 in the directory
if [[ -f "$FIRMWARE_DIR/firmware.uf2" ]]; then
UF2_FILE="$FIRMWARE_DIR/firmware.uf2"
else
UF2_FILE=$(find "$FIRMWARE_DIR" -maxdepth 1 -type f -name '*.uf2' \
-printf '%T@ %p\n' 2>/dev/null | sort -rn | head -n1 | cut -d' ' -f2-)
fi
if [[ -z "${UF2_FILE:-}" || ! -f "$UF2_FILE" ]]; then
log "ERROR: no .uf2 file in $FIRMWARE_DIR"
exit 1
fi
log "Using firmware: $UF2_FILE"
# Wait briefly for the block device to settle
for _ in {1..15}; do
[[ -b "$DEVICE" ]] && break
sleep 0.2
done
[[ -b "$DEVICE" ]] || { log "ERROR: $DEVICE not present"; exit 1; }
# Mount
mkdir -p "$MOUNT_POINT"
if ! mount -t vfat -o rw,sync,noatime,flush "$DEVICE" "$MOUNT_POINT"; then
log "ERROR: failed to mount $DEVICE"
exit 1
fi
# Sanity-check it's really a Pico in BOOTSEL
if [[ ! -f "$MOUNT_POINT/INFO_UF2.TXT" ]]; then
log "ERROR: $DEVICE is not an RP2 boot drive (no INFO_UF2.TXT)"
exit 1
fi
# Copy UF2; the device reboots itself once the write completes
log "Writing firmware..."
cp "$UF2_FILE" "$MOUNT_POINT/" 2>/dev/null
sync 2>/dev/null || true
# cp/sync may report errors because the device disconnects mid-write — that's normal.
log "Flash sequence finished for $DEVICE"

9
autoflasher/install.sh Normal file
View File

@ -0,0 +1,9 @@
sudo install -m 755 flash-rp2040.sh /usr/local/bin/flash-rp2040.sh
sudo install -m 644 99-rp2040-flash.rules /etc/udev/rules.d/
sudo install -m 644 rp2040-flash@.service /etc/systemd/system/
sudo mkdir -p /opt/rp2040-firmware
sudo cp your-build.uf2 /opt/rp2040-firmware/firmware.uf2
sudo systemctl daemon-reload
sudo udevadm control --reload-rules
sudo udevadm trigger

View File

@ -0,0 +1,11 @@
[Unit]
Description=Flash RP2040 on /dev/%i
After=dev-%i.device
BindsTo=dev-%i.device
[Service]
Type=oneshot
ExecStart=/usr/local/bin/flash-rp2040.sh /dev/%i
TimeoutStartSec=30
# Optional: point to a different firmware folder
Environment=RP2040_FIRMWARE_DIR=/opt/rp2040-firmware

3
autoflasher/tail.sh Normal file
View File

@ -0,0 +1,3 @@
journalctl -t 'rp2040-flash*' -f
# or
tail -f /var/log/rp2040-flash.log

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
[env:rp2040]
platform = raspberrypi
board = rpipico
framework = arduino
monitor_speed = 115200
upload_speed = 115200
build_flags =
-D PICO_SCAN_I2C_FOR_WIRE
-I src

View File

@ -0,0 +1,830 @@
#include <Arduino.h>
#include <Wire.h>
#define BUTTON_A 9
#define BUTTON_B 10
#define BUTTON_UP 11
#define BUTTON_DOWN 12
#define BUTTON_LEFT 13
#define BUTTON_RIGHT 14
#define FRONT_LED_1 23
#define FRONT_LED_2 24
#define FRONT_LED_3 25
#define FRONT_LED_4 26
#define FLASHLIGHT_LED 15
#define SAO_GPIO0 0
#define SAO_GPIO1 1
#define LED_MATRIX_SDA 2
#define LED_MATRIX_SCL 3
#define LED_MATRIX_ADDR 0x74
#define MATRIX_COLS 9
#define MATRIX_ROWS 9
#define IDLE_TIMEOUT 5000
const char* RANDOM_MESSAGES[] = {
"Hack the planet!",
"0x00 0x01 0x10",
"Securityfest 2026!",
"RP2040 rocks!",
"Buy the dip!",
"Nice badge!",
"#!.!/#!.!",
"0xDEAD 0xBEEF",
"Pwned!",
"1337 h4x0r",
"Kernel panic!",
"sudo make me",
"CTF{D3bug}",
"0-day here",
"Badgelife!"
};
const int NUM_MESSAGES = sizeof(RANDOM_MESSAGES) / sizeof(RANDOM_MESSAGES[0]);
uint8_t matrixBuffer[MATRIX_ROWS][MATRIX_COLS];
unsigned long lastActivity = 0;
int currentMenuItem = 0;
enum { MODE_MENU, MODE_TETRIS, MODE_SNAKE, MODE_BRICK, MODE_PONG, MODE_TVBGONE, MODE_IDLE } currentMode = MODE_MENU;
bool inSubmenu = false;
void ledMatrixBegin() {
Wire.setSDA(LED_MATRIX_SDA);
Wire.setSCL(LED_MATRIX_SCL);
Wire.begin();
delay(10);
Wire.beginTransmission(LED_MATRIX_ADDR);
Wire.write(0x00);
Wire.write(0x01);
Wire.endTransmission();
}
void ledMatrixClear() {
memset(matrixBuffer, 0, sizeof(matrixBuffer));
for (int page = 0; page < 2; page++) {
Wire.beginTransmission(LED_MATRIX_ADDR);
Wire.write(0x00);
Wire.write(page);
Wire.endTransmission();
Wire.requestFrom(LED_MATRIX_ADDR, MATRIX_COLS);
for (int i = 0; i < MATRIX_COLS; i++) {
Wire.read();
}
for (int c = 0; c < MATRIX_COLS; c++) {
Wire.beginTransmission(LED_MATRIX_ADDR);
Wire.write(0x01 + c);
Wire.write(0x00);
Wire.endTransmission();
}
}
}
void ledMatrixSetPixel(int x, int y, uint8_t brightness) {
if (x >= 0 && x < MATRIX_COLS && y >= 0 && y < MATRIX_ROWS) {
matrixBuffer[y][x] = brightness;
Wire.beginTransmission(LED_MATRIX_ADDR);
Wire.write(0x01 + x);
Wire.write(brightness);
Wire.endTransmission();
}
}
void ledMatrixDrawChar(int x, int y, char c, uint8_t brightness) {
const uint8_t font[16][5] = {
{0x00,0x00,0x00,0x00,0x00},
{0x00,0x00,0x5F,0x00,0x00},
{0x00,0x07,0x00,0x07,0x00},
{0x14,0x7F,0x14,0x7F,0x14},
{0x24,0x2A,0x7F,0x2A,0x12},
{0x46,0x26,0x10,0x26,0x46},
{0x08,0x3E,0x28,0x3E,0x08},
{0x00,0x00,0x07,0x00,0x00},
{0x3E,0x3E,0x3E,0x3E,0x3E},
{0x00,0x07,0x07,0x07,0x00},
{0x1C,0x1C,0x1C,0x1C,0x1C},
{0x1C,0x1C,0x1C,0x10,0x00},
{0x00,0x04,0x3E,0x04,0x00},
{0x08,0x1C,0x3E,0x1C,0x08},
{0x1C,0x3E,0x3E,0x3E,0x08},
{0x1C,0x2A,0x2A,0x2A,0x08}
};
int idx = 0;
if (c >= 'A' && c <= 'Z') idx = c - 'A' + 10;
else if (c >= '0' && c <= '9') idx = c - '0';
else if (c == '!') idx = 1;
else if (c == '.') idx = 15;
else if (c == '(') idx = 13;
else if (c == ')') idx = 13;
else if (c == '{') idx = 0;
else if (c == '}') idx = 0;
else return;
for (int col = 0; col < 5; col++) {
uint8_t colData = font[idx][col];
for (int row = 0; row < 8; row++) {
if (colData & (1 << row)) {
ledMatrixSetPixel(x + col, y + row, brightness);
}
}
}
}
void drawText(const char* text, int x, int y, uint8_t brightness) {
int cursorX = x;
while (*text) {
ledMatrixDrawChar(cursorX, y, *text, brightness);
cursorX += 6;
if (cursorX > MATRIX_COLS) break;
text++;
}
}
void drawTextCentered(const char* text, int y, uint8_t brightness) {
int len = 0;
const char* p = text;
while (*p) { len++; p++; }
int x = (MATRIX_COLS - len * 6) / 2;
if (x < 0) x = 0;
drawText(text, x, y, brightness);
}
void setupButtons() {
pinMode(BUTTON_A, INPUT_PULLUP);
pinMode(BUTTON_B, INPUT_PULLUP);
pinMode(BUTTON_UP, INPUT_PULLUP);
pinMode(BUTTON_DOWN, INPUT_PULLUP);
pinMode(BUTTON_LEFT, INPUT_PULLUP);
pinMode(BUTTON_RIGHT, INPUT_PULLUP);
}
void setupFrontLEDs() {
pinMode(FRONT_LED_1, OUTPUT);
pinMode(FRONT_LED_2, OUTPUT);
pinMode(FRONT_LED_3, OUTPUT);
pinMode(FRONT_LED_4, OUTPUT);
}
void setupFlashlight() {
pinMode(FLASHLIGHT_LED, OUTPUT);
digitalWrite(FLASHLIGHT_LED, LOW);
}
void setupSAO() {
pinMode(SAO_GPIO0, OUTPUT);
pinMode(SAO_GPIO1, OUTPUT);
digitalWrite(SAO_GPIO0, LOW);
digitalWrite(SAO_GPIO1, LOW);
}
bool buttonPressed(int btn) {
return digitalRead(btn) == LOW;
}
bool anyButtonPressed() {
return buttonPressed(BUTTON_A) || buttonPressed(BUTTON_B) ||
buttonPressed(BUTTON_UP) || buttonPressed(BUTTON_DOWN) ||
buttonPressed(BUTTON_LEFT) || buttonPressed(BUTTON_RIGHT);
}
void waitForButtonRelease() {
while (anyButtonPressed()) delay(10);
delay(50);
}
void recordActivity() {
lastActivity = millis();
}
void knightRiderSweep(unsigned long interval) {
static int pos = 0;
static bool dir = true;
static unsigned long lastUpdate = 0;
static bool leds[4] = {0, 0, 0, 0};
if (millis() - lastUpdate > interval) {
lastUpdate = millis();
memset(leds, 0, sizeof(leds));
if (dir) {
pos++;
if (pos >= 4) { pos = 3; dir = false; }
} else {
pos--;
if (pos < 0) { pos = 1; dir = true; }
}
if (pos >= 0 && pos < 4) leds[pos] = true;
}
digitalWrite(FRONT_LED_1, leds[0]);
digitalWrite(FRONT_LED_2, leds[1]);
digitalWrite(FRONT_LED_3, leds[2]);
digitalWrite(FRONT_LED_4, leds[3]);
}
unsigned long saoToggleTime = 0;
bool saoGPIO0State = false;
unsigned long saoPWMTime = 0;
bool saoPWMDirection = true;
float saoPWMValue = 0;
void updateSAO(unsigned long now) {
if (now - saoToggleTime >= 1000) {
saoToggleTime = now;
saoGPIO0State = !saoGPIO0State;
digitalWrite(SAO_GPIO0, saoGPIO0State);
}
if (now - saoPWMTime >= 30) {
saoPWMTime = now;
if (saoPWMDirection) {
saoPWMValue += 1.67f;
if (saoPWMValue >= 100) { saoPWMValue = 100; saoPWMDirection = false; }
} else {
saoPWMValue -= 1.67f;
if (saoPWMValue <= 0) { saoPWMValue = 0; saoPWMDirection = true; }
}
analogWrite(SAO_GPIO1, (int)(saoPWMValue * 255.0f / 100.0f));
}
}
const int TVBGONE_CODES[][2] = {
{9000, 4500},
{4500, 4500}
};
int tvbgoneStep = 0;
unsigned long tvbgoneLastToggle = 0;
bool tvbgoneOn = false;
void updateTVBGone(unsigned long now) {
if (currentMode != MODE_TVBGONE) return;
if (now - tvbgoneLastToggle > 20) {
tvbgoneLastToggle = now;
tvbgoneOn = !tvbgoneOn;
digitalWrite(FLASHLIGHT_LED, tvbgoneOn ? HIGH : LOW);
}
}
void showMenu() {
ledMatrixClear();
const char* items[] = {"Tetris", "Snake", "Brick", "Pong", "TV-B-Gone"};
for (int i = 0; i < 5; i++) {
if (i == currentMenuItem) {
ledMatrixSetPixel(0, 1 + i * 2, 255);
ledMatrixSetPixel(1, 1 + i * 2, 150);
}
ledMatrixSetPixel(2, 1 + i * 2, (i == currentMenuItem) ? 255 : 80);
ledMatrixSetPixel(3, 1 + i * 2, 80);
}
char buf[16];
buf[0] = 'A' + currentMenuItem;
buf[1] = '\0';
drawText(buf, 3, 1 + currentMenuItem * 2, 255);
}
void handleMenuInput() {
if (buttonPressed(BUTTON_UP)) {
recordActivity();
if (currentMenuItem > 0) currentMenuItem--;
waitForButtonRelease();
showMenu();
} else if (buttonPressed(BUTTON_DOWN)) {
recordActivity();
if (currentMenuItem < 4) currentMenuItem++;
waitForButtonRelease();
showMenu();
} else if (buttonPressed(BUTTON_A)) {
recordActivity();
waitForButtonRelease();
switch (currentMenuItem) {
case 0: currentMode = MODE_TETRIS; break;
case 1: currentMode = MODE_SNAKE; break;
case 2: currentMode = MODE_BRICK; break;
case 3: currentMode = MODE_PONG; break;
case 4: currentMode = MODE_TVBGONE; break;
}
ledMatrixClear();
}
}
class TetrisGame {
public:
static const int W = 7;
static const int H = 15;
static const int BLOCKS[7][4];
uint8_t field[H][W];
int pieceX, pieceY, pieceType, pieceRot;
unsigned long lastFall;
int score;
bool gameOver;
TetrisGame() { reset(); }
void reset() {
memset(field, 0, sizeof(field));
score = 0;
gameOver = false;
newPiece();
}
void newPiece() {
pieceType = random(7);
pieceX = W / 2 - 2;
pieceY = 0;
pieceRot = 0;
}
bool canPlace(int px, int py, int type, int rot) {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (getBlock(type, rot, i, j) && (px + j < 0 || px + j >= W || py + i >= H || field[py + i][px + j])) {
return false;
}
}
}
return true;
}
bool getBlock(int type, int rot, int x, int y) {
static const uint8_t pieces[7][4][4] = {
{{0,1,0,0},{0,1,0,0},{0,1,0,0},{0,1,0,0}},
{{0,1,1,0},{0,1,0,0},{0,1,1,0},{0,0,0,0}},
{{0,1,1,0},{0,0,1,0},{0,1,1,0},{0,0,0,0}},
{{0,0,1,0},{0,0,1,0},{0,1,1,0},{0,0,0,0}},
{{0,1,0,0},{0,1,0,0},{0,1,1,0},{0,0,0,0}},
{{0,1,0,0},{0,1,0,0},{0,1,0,0},{0,1,0,0}},
{{0,0,0,0},{0,1,1,0},{0,1,1,0},{0,0,0,0}}
};
int r = rot % 4;
if (type == 0) {
const uint8_t I[4][4] = {{1,1,1,1},{0,0,0,0},{0,0,0,0},{0,0,0,0}};
if (r == 0 && y < 4 && x < 4) return I[y][x];
}
if (y < 4 && x < 4) return pieces[type][r][y * 4 + x];
return 0;
}
void lockPiece() {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (getBlock(pieceType, pieceRot, j, i)) {
int fx = pieceX + j;
int fy = pieceY + i;
if (fy >= 0 && fy < H && fx >= 0 && fx < W) {
field[fy][fx] = (pieceType + 1) * 30;
}
}
}
}
clearLines();
newPiece();
}
void clearLines() {
for (int y = H - 1; y >= 0; y--) {
bool full = true;
for (int x = 0; x < W; x++) {
if (!field[y][x]) { full = false; break; }
}
if (full) {
score += 100;
for (int yy = y; yy > 0; yy--) {
for (int x = 0; x < W; x++) {
field[yy][x] = field[yy - 1][x];
}
}
for (int x = 0; x < W; x++) field[0][x] = 0;
}
}
}
void render() {
ledMatrixClear();
for (int y = 0; y < H && y < MATRIX_ROWS; y++) {
for (int x = 0; x < W && x < MATRIX_COLS; x++) {
if (field[y][x]) {
ledMatrixSetPixel(x, y, field[y][x]);
}
}
}
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (getBlock(pieceType, pieceRot, j, i)) {
int px = pieceX + j;
int py = pieceY + i;
if (py >= 0 && py < MATRIX_ROWS && px >= 0 && px < MATRIX_COLS) {
ledMatrixSetPixel(px, py, 255);
}
}
}
}
}
void update() {
if (gameOver) return;
unsigned long now = millis();
if (now - lastFall > 500) {
lastFall = now;
if (canPlace(pieceX, pieceY + 1, pieceType, pieceRot)) {
pieceY++;
} else {
lockPiece();
if (!canPlace(pieceX, pieceY, pieceType, pieceRot)) {
gameOver = true;
}
}
}
}
void input() {
if (buttonPressed(BUTTON_LEFT)) {
recordActivity();
if (canPlace(pieceX - 1, pieceY, pieceType, pieceRot)) pieceX--;
waitForButtonRelease();
} else if (buttonPressed(BUTTON_RIGHT)) {
recordActivity();
if (canPlace(pieceX + 1, pieceY, pieceType, pieceRot)) pieceX++;
waitForButtonRelease();
} else if (buttonPressed(BUTTON_UP)) {
recordActivity();
if (canPlace(pieceX, pieceY, pieceType, (pieceRot + 1) % 4)) pieceRot = (pieceRot + 1) % 4;
waitForButtonRelease();
} else if (buttonPressed(BUTTON_DOWN)) {
recordActivity();
if (canPlace(pieceX, pieceY + 1, pieceType, pieceRot)) pieceY++;
waitForButtonRelease();
} else if (buttonPressed(BUTTON_B)) {
recordActivity();
currentMode = MODE_MENU;
waitForButtonRelease();
}
}
};
class SnakeGame {
public:
static const int MAX_LEN = 30;
int snakeX[MAX_LEN], snakeY[MAX_LEN];
int snakeLen;
int dirX, dirY;
int foodX, foodY;
unsigned long lastMove;
int score;
bool gameOver;
SnakeGame() { reset(); }
void reset() {
snakeLen = 3;
for (int i = 0; i < snakeLen; i++) {
snakeX[i] = 4;
snakeY[i] = 4 - i;
}
dirX = 0;
dirY = 1;
score = 0;
gameOver = false;
spawnFood();
}
void spawnFood() {
foodX = random(MATRIX_COLS);
foodY = random(MATRIX_ROWS);
for (int i = 0; i < snakeLen; i++) {
if (snakeX[i] == foodX && snakeY[i] == foodY) {
spawnFood();
return;
}
}
}
void render() {
ledMatrixClear();
ledMatrixSetPixel(foodX, foodY, 255);
for (int i = 0; i < snakeLen; i++) {
ledMatrixSetPixel(snakeX[i], snakeY[i], i == 0 ? 255 : 150);
}
}
void update() {
if (gameOver) return;
unsigned long now = millis();
if (now - lastMove > 150) {
lastMove = now;
int headX = snakeX[0] + dirX;
int headY = snakeY[0] + dirY;
if (headX < 0 || headX >= MATRIX_COLS || headY < 0 || headY >= MATRIX_ROWS) {
gameOver = true;
return;
}
for (int i = 1; i < snakeLen; i++) {
if (snakeX[i] == headX && snakeY[i] == headY) {
gameOver = true;
return;
}
}
for (int i = snakeLen - 1; i > 0; i--) {
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
snakeX[0] = headX;
snakeY[0] = headY;
if (headX == foodX && headY == foodY) {
if (snakeLen < MAX_LEN) snakeLen++;
score += 10;
spawnFood();
}
}
}
void input() {
if (buttonPressed(BUTTON_LEFT)) {
recordActivity();
if (dirX != 1) { dirX = -1; dirY = 0; }
waitForButtonRelease();
} else if (buttonPressed(BUTTON_RIGHT)) {
recordActivity();
if (dirX != -1) { dirX = 1; dirY = 0; }
waitForButtonRelease();
} else if (buttonPressed(BUTTON_UP)) {
recordActivity();
if (dirY != 1) { dirX = 0; dirY = -1; }
waitForButtonRelease();
} else if (buttonPressed(BUTTON_DOWN)) {
recordActivity();
if (dirY != -1) { dirX = 0; dirY = 1; }
waitForButtonRelease();
} else if (buttonPressed(BUTTON_B)) {
recordActivity();
currentMode = MODE_MENU;
waitForButtonRelease();
}
}
};
class BrickGame {
public:
static const int ROWS = 4;
static const int COLS = 5;
int bricks[ROWS][COLS];
int ballX, ballY, ballVX, ballVY;
int paddleX;
unsigned long lastUpdate;
int score;
bool gameOver;
BrickGame() { reset(); }
void reset() {
memset(bricks, 0, sizeof(bricks));
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
bricks[r][c] = (r + 1) * 40;
}
}
ballX = 4;
ballY = 4;
ballVX = 1;
ballVY = -1;
paddleX = 4;
score = 0;
gameOver = false;
}
void render() {
ledMatrixClear();
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
if (bricks[r][c]) {
ledMatrixSetPixel(c, r + 1, bricks[r][c]);
}
}
}
ledMatrixSetPixel(ballX, ballY, 255);
ledMatrixSetPixel(paddleX, 8, 255);
if (paddleX > 0) ledMatrixSetPixel(paddleX - 1, 8, 100);
if (paddleX < 8) ledMatrixSetPixel(paddleX + 1, 8, 100);
}
void update() {
if (gameOver) return;
unsigned long now = millis();
if (now - lastUpdate > 200) {
lastUpdate = now;
ballX += ballVX;
ballY += ballVY;
if (ballX < 0 || ballX >= MATRIX_COLS) {
ballVX = -ballVX;
ballX += ballVX;
}
if (ballY < 0) {
ballVY = -ballVY;
ballY += ballVY;
}
if (ballY == 8) {
if (abs(ballX - paddleX) <= 1) {
ballVY = -ballVY;
score += 10;
} else {
gameOver = true;
}
}
int bx = ballX;
int by = ballY;
if (by >= 1 && by <= ROWS && bx >= 0 && bx < COLS) {
if (bricks[by - 1][bx]) {
bricks[by - 1][bx] = 0;
ballVY = -ballVY;
score += 10;
}
}
}
}
void input() {
if (buttonPressed(BUTTON_LEFT)) {
recordActivity();
if (paddleX > 0) paddleX--;
waitForButtonRelease();
} else if (buttonPressed(BUTTON_RIGHT)) {
recordActivity();
if (paddleX < 8) paddleX++;
waitForButtonRelease();
} else if (buttonPressed(BUTTON_B)) {
recordActivity();
currentMode = MODE_MENU;
waitForButtonRelease();
}
}
};
class PongGame {
public:
int leftY, rightY;
int ballX, ballY, ballVX, ballVY;
int leftScore, rightScore;
unsigned long lastUpdate;
bool gameOver;
PongGame() { reset(); }
void reset() {
leftY = 4;
rightY = 4;
ballX = 4;
ballY = 4;
ballVX = 1;
ballVY = random(2) ? 1 : -1;
leftScore = 0;
rightScore = 0;
gameOver = false;
}
void render() {
ledMatrixClear();
ledMatrixSetPixel(0, leftY, 255);
ledMatrixSetPixel(8, rightY, 255);
ledMatrixSetPixel(ballX, ballY, 255);
}
void update() {
if (gameOver) return;
unsigned long now = millis();
if (now - lastUpdate > 200) {
lastUpdate = now;
ballX += ballVX;
ballY += ballVY;
if (ballY < 0 || ballY >= MATRIX_ROWS) {
ballVY = -ballVY;
}
if (ballX == 1 && abs(ballY - leftY) <= 1) {
ballVX = -ballVX;
leftScore++;
}
if (ballX == 7 && abs(ballY - rightY) <= 1) {
ballVX = -ballVX;
rightScore++;
}
if (ballX < 0 || ballX > 8) {
gameOver = true;
}
}
}
void input() {
if (buttonPressed(BUTTON_LEFT)) {
recordActivity();
if (leftY > 0) leftY--;
waitForButtonRelease();
} else if (buttonPressed(BUTTON_RIGHT)) {
recordActivity();
if (rightY > 0) rightY--;
waitForButtonRelease();
} else if (buttonPressed(BUTTON_UP)) {
recordActivity();
rightY = leftY;
if (rightY > 0) rightY--;
waitForButtonRelease();
} else if (buttonPressed(BUTTON_DOWN)) {
recordActivity();
rightY = leftY;
if (rightY < 8) rightY++;
waitForButtonRelease();
} else if (buttonPressed(BUTTON_A)) {
recordActivity();
if (leftY < 8) leftY++;
waitForButtonRelease();
} else if (buttonPressed(BUTTON_B)) {
recordActivity();
currentMode = MODE_MENU;
waitForButtonRelease();
}
}
};
TetrisGame tetrisGame;
SnakeGame snakeGame;
BrickGame brickGame;
PongGame pongGame;
void showIdleScreen() {
ledMatrixClear();
int msgIdx = random(NUM_MESSAGES);
drawTextCentered(RANDOM_MESSAGES[msgIdx], 4, 150);
currentMode = MODE_IDLE;
lastActivity = millis();
}
void setup() {
delay(1000);
setupButtons();
setupFrontLEDs();
setupFlashlight();
setupSAO();
ledMatrixBegin();
ledMatrixClear();
showMenu();
lastActivity = millis();
}
void loop() {
unsigned long now = millis();
knightRiderSweep(80);
updateSAO(now);
updateTVBGone(now);
if (anyButtonPressed()) {
if (currentMode == MODE_IDLE) {
currentMode = MODE_MENU;
showMenu();
}
lastActivity = millis();
}
if (now - lastActivity > IDLE_TIMEOUT) {
if (currentMode != MODE_IDLE && currentMode != MODE_TVBGONE) {
showIdleScreen();
}
}
switch (currentMode) {
case MODE_MENU:
handleMenuInput();
break;
case MODE_TETRIS:
tetrisGame.input();
tetrisGame.update();
tetrisGame.render();
break;
case MODE_SNAKE:
snakeGame.input();
snakeGame.update();
snakeGame.render();
break;
case MODE_BRICK:
brickGame.input();
brickGame.update();
brickGame.render();
break;
case MODE_PONG:
pongGame.input();
pongGame.update();
pongGame.render();
break;
case MODE_TVBGONE:
if (buttonPressed(BUTTON_B)) {
recordActivity();
currentMode = MODE_MENU;
digitalWrite(FLASHLIGHT_LED, LOW);
waitForButtonRelease();
showMenu();
}
break;
case MODE_IDLE:
break;
}
delay(10);
}