/***************************************************************
HNGR13 Bomb Timer - ILI9341 TFT (Querformat) + TM1637 + NeoPixel
- Vollständig kommentierte Version (DEUTSCHE Erklärungen)
- ACHTUNG: Funktionalität wurde NICHT verändert — nur Kommentare ergänzt
- Features (kurz):
* Begrüßungsbildschirm mit Pink Panther Melodie (non-blocking)
* Konfigurationsmodus (UP/DOWN: Anzahl LEDs)
* START: beginnt Timer (bei erstem Start)
* START 5s halten: togglet Start/Stop (Hold-Animation Ring2)
* Visuelle UI auf TFT + 7-Segment + 2 NeoPixel-Ringe
* Explosion/Ende-Animation mit Relais/Buzzer
***************************************************************/
#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <TM1637Display.h>
#include <Adafruit_NeoPixel.h>
// ----------------- PIN-Definition -----------------
// Hardware-Pin-Zuordnung (ändere nur wenn du die Hardware umverdrahten willst)
#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 (low = eingeschaltet)
// 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 -----------------
// Frequenzen für Noten (Standard-Werte)
// REST = 0 signalisiert Pausen
#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 (Array im Format {note, divider, note, divider, ...})
// divider: positiv -> normale Dauer, negativ -> punktierte Dauer (1.5x)
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,
};
// pinkPantherNotes berechnet wie viele Paarungen (note+divider) das Array hat:
int pinkPantherNotes = sizeof(pinkPantherMelody) / sizeof(pinkPantherMelody[0]) / 2;
// Melodie-Playback State (non-blocking): Variablen, die den Fortschritt steuern
bool melodyPlaying = false;
int melodyNoteIndex = 0;
unsigned long melodyNoteStart = 0;
int melodyNoteDuration = 0;
// ----------------- Hardware-Objekte -----------------
// Objekte, die Bibliotheken instanziieren (TM1637, NeoPixel, TFT)
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 -----------------
// 16-bit Farben im Format 565 (da Adafruit_ILI9341 16-bit erwartet)
// Du kannst hier Werte anpassen, wenn du andere UI-Farben möchtest.
#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 -----------------
// Parameter, die das zeitliche und visuelle Verhalten steuern.
// Wenn du "mehr Zeit" pro Segment willst: SEGMENT_HUNDREDTHS verändern.
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 pro Segment (Ein Segment = 6000 Hundertstelsekunden)
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 -----------------
// Laufzeit-Variablen: speichern aktuellen Zustand des Systems
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 -----------------
// Startet die Melodie-Playback (non-blocking)
void startPinkPantherMelody() {
melodyPlaying = true;
melodyNoteIndex = 0;
melodyNoteStart = millis();
melodyNoteDuration = 0;
}
// Stoppt die Melodie komplett (stellt sicher, dass Ton aus ist)
void stopPinkPantherMelody() {
melodyPlaying = false;
noTone(BUZZER);
}
// updatePinkPantherMelody() ist *non-blocking* und muss regelmäßig
// aus loop() aufgerufen werden. Es prüft, ob die aktuelle Note abgelaufen ist
// und beginnt ggf. die nächste Note.
// Wichtig: tone(BUZZER, freq, duration) wird verwendet (dauer ist optional)
// und das macht die Wiedergabe nicht-blockierend (ton wird vom AVR/HW gesteuert).
void updatePinkPantherMelody() {
if (!melodyPlaying) return;
unsigned long now = millis();
// Wenn aktuelle Note noch spielt, nichts tun
if (now - melodyNoteStart < melodyNoteDuration) {
return;
}
// Wenn am Ende, wieder von vorne
if (melodyNoteIndex >= pinkPantherNotes * 2) {
melodyNoteIndex = 0;
}
// Berechne "wholenote" aus Tempo (MS für eine ganze Note)
int wholenote = (60000 * 4) / pinkPantherTempo;
int divider = pinkPantherMelody[melodyNoteIndex + 1];
if (divider > 0) {
melodyNoteDuration = wholenote / divider;
} else if (divider < 0) {
// negative Divider -> punktierte Note (1.5x)
melodyNoteDuration = wholenote / abs(divider);
melodyNoteDuration *= 1.5;
}
int note = pinkPantherMelody[melodyNoteIndex];
if (note != REST) {
// tone ist non-blocking (dauer in ms kann als dritte übergabe spezifiziert werden)
tone(BUZZER, note, melodyNoteDuration * 0.9);
} else {
noTone(BUZZER);
}
melodyNoteStart = now;
melodyNoteIndex += 2;
}
// ----------------- Hilfsfunktionen -----------------
// beepBlocking: Blockierender Piepton (nutze sparsam, verhindert andere Tasks)
void beepBlocking(int freq, int dur) {
tone(BUZZER, freq, dur);
delay(dur);
noTone(BUZZER);
}
// shortBeepNonBlock: Erzeugt einen nicht-blockierenden Piepton (ton für dur ms)
// Hinweis: Wird nicht automatisch gestoppt, wenn eine neue tone()-Aufruf kommt.
// In AVR/Arduino führt tone(freq,dur) selbständig zum Stop nach dur ms.
void shortBeepNonBlock(int freq, int dur) {
tone(BUZZER, freq, dur);
}
// fillRing1: füllt ring1 bis 'count' Pixel; blinkIndex optional für Hervorhebung
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();
}
// pulseRelay: schaltet Relais für durationMs (blockierend)
// ACHTUNG: blockierender delay benutzt -> kurze Zeiten okay
void pulseRelay(int pin, int durationMs) {
digitalWrite(pin, HIGH);
delay(durationMs);
digitalWrite(pin, LOW);
}
// showTimeValueOnSeg: Zeigt numerischen Wert auf TM1637 an
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);
}
// clearRing2: Nullt Ring2
void clearRing2() {
ring2.clear();
ring2.show();
}
// updateRing2DuringHold: Graphische Hold-Anzeige mit Tönen
// progress: 0.0 - 1.0, wasRunningWhenHoldStarted entscheidet Richtung
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();
// Wenn sich die Anzahl der leuchtenden LEDs ändert, spiele Ton (Feedback)
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;
}
}
// tftSetStatus: einfache Helferfunktion, um den Status-String zu setzen
void tftSetStatus(const char* status) {
strncpy(tftStatus, status, sizeof(tftStatus) - 1);
tftStatus[sizeof(tftStatus) - 1] = '\0';
}
// Kleine Wrapper für TFT-Primitive (nur um Lesbarkeit zu verbessern)
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);
}
// drawGradientBar: zeichnet eine simple âFortschrittsleisteâ mit Panel-Hintergrund
// progress: aktuell (z.B. Sekunden oder Anzahl LEDs), maxVal: maximaler Wert
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);
}
}
}
// drawStatusPanel: zeichnet ein kleines Panel mit Titel & Wert (wird aktuell nicht
// überall verwendet, ist aber nützlich für Erweiterungen)
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);
}
// drawLoadingDots: kleine animierte "..." Anzeige (used on greetingscreen)
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 -----------------
// Kernfunktion, die den gesamten TFT-Inhalt zeichnet. Sie macht intelligente
// Checks, um nur zu zeichnen, wenn wirklich nötig (forceRedraw, Statusänderung usw.)
void tftDraw() {
if (!tftPresent) return;
unsigned long now = millis();
if (forceRedraw) {
tft.fillScreen(COLOR_BG);
forceRedraw = false;
}
// Wenn System ausgeschaltet: zeige Aufforderung zum einschalten
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;
}
// Begrüßungs-/Start-ACK Phase (nach Einschalten, vor dem ersten START)
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;
}
// kleine animierte Punkte rechts
drawLoadingDots(250, 105, now, COLOR_INFO);
return;
}
// Explosion-Zustand: spezielles blinken & NeoPixel-Show
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);
}
// Animationssequenz auf NeoPixels (zuerst wild, danach pulsierend)
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;
}
// Normaler Betriebsbildschirm: Statuszeile oben
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);
}
// LED Fortschritts-Balken (zeigt verbleibende Segmente)
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;
}
// Zeit pro Segment Balken (60s Skala)
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;
}
// Mittleres Panel: großer Status (AKTIV / KONFIGURATION / PAUSIERT)
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");
}
}
// Footer-Hinweise
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);
// Tasten mit internen Pullups
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);
// Relais & LEDs initial LOW (aus)
digitalWrite(RELAY1, LOW);
digitalWrite(RELAY2, LOW);
digitalWrite(LED_BTNS, LOW);
// 7-Segment Display konfigurieren
tmDisplay.setBrightness(7);
showTimeValueOnSeg(timeValue, false, true);
// NeoPixel Ringe initialisieren
ring1.begin(); ring1.show();
ring2.begin(); ring2.show();
fillRing1(ledsRemaining, ledsRemaining - 1, true);
// TFT starten & initial zeichnen
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();
// 1) Pink Panther Melodie Update (non-blocking)
updatePinkPantherMelody();
// 2) Hauptschalter lesen und ggf. Zustand wechseln
bool currentSystemEnabled = (digitalRead(MAIN_SW) == LOW);
if (currentSystemEnabled != systemEnabled) {
// Systemzustand hat sich geändert -> erzwinge redraw
forceRedraw = true;
if (!currentSystemEnabled) {
// System wird ausgeschaltet - Melodie stoppen
stopPinkPantherMelody();
}
}
systemEnabled = currentSystemEnabled;
lastSystemEnabled = systemEnabled;
// Wenn System ausgeschaltet, minimalen Idle-Zyklus laufen lassen
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;
}
// 3) Begrüßung mit Pink Panther Melodie (vor dem ersten START)
if (systemEnabled && !greetingAck) {
// Starte Melodie, falls nicht bereits spielend
if (!melodyPlaying) {
startPinkPantherMelody();
}
// START-Taste bestätigt Begrüßung und wechselt in Konfigurationsmodus
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;
}
// Wenn Begrüßung bestätigt wurde, stelle sicher, dass die Melodie aus ist
if (greetingAck && melodyPlaying) {
stopPinkPantherMelody();
}
// -------------------- Non-blocking Explosion Handler --------------------
// Diese Sektion behandelt den Ablauf nach "exploded=true" ohne blocking.
if (exploded) {
unsigned long nowE = now;
// Kurzer Explosionston, danach Relais-Ansteuerung als Impuls
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) {
// Relais aus -> zurücksetzen auf Bereit
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;
}
// TFT weiterhin animieren
if (tftPresent && now - lastTftUpdate >= TFT_UPDATE_MS) {
tftDraw();
lastTftUpdate = now;
}
}
// 4) START-Button Rohzustand lesen
int startRaw = digitalRead(BTN_START);
// LED_BTNS zeigt an, ob noch in Konfiguration (an) oder bereits gestartet (aus)
if (!hasStarted) digitalWrite(LED_BTNS, HIGH);
else digitalWrite(LED_BTNS, LOW);
// --- Hold / Start-Stop logic ---
// Das Verhalten:
// * Beim ersten kurzen Drücken von START (wenn !hasStarted): sofortiger Start,
// es wird ein kurzes akustisches Signal gespielt (3x Piepen) und running=true
// * Bei anschließendem Halten (HOLD_MS) toggelt START -> stoppt/startet den Timer
if (startRaw == LOW) {
if (holdStart == 0) {
// Anfang des Halts (erste Erkennung)
holdStart = now;
actionExecutedWhileHeld = false;
wasRunningAtHoldStart = running;
prevRing2Lit = wasRunningAtHoldStart ? R2_LEDS : 0;
if (!hasStarted) {
// Sofortiger Start beim ersten Drücken (kurzer Tap startet sofort)
running = true; hasStarted = true; digitalWrite(RELAY2, HIGH);
previousMillis = now; lastSecondBeepSec = timeValue / 100;
// Akustisches Bestätigungs-Triple (blockierend, bewusst kurz)
beepBlocking(1500,200); delay(70); beepBlocking(1500,200); delay(70); beepBlocking(1500,200); delay(260);
tftSetStatus("Timer gestartet");
holdStart = 1; actionExecutedWhileHeld = true; clearRing2();
return;
}
// Wenn bereits gestartet: wechsle in Pause-Vorbereitung (visuelles Feedback)
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();
}
// Wenn Taste gehalten, zeige Fortschritt und spiele Töne bei Fortschrittsschritten
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) {
// Hold abgeschlossen -> Toggle Start/Stop
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 {
// Taste losgelassen: wenn wir nicht den Hold-Action ausgeführt haben, und pausedForHold true,
// wird der Timer wieder fortgesetzt (es war nur ein kurzer Druck).
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) ---
// Hier wird die Anzahl der Segmente (LEDs) konfiguriert
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 ---
// Timer arbeitet intern mit Hundertstelsekunden (SEGMENT_HUNDREDTHS)
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;
// Wenn ein Segment (timeValue) aufgebraucht ist -> nächstes Segment
while (timeValue <= 0) {
timeValue += SEGMENT_HUNDREDTHS;
ledsRemaining--;
if (ledsRemaining < 0) ledsRemaining = 0;
fillRing1(ledsRemaining, ledsRemaining - 1, true);
if (ledsRemaining <= 0) {
// Endzustand: BOOM! -> Explosion auslösen
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;
}
}
// Update 7-Segment-Anzeige (zeigt timeValue in Hundertstelsekunden)
showTimeValueOnSeg(timeValue, true, colonOn);
} else showTimeValueOnSeg(timeValue, true, colonOn);
// Wenn sich die volle Sekunde ändert, spiele kurzen Ton -> akustisches Tick
int currentSec = timeValue / 100;
if (currentSec != lastSecondBeepSec) {
lastSecondBeepSec = currentSec;
shortBeepNonBlock(2000, 80);
colonOn = !colonOn;
}
} else {
// Wenn nicht laufend: 7-Segment blinken
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