Initial implementation of HeisserDraht buzz wire game
ESP32-C3 based hot wire game with WS2812B LED effects, SSD1306 OLED display, debounced touch detection, and full game state machine (IDLE → COUNTDOWN → PLAYING → GAME_OVER). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Arduino build artifacts
|
||||
build/
|
||||
*.elf
|
||||
*.bin
|
||||
*.hex
|
||||
*.map
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
.idea/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Backup files
|
||||
*~
|
||||
*.bak
|
||||
*.swp
|
||||
54
HeisserDraht.ino
Normal file
54
HeisserDraht.ino
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* HeisserDraht - ESP32-C3 Buzz Wire Game
|
||||
*
|
||||
* Hardware:
|
||||
* - ESP32-C3 (RISC-V)
|
||||
* - 16x WS2812B LEDs on GPIO 8
|
||||
* - SSD1306 128x64 OLED on I2C (SDA=4, SCL=5)
|
||||
* - Wire touch sensor on GPIO 2 (INPUT_PULLUP, touch = GND)
|
||||
* - Start button on GPIO 3 (INPUT_PULLUP, press = GND)
|
||||
* - Optional passive buzzer on GPIO 10
|
||||
*
|
||||
* Game flow: IDLE → COUNTDOWN → PLAYING → GAME_OVER → IDLE
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "game_logic.h"
|
||||
#include "led_effects.h"
|
||||
#include "display.h"
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(500);
|
||||
Serial.println();
|
||||
Serial.println("=== HEISSER DRAHT " VERSION_STR " ===");
|
||||
Serial.println("[INIT] Starting...");
|
||||
|
||||
// Initialize subsystems
|
||||
gameInit();
|
||||
Serial.println("[INIT] Game logic OK");
|
||||
|
||||
ledsInit();
|
||||
Serial.println("[INIT] LEDs OK");
|
||||
|
||||
displayInit();
|
||||
Serial.println("[INIT] Display OK");
|
||||
|
||||
Serial.println("[INIT] Ready! Press START to begin.");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Check inputs
|
||||
gameCheckStartButton();
|
||||
gameCheckTouch();
|
||||
|
||||
// Update game state
|
||||
gameUpdate();
|
||||
|
||||
// Update outputs
|
||||
ledsUpdate();
|
||||
displayUpdate();
|
||||
|
||||
// Small delay for watchdog
|
||||
delay(LOOP_DELAY_MS);
|
||||
}
|
||||
50
config.h
Normal file
50
config.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
// ── Pin Assignments ─────────────────────────────────────────────────
|
||||
#define PIN_NEOPIXEL 8 // WS2812B data (330Ω series resistor recommended)
|
||||
#define PIN_SDA 4 // I2C SDA (OLED)
|
||||
#define PIN_SCL 5 // I2C SCL (OLED)
|
||||
#define PIN_WIRE_TOUCH 2 // Wire touch detect (INPUT_PULLUP, loop-to-GND)
|
||||
#define PIN_START_BTN 3 // Start button (INPUT_PULLUP, button-to-GND)
|
||||
#define PIN_BUZZER 10 // Optional passive piezo buzzer
|
||||
|
||||
// ── LED Configuration ───────────────────────────────────────────────
|
||||
#define NUM_LEDS 16 // Number of WS2812B LEDs (adjustable 10-20)
|
||||
#define LED_BRIGHTNESS 60 // Default brightness (0-255)
|
||||
|
||||
// ── OLED Configuration ──────────────────────────────────────────────
|
||||
#define SCREEN_WIDTH 128
|
||||
#define SCREEN_HEIGHT 64
|
||||
#define OLED_ADDR 0x3C
|
||||
#define DISPLAY_UPDATE_MS 150 // Throttle display updates to avoid I2C flicker
|
||||
|
||||
// ── Game Timing ─────────────────────────────────────────────────────
|
||||
#define GAME_DURATION_S 60 // Game duration in seconds
|
||||
#define COUNTDOWN_SECS 3 // Countdown before game starts
|
||||
#define MAX_STRIKES 10 // Maximum strikes before game over
|
||||
|
||||
// ── Debounce ────────────────────────────────────────────────────────
|
||||
#define TOUCH_DEBOUNCE_MS 200 // Touch detection debounce
|
||||
#define BUTTON_DEBOUNCE_MS 50 // Start button debounce
|
||||
|
||||
// ── Scoring ─────────────────────────────────────────────────────────
|
||||
#define SCORE_BASE 1000 // Starting score
|
||||
#define SCORE_STRIKE_PENALTY 50 // Points lost per strike
|
||||
#define SCORE_TIME_DIVISOR 100 // elapsed_ms / divisor = time penalty
|
||||
|
||||
// ── Buzzer Tones ────────────────────────────────────────────────────
|
||||
#define TONE_COUNTDOWN_HZ 800
|
||||
#define TONE_GO_HZ 1200
|
||||
#define TONE_TOUCH_HZ 1000
|
||||
#define TONE_WIN_HZ 1500
|
||||
#define TONE_LOSE_HZ 400
|
||||
#define TONE_DURATION_MS 150
|
||||
|
||||
// ── Loop Timing ─────────────────────────────────────────────────────
|
||||
#define LOOP_DELAY_MS 10 // Main loop delay for watchdog
|
||||
|
||||
// ── Version ─────────────────────────────────────────────────────────
|
||||
#define VERSION_STR "v1.0"
|
||||
|
||||
#endif // CONFIG_H
|
||||
164
display.cpp
Normal file
164
display.cpp
Normal file
@@ -0,0 +1,164 @@
|
||||
#include "display.h"
|
||||
#include "config.h"
|
||||
#include "game_logic.h"
|
||||
#include <Wire.h>
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <Adafruit_SSD1306.h>
|
||||
|
||||
static Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
|
||||
static unsigned long lastUpdateMs = 0;
|
||||
|
||||
// ── Screens ─────────────────────────────────────────────────────────
|
||||
|
||||
static void screenSplash() {
|
||||
oled.clearDisplay();
|
||||
oled.setTextColor(SSD1306_WHITE);
|
||||
|
||||
oled.setTextSize(2);
|
||||
oled.setCursor(8, 10);
|
||||
oled.print("HEISSER");
|
||||
oled.setCursor(20, 30);
|
||||
oled.print("DRAHT");
|
||||
|
||||
oled.setTextSize(1);
|
||||
oled.setCursor(48, 52);
|
||||
oled.print(VERSION_STR);
|
||||
|
||||
oled.display();
|
||||
}
|
||||
|
||||
static void screenIdle() {
|
||||
oled.clearDisplay();
|
||||
oled.setTextColor(SSD1306_WHITE);
|
||||
|
||||
oled.setTextSize(2);
|
||||
oled.setCursor(8, 5);
|
||||
oled.print("HEISSER");
|
||||
oled.setCursor(20, 23);
|
||||
oled.print("DRAHT");
|
||||
|
||||
// Blinking "Druecke START"
|
||||
if ((millis() / 500) % 2 == 0) {
|
||||
oled.setTextSize(1);
|
||||
oled.setCursor(12, 50);
|
||||
oled.print("Druecke START");
|
||||
}
|
||||
|
||||
oled.display();
|
||||
}
|
||||
|
||||
static void screenCountdown() {
|
||||
GameData& gd = gameGetData();
|
||||
oled.clearDisplay();
|
||||
oled.setTextColor(SSD1306_WHITE);
|
||||
|
||||
if (gd.countdownValue > 0) {
|
||||
oled.setTextSize(4);
|
||||
oled.setCursor(52, 16);
|
||||
oled.print(gd.countdownValue);
|
||||
} else {
|
||||
oled.setTextSize(3);
|
||||
oled.setCursor(36, 20);
|
||||
oled.print("GO!");
|
||||
}
|
||||
|
||||
oled.display();
|
||||
}
|
||||
|
||||
static void screenPlaying() {
|
||||
GameData& gd = gameGetData();
|
||||
oled.clearDisplay();
|
||||
oled.setTextColor(SSD1306_WHITE);
|
||||
|
||||
// Timer MM:SS
|
||||
unsigned long remainMs = 0;
|
||||
unsigned long totalMs = (unsigned long)GAME_DURATION_S * 1000UL;
|
||||
if (gd.elapsedMs < totalMs) remainMs = totalMs - gd.elapsedMs;
|
||||
uint8_t mins = (remainMs / 1000) / 60;
|
||||
uint8_t secs = (remainMs / 1000) % 60;
|
||||
|
||||
oled.setTextSize(1);
|
||||
oled.setCursor(0, 0);
|
||||
oled.print("Zeit: ");
|
||||
if (mins < 10) oled.print('0');
|
||||
oled.print(mins);
|
||||
oled.print(':');
|
||||
if (secs < 10) oled.print('0');
|
||||
oled.print(secs);
|
||||
|
||||
// Strikes
|
||||
oled.setCursor(0, 14);
|
||||
oled.print("Fehler: ");
|
||||
oled.print(gd.strikes);
|
||||
oled.print('/');
|
||||
oled.print(MAX_STRIKES);
|
||||
|
||||
// Score (large)
|
||||
oled.setTextSize(3);
|
||||
oled.setCursor(10, 34);
|
||||
oled.print(gd.score);
|
||||
|
||||
oled.display();
|
||||
}
|
||||
|
||||
static void screenGameOver() {
|
||||
GameData& gd = gameGetData();
|
||||
oled.clearDisplay();
|
||||
oled.setTextColor(SSD1306_WHITE);
|
||||
|
||||
oled.setTextSize(1);
|
||||
oled.setCursor(20, 0);
|
||||
oled.print(gd.won ? "GESCHAFFT!" : "GAME OVER!");
|
||||
|
||||
// Final score
|
||||
oled.setTextSize(2);
|
||||
oled.setCursor(10, 14);
|
||||
oled.print("Score:");
|
||||
oled.print(gd.score);
|
||||
|
||||
// Stats
|
||||
oled.setTextSize(1);
|
||||
unsigned long totalSec = gd.elapsedMs / 1000;
|
||||
oled.setCursor(0, 36);
|
||||
oled.print("Zeit: ");
|
||||
oled.print(totalSec);
|
||||
oled.print("s Fehler: ");
|
||||
oled.print(gd.strikes);
|
||||
|
||||
// Restart hint (blinking)
|
||||
if ((millis() / 600) % 2 == 0) {
|
||||
oled.setCursor(8, 54);
|
||||
oled.print("START = Nochmal");
|
||||
}
|
||||
|
||||
oled.display();
|
||||
}
|
||||
|
||||
// ── Public API ──────────────────────────────────────────────────────
|
||||
|
||||
void displayInit() {
|
||||
Wire.begin(PIN_SDA, PIN_SCL);
|
||||
|
||||
if (!oled.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
|
||||
Serial.println("[OLED] Init FAILED");
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println("[OLED] Init OK");
|
||||
screenSplash();
|
||||
delay(1500);
|
||||
}
|
||||
|
||||
void displayUpdate() {
|
||||
unsigned long now = millis();
|
||||
if (now - lastUpdateMs < DISPLAY_UPDATE_MS) return;
|
||||
lastUpdateMs = now;
|
||||
|
||||
GameData& gd = gameGetData();
|
||||
switch (gd.state) {
|
||||
case STATE_IDLE: screenIdle(); break;
|
||||
case STATE_COUNTDOWN: screenCountdown(); break;
|
||||
case STATE_PLAYING: screenPlaying(); break;
|
||||
case STATE_GAME_OVER: screenGameOver(); break;
|
||||
}
|
||||
}
|
||||
9
display.h
Normal file
9
display.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#ifndef DISPLAY_H
|
||||
#define DISPLAY_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
void displayInit();
|
||||
void displayUpdate(); // Called every loop, dispatches based on game state (throttled)
|
||||
|
||||
#endif // DISPLAY_H
|
||||
142
game_logic.cpp
Normal file
142
game_logic.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
#include "game_logic.h"
|
||||
#include "config.h"
|
||||
#include "led_effects.h"
|
||||
|
||||
static GameData gd;
|
||||
|
||||
void gameInit() {
|
||||
pinMode(PIN_WIRE_TOUCH, INPUT_PULLUP);
|
||||
pinMode(PIN_START_BTN, INPUT_PULLUP);
|
||||
pinMode(PIN_BUZZER, OUTPUT);
|
||||
digitalWrite(PIN_BUZZER, LOW);
|
||||
|
||||
gd.state = STATE_IDLE;
|
||||
gd.strikes = 0;
|
||||
gd.score = SCORE_BASE;
|
||||
gd.gameStartMs = 0;
|
||||
gd.elapsedMs = 0;
|
||||
gd.countdownValue = COUNTDOWN_SECS;
|
||||
gd.countdownStepMs = 0;
|
||||
gd.touchActive = false;
|
||||
gd.lastTouchMs = 0;
|
||||
gd.lastButtonMs = 0;
|
||||
gd.won = false;
|
||||
}
|
||||
|
||||
GameData& gameGetData() {
|
||||
return gd;
|
||||
}
|
||||
|
||||
void gameCheckStartButton() {
|
||||
if (digitalRead(PIN_START_BTN) == LOW) {
|
||||
unsigned long now = millis();
|
||||
if (now - gd.lastButtonMs < BUTTON_DEBOUNCE_MS) return;
|
||||
gd.lastButtonMs = now;
|
||||
|
||||
if (gd.state == STATE_IDLE || gd.state == STATE_GAME_OVER) {
|
||||
// Reset game data
|
||||
gd.strikes = 0;
|
||||
gd.score = SCORE_BASE;
|
||||
gd.elapsedMs = 0;
|
||||
gd.countdownValue = COUNTDOWN_SECS;
|
||||
gd.touchActive = false;
|
||||
gd.won = false;
|
||||
|
||||
// Start countdown
|
||||
gd.state = STATE_COUNTDOWN;
|
||||
gd.countdownStepMs = millis();
|
||||
|
||||
Serial.println("[GAME] START pressed -> COUNTDOWN");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gameCheckTouch() {
|
||||
if (gd.state != STATE_PLAYING) return;
|
||||
|
||||
bool touching = (digitalRead(PIN_WIRE_TOUCH) == LOW);
|
||||
unsigned long now = millis();
|
||||
|
||||
if (touching && !gd.touchActive) {
|
||||
if (now - gd.lastTouchMs < TOUCH_DEBOUNCE_MS) return;
|
||||
gd.lastTouchMs = now;
|
||||
gd.touchActive = true;
|
||||
gd.strikes++;
|
||||
|
||||
Serial.print("[GAME] TOUCH! Strikes: ");
|
||||
Serial.println(gd.strikes);
|
||||
|
||||
// Buzzer alert
|
||||
tone(PIN_BUZZER, TONE_TOUCH_HZ, TONE_DURATION_MS);
|
||||
|
||||
// Blocking LED flash
|
||||
ledsTouchFlash();
|
||||
|
||||
// Check for game over by max strikes
|
||||
if (gd.strikes >= MAX_STRIKES) {
|
||||
gd.won = false;
|
||||
gd.state = STATE_GAME_OVER;
|
||||
// Lose tone
|
||||
tone(PIN_BUZZER, TONE_LOSE_HZ, 500);
|
||||
Serial.println("[GAME] MAX STRIKES -> GAME_OVER (lose)");
|
||||
}
|
||||
} else if (!touching) {
|
||||
gd.touchActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
void gameUpdate() {
|
||||
unsigned long now = millis();
|
||||
|
||||
switch (gd.state) {
|
||||
case STATE_IDLE:
|
||||
// Nothing to update, waiting for start button
|
||||
break;
|
||||
|
||||
case STATE_COUNTDOWN:
|
||||
if (now - gd.countdownStepMs >= 1000) {
|
||||
gd.countdownStepMs = now;
|
||||
if (gd.countdownValue > 0) {
|
||||
// Countdown beep
|
||||
tone(PIN_BUZZER, TONE_COUNTDOWN_HZ, 100);
|
||||
Serial.print("[GAME] Countdown: ");
|
||||
Serial.println(gd.countdownValue);
|
||||
gd.countdownValue--;
|
||||
} else {
|
||||
// GO!
|
||||
tone(PIN_BUZZER, TONE_GO_HZ, 200);
|
||||
gd.state = STATE_PLAYING;
|
||||
gd.gameStartMs = millis();
|
||||
Serial.println("[GAME] GO! -> PLAYING");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case STATE_PLAYING:
|
||||
gd.elapsedMs = now - gd.gameStartMs;
|
||||
|
||||
// Calculate live score
|
||||
gd.score = SCORE_BASE
|
||||
- (gd.strikes * SCORE_STRIKE_PENALTY)
|
||||
- (int)(gd.elapsedMs / SCORE_TIME_DIVISOR);
|
||||
if (gd.score < 0) gd.score = 0;
|
||||
|
||||
// Check time up
|
||||
if (gd.elapsedMs >= (unsigned long)GAME_DURATION_S * 1000UL) {
|
||||
gd.won = (gd.strikes < MAX_STRIKES);
|
||||
gd.state = STATE_GAME_OVER;
|
||||
if (gd.won) {
|
||||
tone(PIN_BUZZER, TONE_WIN_HZ, 300);
|
||||
Serial.println("[GAME] TIME UP -> GAME_OVER (win)");
|
||||
} else {
|
||||
tone(PIN_BUZZER, TONE_LOSE_HZ, 500);
|
||||
Serial.println("[GAME] TIME UP -> GAME_OVER (lose)");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case STATE_GAME_OVER:
|
||||
// Waiting for start button to restart
|
||||
break;
|
||||
}
|
||||
}
|
||||
37
game_logic.h
Normal file
37
game_logic.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#ifndef GAME_LOGIC_H
|
||||
#define GAME_LOGIC_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// Game states
|
||||
enum GameState {
|
||||
STATE_IDLE,
|
||||
STATE_COUNTDOWN,
|
||||
STATE_PLAYING,
|
||||
STATE_GAME_OVER
|
||||
};
|
||||
|
||||
// Game data
|
||||
struct GameData {
|
||||
GameState state;
|
||||
uint8_t strikes;
|
||||
int score;
|
||||
unsigned long gameStartMs;
|
||||
unsigned long elapsedMs;
|
||||
uint8_t countdownValue; // 3, 2, 1, 0(GO!)
|
||||
unsigned long countdownStepMs;
|
||||
bool touchActive; // true while wire is being touched
|
||||
unsigned long lastTouchMs;
|
||||
unsigned long lastButtonMs;
|
||||
bool won; // true if timer ran out with strikes < MAX
|
||||
};
|
||||
|
||||
void gameInit();
|
||||
void gameUpdate();
|
||||
GameData& gameGetData();
|
||||
|
||||
// Called by main loop to check inputs
|
||||
void gameCheckStartButton();
|
||||
void gameCheckTouch();
|
||||
|
||||
#endif // GAME_LOGIC_H
|
||||
132
led_effects.cpp
Normal file
132
led_effects.cpp
Normal file
@@ -0,0 +1,132 @@
|
||||
#include "led_effects.h"
|
||||
#include "config.h"
|
||||
#include "game_logic.h"
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
|
||||
static Adafruit_NeoPixel strip(NUM_LEDS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
static uint32_t colorWheel(uint8_t pos) {
|
||||
pos = 255 - pos;
|
||||
if (pos < 85) return strip.Color(255 - pos * 3, 0, pos * 3);
|
||||
if (pos < 170) { pos -= 85; return strip.Color(0, pos * 3, 255 - pos * 3); }
|
||||
pos -= 170;
|
||||
return strip.Color(pos * 3, 255 - pos * 3, 0);
|
||||
}
|
||||
|
||||
// ── Animations ──────────────────────────────────────────────────────
|
||||
|
||||
static void animIdle() {
|
||||
// Rainbow chase
|
||||
static uint16_t hue = 0;
|
||||
for (int i = 0; i < NUM_LEDS; i++) {
|
||||
strip.setPixelColor(i, colorWheel((i * 256 / NUM_LEDS + hue) & 0xFF));
|
||||
}
|
||||
hue += 2;
|
||||
strip.show();
|
||||
}
|
||||
|
||||
static void animCountdown() {
|
||||
GameData& gd = gameGetData();
|
||||
uint32_t color;
|
||||
switch (gd.countdownValue) {
|
||||
case 3: // fall through
|
||||
case 2: color = strip.Color(255, 0, 0); break; // Red
|
||||
case 1: color = strip.Color(255, 180, 0); break; // Yellow
|
||||
default: color = strip.Color(0, 255, 0); break; // Green (GO!)
|
||||
}
|
||||
for (int i = 0; i < NUM_LEDS; i++) {
|
||||
strip.setPixelColor(i, color);
|
||||
}
|
||||
strip.show();
|
||||
}
|
||||
|
||||
static void animPlaying() {
|
||||
GameData& gd = gameGetData();
|
||||
unsigned long totalMs = (unsigned long)GAME_DURATION_S * 1000UL;
|
||||
float progress = 1.0f - (float)gd.elapsedMs / (float)totalMs;
|
||||
if (progress < 0.0f) progress = 0.0f;
|
||||
|
||||
int litLeds = (int)(progress * NUM_LEDS + 0.5f);
|
||||
|
||||
for (int i = 0; i < NUM_LEDS; i++) {
|
||||
if (i < litLeds) {
|
||||
// Color transitions: green → yellow → red as time depletes
|
||||
if (progress > 0.5f) strip.setPixelColor(i, strip.Color(0, 255, 0));
|
||||
else if (progress > 0.2f) strip.setPixelColor(i, strip.Color(255, 180, 0));
|
||||
else strip.setPixelColor(i, strip.Color(255, 0, 0));
|
||||
} else {
|
||||
strip.setPixelColor(i, 0);
|
||||
}
|
||||
}
|
||||
strip.show();
|
||||
}
|
||||
|
||||
static void animGameOverWin() {
|
||||
// Green/gold sparkle
|
||||
static unsigned long lastSparkle = 0;
|
||||
unsigned long now = millis();
|
||||
if (now - lastSparkle > 80) {
|
||||
lastSparkle = now;
|
||||
for (int i = 0; i < NUM_LEDS; i++) {
|
||||
if (random(3) == 0)
|
||||
strip.setPixelColor(i, strip.Color(255, 215, 0)); // Gold
|
||||
else
|
||||
strip.setPixelColor(i, strip.Color(0, 180, 0)); // Green
|
||||
}
|
||||
strip.show();
|
||||
}
|
||||
}
|
||||
|
||||
static void animGameOverLose() {
|
||||
// Pulsing red breathing
|
||||
static uint8_t breathVal = 0;
|
||||
static int8_t breathDir = 2;
|
||||
breathVal += breathDir;
|
||||
if (breathVal >= 250 || breathVal <= 5) breathDir = -breathDir;
|
||||
|
||||
for (int i = 0; i < NUM_LEDS; i++) {
|
||||
strip.setPixelColor(i, strip.Color(breathVal, 0, 0));
|
||||
}
|
||||
strip.show();
|
||||
}
|
||||
|
||||
// ── Public API ──────────────────────────────────────────────────────
|
||||
|
||||
void ledsInit() {
|
||||
strip.begin();
|
||||
strip.setBrightness(LED_BRIGHTNESS);
|
||||
strip.show();
|
||||
}
|
||||
|
||||
void ledsSetBrightness(uint8_t b) {
|
||||
strip.setBrightness(b);
|
||||
}
|
||||
|
||||
void ledsTouchFlash() {
|
||||
// Blocking 3x red blink (~300ms total)
|
||||
for (int flash = 0; flash < 3; flash++) {
|
||||
for (int i = 0; i < NUM_LEDS; i++)
|
||||
strip.setPixelColor(i, strip.Color(255, 0, 0));
|
||||
strip.show();
|
||||
delay(50);
|
||||
for (int i = 0; i < NUM_LEDS; i++)
|
||||
strip.setPixelColor(i, 0);
|
||||
strip.show();
|
||||
delay(50);
|
||||
}
|
||||
}
|
||||
|
||||
void ledsUpdate() {
|
||||
GameData& gd = gameGetData();
|
||||
switch (gd.state) {
|
||||
case STATE_IDLE: animIdle(); break;
|
||||
case STATE_COUNTDOWN: animCountdown(); break;
|
||||
case STATE_PLAYING: animPlaying(); break;
|
||||
case STATE_GAME_OVER:
|
||||
if (gd.won) animGameOverWin();
|
||||
else animGameOverLose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
11
led_effects.h
Normal file
11
led_effects.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#ifndef LED_EFFECTS_H
|
||||
#define LED_EFFECTS_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
void ledsInit();
|
||||
void ledsUpdate(); // Called every loop, dispatches based on game state
|
||||
void ledsTouchFlash(); // Blocking 3x red blink (~300ms)
|
||||
void ledsSetBrightness(uint8_t b);
|
||||
|
||||
#endif // LED_EFFECTS_H
|
||||
Reference in New Issue
Block a user