From c9100ea9575636a79d342ff35b1af2ff711cd5e9 Mon Sep 17 00:00:00 2001 From: superminaren Date: Fri, 8 May 2026 10:20:30 +0200 Subject: [PATCH] Autoflasher added --- autoflasher/99-rp2040-flash.rules | 2 + autoflasher/README.md | 175 +++++ autoflasher/flash-rp2040.sh | 67 ++ autoflasher/install.sh | 9 + autoflasher/rp2040-flash@.service | 11 + autoflasher/tail.sh | 3 + rp2040_badge_primary/js/index.html | 1034 +++++++++++++++++++++++++++ rp2040_badge_primary/platformio.ini | 9 + rp2040_badge_primary/src/main.cpp | 830 +++++++++++++++++++++ 9 files changed, 2140 insertions(+) create mode 100644 autoflasher/99-rp2040-flash.rules create mode 100644 autoflasher/README.md create mode 100644 autoflasher/flash-rp2040.sh create mode 100644 autoflasher/install.sh create mode 100644 autoflasher/rp2040-flash@.service create mode 100644 autoflasher/tail.sh create mode 100644 rp2040_badge_primary/js/index.html create mode 100644 rp2040_badge_primary/platformio.ini create mode 100644 rp2040_badge_primary/src/main.cpp diff --git a/autoflasher/99-rp2040-flash.rules b/autoflasher/99-rp2040-flash.rules new file mode 100644 index 0000000..fdd0c35 --- /dev/null +++ b/autoflasher/99-rp2040-flash.rules @@ -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" diff --git a/autoflasher/README.md b/autoflasher/README.md new file mode 100644 index 0000000..87f91e4 --- /dev/null +++ b/autoflasher/README.md @@ -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 +``` + + diff --git a/autoflasher/flash-rp2040.sh b/autoflasher/flash-rp2040.sh new file mode 100644 index 0000000..e918ba1 --- /dev/null +++ b/autoflasher/flash-rp2040.sh @@ -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 }" +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" diff --git a/autoflasher/install.sh b/autoflasher/install.sh new file mode 100644 index 0000000..67f39e7 --- /dev/null +++ b/autoflasher/install.sh @@ -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 diff --git a/autoflasher/rp2040-flash@.service b/autoflasher/rp2040-flash@.service new file mode 100644 index 0000000..210b6d2 --- /dev/null +++ b/autoflasher/rp2040-flash@.service @@ -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 diff --git a/autoflasher/tail.sh b/autoflasher/tail.sh new file mode 100644 index 0000000..bd27313 --- /dev/null +++ b/autoflasher/tail.sh @@ -0,0 +1,3 @@ +journalctl -t 'rp2040-flash*' -f +# or +tail -f /var/log/rp2040-flash.log diff --git a/rp2040_badge_primary/js/index.html b/rp2040_badge_primary/js/index.html new file mode 100644 index 0000000..cbc804e --- /dev/null +++ b/rp2040_badge_primary/js/index.html @@ -0,0 +1,1034 @@ + + + + + + Securityfest 2026 Badge + + + +

Securityfest 2026 Badge

+ +
+ + +
+
+
+
+
+
+ +
+
+
+ +
+ + + + + + +
+ +
+
+
+ GPIO0 +
+
+
+ GPIO1 PWM +
+
+
+ +
+ Navigate + A Select + B Back +
+ + + + diff --git a/rp2040_badge_primary/platformio.ini b/rp2040_badge_primary/platformio.ini new file mode 100644 index 0000000..0bdde77 --- /dev/null +++ b/rp2040_badge_primary/platformio.ini @@ -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 \ No newline at end of file diff --git a/rp2040_badge_primary/src/main.cpp b/rp2040_badge_primary/src/main.cpp new file mode 100644 index 0000000..a10c2c4 --- /dev/null +++ b/rp2040_badge_primary/src/main.cpp @@ -0,0 +1,830 @@ +#include +#include + +#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); +}