Autoflasher added
This commit is contained in:
parent
1d192c0b7b
commit
c9100ea957
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in New Issue