/***************************************************************
HNGR13 Bomb Timer - ILI9341 TFT (Querformat) + TM1637 + NeoPixel
- Mit Pink Panther Theme beim Begrüßungsbildschirm
- Grafisch aufgewertet mit modernen UI-Elementen
- Adafruit_ILI9341 & Adafruit_GFX verwendet
- Hauptschalter D12: "Bitte einschalten" wird gezeigt bis ON
- Nach Einschalten: "HNGR13 BOMB - bitte START drücken" bis START
- Pink Panther spielt während Begrüßungsbildschirm (non-blocking)
- Danach: Konfigurationsmodus (UP/DOWN LEDs einstellen)
- START (in Config): erster Timerstart -> sofort (3x Piep non-blocking)
- Später: 5s halten toggelt Start/Stop mit Hold-Animation (Ring2)
***************************************************************/
#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <TM1637Display.h>
#include <Adafruit_NeoPixel.h>
// ----------------- PIN-Definition -----------------
#define BTN_START 6
#define BTN_UP 3
#define BTN_DOWN 4
#define DISP_CLK A4
#define DISP_DIO A5
#define RING1_PIN 2
#define RING2_PIN 5
#define RELAY1 A0
#define RELAY2 A1
#define BUZZER 7
#define LED_BTNS A2
#define MAIN_SW A3 // Hauptschalter
// TFT pins (CS, DC, RST) -> SPI: MOSI=D11, MISO=D12, SCK=D13
#define TFT_CS 10
#define TFT_DC 9
#define TFT_RST 8
// ----------------- Pink Panther Noten -----------------
#define NOTE_B0 31
#define NOTE_C1 33
#define NOTE_CS1 35
#define NOTE_D1 37
#define NOTE_DS1 39
#define NOTE_E1 41
#define NOTE_F1 44
#define NOTE_FS1 46
#define NOTE_G1 49
#define NOTE_GS1 52
#define NOTE_A1 55
#define NOTE_AS1 58
#define NOTE_B1 62
#define NOTE_C2 65
#define NOTE_CS2 69
#define NOTE_D2 73
#define NOTE_DS2 78
#define NOTE_E2 82
#define NOTE_F2 87
#define NOTE_FS2 93
#define NOTE_G2 98
#define NOTE_GS2 104
#define NOTE_A2 110
#define NOTE_AS2 117
#define NOTE_B2 123
#define NOTE_C3 131
#define NOTE_CS3 139
#define NOTE_D3 147
#define NOTE_DS3 156
#define NOTE_E3 165
#define NOTE_F3 175
#define NOTE_FS3 185
#define NOTE_G3 196
#define NOTE_GS3 208
#define NOTE_A3 220
#define NOTE_AS3 233
#define NOTE_B3 247
#define NOTE_C4 262
#define NOTE_CS4 277
#define NOTE_D4 294
#define NOTE_DS4 311
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_FS4 370
#define NOTE_G4 392
#define NOTE_GS4 415
#define NOTE_A4 440
#define NOTE_AS4 466
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_CS5 554
#define NOTE_D5 587
#define NOTE_DS5 622
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
#define NOTE_GS5 831
#define NOTE_A5 880
#define NOTE_AS5 932
#define NOTE_B5 988
#define NOTE_C6 1047
#define NOTE_CS6 1109
#define NOTE_D6 1175
#define NOTE_DS6 1245
#define NOTE_E6 1319
#define NOTE_F6 1397
#define NOTE_FS6 1480
#define NOTE_G6 1568
#define NOTE_GS6 1661
#define NOTE_A6 1760
#define NOTE_AS6 1865
#define NOTE_B6 1976
#define NOTE_C7 2093
#define NOTE_CS7 2217
#define NOTE_D7 2349
#define NOTE_DS7 2489
#define NOTE_E7 2637
#define NOTE_F7 2794
#define NOTE_FS7 2960
#define NOTE_G7 3136
#define NOTE_GS7 3322
#define NOTE_A7 3520
#define NOTE_AS7 3729
#define NOTE_B7 3951
#define NOTE_C8 4186
#define NOTE_CS8 4435
#define NOTE_D8 4699
#define NOTE_DS8 4978
#define REST 0
// Pink Panther Melodie
int pinkPantherTempo = 120;
int pinkPantherMelody[] = {
REST,2, REST,4, REST,8, NOTE_DS4,8,
NOTE_E4,-4, REST,8, NOTE_FS4,8, NOTE_G4,-4, REST,8, NOTE_DS4,8,
NOTE_E4,-8, NOTE_FS4,8, NOTE_G4,-8, NOTE_C5,8, NOTE_B4,-8, NOTE_E4,8, NOTE_G4,-8, NOTE_B4,8,
NOTE_AS4,2, NOTE_A4,-16, NOTE_G4,-16, NOTE_E4,-16, NOTE_D4,-16,
NOTE_E4,2, REST,4, REST,8, NOTE_DS4,4,
NOTE_E4,-4, REST,8, NOTE_FS4,8, NOTE_G4,-4, REST,8, NOTE_DS4,8,
NOTE_E4,-8, NOTE_FS4,8, NOTE_G4,-8, NOTE_C5,8, NOTE_B4,-8, NOTE_G4,8, NOTE_B4,-8, NOTE_E5,8,
NOTE_DS5,1,
NOTE_D5,2, REST,4, REST,8, NOTE_DS4,8,
NOTE_E4,-4, REST,8, NOTE_FS4,8, NOTE_G4,-4, REST,8, NOTE_DS4,8,
NOTE_E4,-8, NOTE_FS4,8, NOTE_G4,-8, NOTE_C5,8, NOTE_B4,-8, NOTE_E4,8, NOTE_G4,-8, NOTE_B4,8,
NOTE_AS4,2, NOTE_A4,-16, NOTE_G4,-16, NOTE_E4,-16, NOTE_D4,-16,
NOTE_E4,-4, REST,4,
REST,4, NOTE_E5,-8, NOTE_D5,8, NOTE_B4,-8, NOTE_A4,8, NOTE_G4,-8, NOTE_E4,-8,
NOTE_AS4,16, NOTE_A4,-8, NOTE_AS4,16, NOTE_A4,-8, NOTE_AS4,16, NOTE_A4,-8, NOTE_AS4,16, NOTE_A4,-8,
NOTE_G4,-16, NOTE_E4,-16, NOTE_D4,-16, NOTE_E4,16, NOTE_E4,16, NOTE_E4,2,
};
int pinkPantherNotes = sizeof(pinkPantherMelody) / sizeof(pinkPantherMelody[0]) / 2;
// Melodie-Playback State (non-blocking)
bool melodyPlaying = false;
int melodyNoteIndex = 0;
unsigned long melodyNoteStart = 0;
int melodyNoteDuration = 0;
// ----------------- Hardware-Objekte -----------------
TM1637Display tmDisplay(DISP_CLK, DISP_DIO);
Adafruit_NeoPixel ring1(16, RING1_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel ring2(16, RING2_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
// ----------------- Erweiterte Farb-Definitionen -----------------
#define COLOR_BG 0x0841 // Dunkelblau-grau
#define COLOR_PANEL 0x2945 // Helleres Panel-Grau
#define COLOR_ACCENT 0xFD20 // Orange/Rot Akzent
#define COLOR_SUCCESS 0x07E0 // Grün
#define COLOR_WARNING 0xFFE0 // Gelb
#define COLOR_DANGER 0xF800 // Rot
#define COLOR_INFO 0x07FF // Cyan
#define COLOR_TEXT 0xFFFF // Weiß
#define COLOR_TEXT_DIM 0x8410 // Grau
// ----------------- Konfiguration -----------------
const unsigned long HOLD_MS = 5000UL;
const int R2_LEDS = 16;
const int STEP_TONE_MS = 100;
const unsigned long CONFIRM_BEEP_MS = 500UL;
const int SEGMENT_HUNDREDTHS = 6000; // 60.00s per segment
const int FREQ_LOW = 400;
const int FREQ_HIGH = 2000;
const int FREQ_STEP = (FREQ_HIGH - FREQ_LOW) / (R2_LEDS - 1);
const unsigned long TFT_UPDATE_MS = 200;
const unsigned long BTN_DEBOUNCE_MS = 200;
// ----------------- State -----------------
unsigned long previousMillis = 0;
unsigned long displayBlinkMillis = 0;
unsigned long ringBlinkMillis = 0;
unsigned long lastTftUpdate = 0;
bool dispBlinkState = true;
bool colonOn = true;
int totalLeds = 10;
int ledsRemaining = totalLeds;
int timeValue = SEGMENT_HUNDREDTHS;
bool running = false;
bool hasStarted = false;
unsigned long holdStart = 0;
bool actionExecutedWhileHeld = false;
bool wasRunningAtHoldStart = false;
bool pausedForHold = false;
int prevRing2Lit = -1;
int lastSecondBeepSec = -1;
bool tftPresent = true;
char tftStatus[40] = "Bereit";
int lastLedsRemaining = -1;
int lastTotalLeds = -1;
bool systemEnabled = false;
bool forceRedraw = false;
bool lastSystemEnabled = false;
unsigned long lastUpPress = 0;
unsigned long lastDownPress = 0;
bool greetingAck = false;
unsigned long animStartTime = 0;
bool animActive = false;
bool exploded = false;
unsigned long explosionStart = 0;
bool explosionToneOn = false;
unsigned long relay1OnTime = 0;
const unsigned long EXPLOSION_BUZZ_MS = 2000UL;
const unsigned long RELAY1_PULSE_MS = 7000UL;
// ----------------- Pink Panther Melodie Funktionen -----------------
void startPinkPantherMelody() {
melodyPlaying = true;
melodyNoteIndex = 0;
melodyNoteStart = millis();
melodyNoteDuration = 0;
}
void stopPinkPantherMelody() {
melodyPlaying = false;
noTone(BUZZER);
}
void updatePinkPantherMelody() {
if (!melodyPlaying) return;
unsigned long now = millis();
// Wenn aktuelle Note noch spielt
if (now - melodyNoteStart < melodyNoteDuration) {
return;
}
// Nächste Note starten
if (melodyNoteIndex >= pinkPantherNotes * 2) {
// Melodie zu Ende, von vorne beginnen (Loop)
melodyNoteIndex = 0;
}
int wholenote = (60000 * 4) / pinkPantherTempo;
int divider = pinkPantherMelody[melodyNoteIndex + 1];
if (divider > 0) {
melodyNoteDuration = wholenote / divider;
} else if (divider < 0) {
melodyNoteDuration = wholenote / abs(divider);
melodyNoteDuration *= 1.5;
}
int note = pinkPantherMelody[melodyNoteIndex];
if (note != REST) {
tone(BUZZER, note, melodyNoteDuration * 0.9);
} else {
noTone(BUZZER);
}
melodyNoteStart = now;
melodyNoteIndex += 2;
}
// ----------------- Hilfsfunktionen -----------------
void beepBlocking(int freq, int dur) {
tone(BUZZER, freq, dur);
delay(dur);
noTone(BUZZER);
}
void shortBeepNonBlock(int freq, int dur) {
tone(BUZZER, freq, dur);
}
void fillRing1(int count, int blinkIndex = -1, bool blinkOn = true) {
ring1.clear();
for (int i = 0; i < count && i < ring1.numPixels(); ++i)
ring1.setPixelColor(i, ring1.Color(255, 0, 255));
if (blinkIndex >= 0 && blinkIndex < ring1.numPixels() && blinkOn)
ring1.setPixelColor(blinkIndex, ring1.Color(255, 131, 250));
ring1.show();
}
void pulseRelay(int pin, int durationMs) {
digitalWrite(pin, HIGH);
delay(durationMs);
digitalWrite(pin, LOW);
}
void showTimeValueOnSeg(int tv, bool show = true, bool colon = true) {
if (!show) { tmDisplay.clear(); return; }
uint8_t mask = colon ? 0b01000000 : 0x00;
tmDisplay.showNumberDecEx(tv, mask, true);
}
void clearRing2() {
ring2.clear();
ring2.show();
}
void updateRing2DuringHold(float progress, bool wasRunningWhenHoldStarted) {
if (progress < 0.0f) progress = 0.0f;
if (progress > 1.0f) progress = 1.0f;
int lit;
if (!wasRunningWhenHoldStarted) lit = (int)floor(progress * R2_LEDS + 0.0001f);
else { lit = (int)ceil((1.0f - progress) * R2_LEDS - 0.0001f); if (lit < 0) lit = 0; }
if (lit > R2_LEDS) lit = R2_LEDS;
ring2.clear();
for (int i = 0; i < lit && i < ring2.numPixels(); ++i)
ring2.setPixelColor(i, ring2.Color(255,215,0));
ring2.show();
if (lit != prevRing2Lit) {
int toneIndex = -1;
if (!wasRunningWhenHoldStarted) {
if (lit > prevRing2Lit) toneIndex = lit - 1;
} else {
if (lit < prevRing2Lit) toneIndex = lit;
}
if (toneIndex >= 0) {
int freq = FREQ_LOW + toneIndex * FREQ_STEP;
shortBeepNonBlock(freq, STEP_TONE_MS);
}
prevRing2Lit = lit;
}
}
void tftSetStatus(const char* status) {
strncpy(tftStatus, status, sizeof(tftStatus) - 1);
tftStatus[sizeof(tftStatus) - 1] = '\0';
}
void drawRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t color) {
tft.drawRoundRect(x, y, w, h, r, color);
}
void fillRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t color) {
tft.fillRoundRect(x, y, w, h, r, color);
}
void drawGradientBar(int16_t x, int16_t y, int16_t w, int16_t h, int16_t progress, int16_t maxVal, uint16_t color1, uint16_t color2) {
fillRoundRect(x, y, w, h, 3, COLOR_PANEL);
drawRoundRect(x, y, w, h, 3, COLOR_TEXT_DIM);
if (maxVal > 0 && progress > 0) {
int16_t fillW = (w * progress) / maxVal;
if (fillW > w-2) fillW = w-2;
if (fillW > 0) {
fillRoundRect(x+1, y+1, fillW, h-2, 2, color1);
}
}
}
void drawStatusPanel(const char* title, const char* value, uint16_t color, int16_t y) {
fillRoundRect(5, y, 310, 24, 5, COLOR_PANEL);
drawRoundRect(5, y, 310, 24, 5, color);
tft.setTextColor(COLOR_TEXT_DIM);
tft.setTextSize(1);
tft.setCursor(10, y + 4);
tft.print(title);
tft.setTextColor(color);
tft.setTextSize(1);
tft.setCursor(10, y + 14);
tft.print(value);
}
void drawLoadingDots(int16_t x, int16_t y, unsigned long time, uint16_t color) {
int dots = ((time / 300) % 4);
tft.fillRect(x, y, 30, 8, COLOR_PANEL);
tft.setTextColor(color);
tft.setCursor(x, y);
for(int i = 0; i < dots; i++) {
tft.print(".");
}
}
// ----------------- Erweiterte TFT Anzeige -----------------
void tftDraw() {
if (!tftPresent) return;
unsigned long now = millis();
if (forceRedraw) {
tft.fillScreen(COLOR_BG);
forceRedraw = false;
}
if (!systemEnabled) {
static bool greetingDrawn = false;
greetingDrawn = false;
tft.fillScreen(COLOR_BG);
fillRoundRect(20, 60, 280, 120, 10, COLOR_PANEL);
drawRoundRect(20, 60, 280, 120, 10, COLOR_WARNING);
tft.drawCircle(160, 100, 15, COLOR_WARNING);
tft.drawLine(160, 85, 160, 105, COLOR_WARNING);
tft.drawLine(155, 110, 165, 110, COLOR_WARNING);
tft.setTextColor(COLOR_TEXT);
tft.setTextSize(2);
tft.setCursor(90, 130);
tft.print("System");
tft.setCursor(70, 150);
tft.print("einschalten!");
return;
}
if (!greetingAck) {
static bool greetingDrawn = false;
if (forceRedraw || lastSystemEnabled != systemEnabled || !greetingDrawn) {
tft.fillScreen(COLOR_BG);
fillRoundRect(10, 10, 300, 50, 8, COLOR_PANEL);
drawRoundRect(10, 10, 300, 50, 8, COLOR_ACCENT);
tft.setTextColor(COLOR_ACCENT);
tft.setTextSize(2);
tft.setCursor(25, 20);
tft.print("HNGR13");
tft.setTextSize(1);
tft.setCursor(25, 40);
tft.print("Bomb Timer System v2.0");
fillRoundRect(10, 80, 300, 40, 8, COLOR_PANEL);
drawRoundRect(10, 80, 300, 40, 8, COLOR_INFO);
tft.setTextColor(COLOR_TEXT);
tft.setTextSize(1);
tft.setCursor(20, 90);
tft.print("Bereit zum Start");
tft.setTextColor(COLOR_INFO);
tft.setCursor(20, 105);
tft.print(">> START druecken zum Fortfahren");
greetingDrawn = true;
}
drawLoadingDots(250, 105, now, COLOR_INFO);
return;
}
if (exploded) {
unsigned long explosionTime = now - explosionStart;
bool fastBlink = explosionTime < 2000;
int blinkSpeed = fastBlink ? 150 : 400;
bool flash = ((explosionTime / blinkSpeed) % 2) == 0;
tft.fillScreen(flash ? COLOR_DANGER : ILI9341_BLACK);
if (flash) {
tft.drawRect(0, 0, 320, 240, COLOR_TEXT);
tft.drawRect(2, 2, 316, 236, COLOR_TEXT);
tft.setTextColor(COLOR_TEXT);
tft.setTextSize(4);
tft.setCursor(70, 70);
tft.print("BOOM!");
tft.setTextSize(2);
tft.setCursor(50, 120);
tft.print("EXPLOSION!");
tft.setTextSize(1);
tft.setCursor(75, 160);
tft.print("Bombe explodiert!");
tft.drawCircle(160, 100, 60, COLOR_WARNING);
tft.drawCircle(160, 100, 80, COLOR_TEXT);
}
if (explosionTime < 3000) {
for (int i = 0; i < ring1.numPixels(); ++i) {
int colorChoice = (i + (explosionTime / 80)) % 5;
uint32_t color;
switch (colorChoice) {
case 0: color = ring1.Color(255, 0, 0); break;
case 1: color = ring1.Color(255, 80, 0); break;
case 2: color = ring1.Color(255, 255, 0); break;
case 3: color = ring1.Color(255, 255, 255); break;
case 4: color = ring1.Color(255, 50, 150); break;
}
ring1.setPixelColor(i, color);
}
ring1.show();
int rotPhase = (explosionTime / 40) % ring2.numPixels();
ring2.clear();
for (int i = 0; i < 8; i++) {
int pos = (rotPhase + i * 2) % ring2.numPixels();
uint32_t brightness = 255 - (i * 25);
ring2.setPixelColor(pos, ring2.Color(brightness, brightness/3, 0));
}
ring2.show();
} else {
bool pulse = ((explosionTime / 300) % 2) == 0;
uint32_t pulseColor = pulse ? ring1.Color(255, 100, 0) : ring1.Color(80, 30, 0);
for (int i = 0; i < ring1.numPixels(); ++i) {
ring1.setPixelColor(i, pulseColor);
}
ring1.show();
int wavePos = (explosionTime / 120) % ring2.numPixels();
ring2.clear();
for (int i = 0; i < 6; i++) {
int pos = (wavePos + i) % ring2.numPixels();
uint32_t brightness = 200 - (i * 30);
ring2.setPixelColor(pos, ring2.Color(brightness, 0, 0));
}
ring2.show();
}
return;
}
static char lastStatus[40] = "";
if (strcmp(tftStatus, lastStatus) != 0 || forceRedraw) {
fillRoundRect(5, 5, 310, 20, 4, COLOR_PANEL);
drawRoundRect(5, 5, 310, 20, 4, running ? COLOR_SUCCESS : COLOR_INFO);
tft.setTextColor(running ? COLOR_SUCCESS : COLOR_INFO);
tft.setTextSize(1);
tft.setCursor(10, 12);
tft.print("Status: ");
tft.print(tftStatus);
strcpy(lastStatus, tftStatus);
}
static int lastLedBarW = -1;
static int lastDisplayedLedsRemaining = -1;
static int lastDisplayedTotalLeds = -1;
int ledBarW = 0;
if (totalLeds > 0) ledBarW = (296 * ledsRemaining) / totalLeds;
if (ledBarW < 0) ledBarW = 0;
if (ledBarW > 296) ledBarW = 296;
if (ledBarW != lastLedBarW || ledsRemaining != lastDisplayedLedsRemaining || totalLeds != lastDisplayedTotalLeds || forceRedraw) {
uint16_t ledColor = COLOR_SUCCESS;
if (ledsRemaining <= totalLeds * 0.3) ledColor = COLOR_DANGER;
else if (ledsRemaining <= totalLeds * 0.6) ledColor = COLOR_WARNING;
drawGradientBar(12, 35, 296, 20, ledsRemaining, totalLeds, ledColor, ledColor);
tft.fillRect(12, 28, 100, 6, COLOR_BG);
tft.setTextColor(COLOR_TEXT_DIM);
tft.setTextSize(1);
tft.setCursor(12, 28);
tft.print("LEDs");
char ledText[20];
snprintf(ledText, sizeof(ledText), "%02d/%02d", ledsRemaining, totalLeds);
tft.fillRect(250, 28, 60, 6, COLOR_BG);
tft.setTextColor(ledColor);
tft.setCursor(260, 28);
tft.print(ledText);
lastLedBarW = ledBarW;
lastDisplayedLedsRemaining = ledsRemaining;
lastDisplayedTotalLeds = totalLeds;
}
static int lastTimeBarW = -1;
int secondsRemain = timeValue / 100;
if (secondsRemain < 0) secondsRemain = 0;
if (secondsRemain > 60) secondsRemain = 60;
int timeBarW = (296 * secondsRemain) / 60;
if (timeBarW != lastTimeBarW || forceRedraw) {
uint16_t timeColor = COLOR_INFO;
if (secondsRemain <= 15) timeColor = COLOR_DANGER;
else if (secondsRemain <= 30) timeColor = COLOR_WARNING;
drawGradientBar(12, 70, 296, 20, secondsRemain, 60, timeColor, timeColor);
tft.fillRect(12, 63, 100, 6, COLOR_BG);
tft.setTextColor(COLOR_TEXT_DIM);
tft.setTextSize(1);
tft.setCursor(12, 63);
tft.print("Zeit pro Segment");
char timeText[20];
snprintf(timeText, sizeof(timeText), "%02ds", secondsRemain);
tft.fillRect(270, 63, 40, 6, COLOR_BG);
tft.setTextColor(timeColor);
tft.setCursor(270, 63);
tft.print(timeText);
lastTimeBarW = timeBarW;
}
fillRoundRect(50, 110, 220, 60, 10, COLOR_PANEL);
if (running) {
drawRoundRect(50, 110, 220, 60, 10, secondsRemain <= 10 ? COLOR_DANGER : COLOR_SUCCESS);
tft.setTextColor(secondsRemain <= 10 ? COLOR_DANGER : COLOR_SUCCESS);
tft.setTextSize(2);
tft.setCursor(90, 125);
tft.print("AKTIV");
tft.setTextSize(1);
tft.setCursor(70, 150);
tft.print("Timer laeuft");
} else {
drawRoundRect(50, 110, 220, 60, 10, COLOR_WARNING);
tft.setTextColor(COLOR_WARNING);
tft.setTextSize(2);
tft.setCursor(70, 125);
if (!hasStarted) {
tft.print("KONFIGURATION");
} else {
tft.print("PAUSIERT");
}
tft.setTextSize(1);
tft.setCursor(80, 150);
if (!hasStarted) {
tft.print("UP/DOWN: LEDs einstellen");
} else {
tft.print("START halten zum Fortfahren");
}
}
static bool footerDrawn = false;
if (!footerDrawn || forceRedraw) {
tft.fillRect(5, 190, 310, 25, COLOR_BG);
tft.setTextColor(COLOR_TEXT_DIM);
tft.setTextSize(1);
tft.setCursor(10, 190);
if (!hasStarted) {
tft.print("UP/DOWN: LEDs | START: Begin Timer");
} else {
tft.print("START (5s halten): Start/Stop Toggle");
}
footerDrawn = true;
}
}
// ----------------- Setup -----------------
void setup() {
Serial.begin(115200);
pinMode(BTN_START, INPUT_PULLUP);
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_DOWN, INPUT_PULLUP);
pinMode(RELAY1, OUTPUT);
pinMode(RELAY2, OUTPUT);
pinMode(BUZZER, OUTPUT);
pinMode(LED_BTNS, OUTPUT);
pinMode(MAIN_SW, INPUT_PULLUP);
digitalWrite(RELAY1, LOW);
digitalWrite(RELAY2, LOW);
digitalWrite(LED_BTNS, LOW);
tmDisplay.setBrightness(7);
showTimeValueOnSeg(timeValue, false, true);
ring1.begin(); ring1.show();
ring2.begin(); ring2.show();
fillRing1(ledsRemaining, ledsRemaining - 1, true);
tft.begin();
tft.setRotation(1);
tft.fillScreen(COLOR_BG);
forceRedraw = true;
tftDraw();
lastTftUpdate = millis();
Serial.println("HNGR13 Enhanced UI with Pink Panther ready");
}
// ----------------- Loop -----------------
void loop() {
unsigned long now = millis();
// Pink Panther Melodie Update (non-blocking)
updatePinkPantherMelody();
// Hauptschalter
bool currentSystemEnabled = (digitalRead(MAIN_SW) == LOW);
if (currentSystemEnabled != systemEnabled) {
forceRedraw = true;
if (!currentSystemEnabled) {
// System wird ausgeschaltet - Melodie stoppen
stopPinkPantherMelody();
}
}
systemEnabled = currentSystemEnabled;
lastSystemEnabled = systemEnabled;
if (!systemEnabled) {
tmDisplay.clear();
ring1.clear(); ring1.show();
ring2.clear(); ring2.show();
digitalWrite(LED_BTNS, LOW);
running = false;
hasStarted = false;
greetingAck = false;
exploded = false;
if (tftPresent && now - lastTftUpdate >= TFT_UPDATE_MS) {
tftDraw();
lastTftUpdate = now;
}
delay(50);
return;
}
// Begrüßung mit Pink Panther Melodie
if (systemEnabled && !greetingAck) {
// Melodie starten wenn noch nicht aktiv
if (!melodyPlaying) {
startPinkPantherMelody();
}
if (digitalRead(BTN_START) == LOW) {
greetingAck = true;
stopPinkPantherMelody(); // Melodie stoppen wenn START gedrückt
static bool greetingDrawn = false;
greetingDrawn = false;
forceRedraw = true;
tftSetStatus("Konfig: LEDs einstellen");
tftDraw();
lastTftUpdate = millis();
return;
}
if (tftPresent && now - lastTftUpdate >= TFT_UPDATE_MS) {
tftDraw();
lastTftUpdate = now;
}
return;
}
// Nach Begrüßung: Melodie sicher stoppen falls noch aktiv
if (greetingAck && melodyPlaying) {
stopPinkPantherMelody();
}
// -------------------- Non-blocking Explosion Handler --------------------
if (exploded) {
unsigned long nowE = now;
if (!explosionToneOn && nowE - explosionStart < EXPLOSION_BUZZ_MS) {
tone(BUZZER, 400);
explosionToneOn = true;
}
if (explosionToneOn && nowE - explosionStart >= EXPLOSION_BUZZ_MS) {
noTone(BUZZER);
explosionToneOn = false;
digitalWrite(RELAY1, HIGH);
relay1OnTime = nowE;
}
if (relay1OnTime != 0 && nowE - relay1OnTime >= RELAY1_PULSE_MS) {
digitalWrite(RELAY1, LOW);
exploded = false;
hasStarted = false;
running = false;
ledsRemaining = totalLeds;
timeValue = SEGMENT_HUNDREDTHS;
showTimeValueOnSeg(timeValue, true, true);
tftSetStatus("Bereit");
fillRing1(ledsRemaining, ledsRemaining - 1, true);
clearRing2();
forceRedraw = true;
}
if (tftPresent && now - lastTftUpdate >= TFT_UPDATE_MS) {
tftDraw();
lastTftUpdate = now;
}
}
int startRaw = digitalRead(BTN_START);
if (!hasStarted) digitalWrite(LED_BTNS, HIGH);
else digitalWrite(LED_BTNS, LOW);
// --- Hold / Start-Stop logic ---
if (startRaw == LOW) {
if (holdStart == 0) {
holdStart = now;
actionExecutedWhileHeld = false;
wasRunningAtHoldStart = running;
prevRing2Lit = wasRunningAtHoldStart ? R2_LEDS : 0;
if (!hasStarted) {
running = true; hasStarted = true; digitalWrite(RELAY2, HIGH);
previousMillis = now; lastSecondBeepSec = timeValue / 100;
beepBlocking(1500,200); delay(70); beepBlocking(1500,200); delay(70); beepBlocking(1500,200); delay(260);
tftSetStatus("Timer gestartet");
holdStart = 1; actionExecutedWhileHeld = true; clearRing2();
return;
}
if (wasRunningAtHoldStart) { pausedForHold = true; running = false; tftSetStatus("Pause (5s halten)"); }
else { pausedForHold = false; tftSetStatus("Start (5s halten)"); }
ring2.clear();
if (wasRunningAtHoldStart) for (int i=0;i<R2_LEDS;i++) ring2.setPixelColor(i, ring2.Color(150,0,0));
ring2.show();
}
if (!actionExecutedWhileHeld) {
unsigned long held = now - holdStart;
float progress = (float)held / (float)HOLD_MS;
if (progress > 1.0f) progress = 1.0f;
updateRing2DuringHold(progress, wasRunningAtHoldStart);
if (progress >= 1.0f) {
if (!wasRunningAtHoldStart) {
running = true; hasStarted = true; digitalWrite(RELAY2, HIGH);
previousMillis = now; lastSecondBeepSec = timeValue / 100; tftSetStatus("Timer laeuft");
} else {
running = false; digitalWrite(RELAY2, LOW); tftSetStatus("Timer gestoppt");
}
beepBlocking(1000, CONFIRM_BEEP_MS);
actionExecutedWhileHeld = true; clearRing2(); pausedForHold = false;
}
}
} else {
if (holdStart != 0) {
if (!actionExecutedWhileHeld && pausedForHold) {
running = true; previousMillis = now; lastSecondBeepSec = timeValue / 100; pausedForHold = false;
tftSetStatus("Timer fortgesetzt");
}
holdStart = 0; actionExecutedWhileHeld = false; prevRing2Lit = -1; clearRing2();
}
}
// --- UP / DOWN (nur vor erstem Start, mit Entprellung) ---
if (!hasStarted) {
if (digitalRead(BTN_UP) == LOW && now - lastUpPress >= BTN_DEBOUNCE_MS) {
if (totalLeds < 16) totalLeds++;
ledsRemaining = totalLeds;
shortBeepNonBlock(800,80);
fillRing1(ledsRemaining, ledsRemaining - 1, true);
tftSetStatus(("LEDs: " + String(totalLeds)).c_str());
lastUpPress = now;
}
if (digitalRead(BTN_DOWN) == LOW && now - lastDownPress >= BTN_DEBOUNCE_MS) {
if (totalLeds > 1) totalLeds--;
ledsRemaining = totalLeds;
shortBeepNonBlock(600,80);
fillRing1(ledsRemaining, ledsRemaining - 1, true);
tftSetStatus(("LEDs: " + String(totalLeds)).c_str());
lastDownPress = now;
}
}
// --- Timer in 10ms Quanten ---
if (running) {
unsigned long now2 = millis();
if (previousMillis == 0) { previousMillis = now2; lastSecondBeepSec = timeValue / 100; }
unsigned long elapsed = now2 - previousMillis;
if (elapsed >= 10) {
unsigned long steps = elapsed / 10;
int dec = (int)steps;
timeValue -= dec;
previousMillis += steps * 10UL;
while (timeValue <= 0) {
timeValue += SEGMENT_HUNDREDTHS;
ledsRemaining--;
if (ledsRemaining < 0) ledsRemaining = 0;
fillRing1(ledsRemaining, ledsRemaining - 1, true);
if (ledsRemaining <= 0) {
running = false;
digitalWrite(RELAY2, LOW);
tftSetStatus("BOOM! Bombe explodiert!");
exploded = true;
explosionStart = millis();
explosionToneOn = false;
relay1OnTime = 0;
timeValue = SEGMENT_HUNDREDTHS;
showTimeValueOnSeg(timeValue, true, true);
forceRedraw = true;
break;
}
}
showTimeValueOnSeg(timeValue, true, colonOn);
} else showTimeValueOnSeg(timeValue, true, colonOn);
int currentSec = timeValue / 100;
if (currentSec != lastSecondBeepSec) {
lastSecondBeepSec = currentSec;
shortBeepNonBlock(2000, 80);
colonOn = !colonOn;
}
} else {
if (now - displayBlinkMillis >= 500) {
displayBlinkMillis = now; dispBlinkState = !dispBlinkState;
}
if (dispBlinkState) {
if (!hasStarted) tmDisplay.showNumberDecEx(SEGMENT_HUNDREDTHS, 0b01000000, true);
else showTimeValueOnSeg(timeValue, true, true);
} else tmDisplay.clear();
}
// --- Ring1 blink (2Hz next LED) ---
if (running) {
if (now - ringBlinkMillis >= 500) {
ringBlinkMillis = now;
static bool ringBlinkState = true; ringBlinkState = !ringBlinkState;
fillRing1(ledsRemaining, ledsRemaining - 1, ringBlinkState);
}
} else fillRing1(ledsRemaining, ledsRemaining - 1, true);
// --- TFT: häufigere Updates für flüssigere Animationen ---
bool needRedraw = false;
if (now - lastTftUpdate >= TFT_UPDATE_MS) needRedraw = true;
if (ledsRemaining != lastLedsRemaining || totalLeds != lastTotalLeds) needRedraw = true;
if (needRedraw && tftPresent) {
tftDraw();
lastTftUpdate = now;
lastLedsRemaining = ledsRemaining;
lastTotalLeds = totalLeds;
}
}
1000 µF Kondensator
1000 µF Kondensator
Beleuchtung up/down
Hauptschalter