/***************************************************************
HNGR13 Bomb Timer - ILI9341 TFT (Querformat) + TM1637 + NeoPixel
- 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
- 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
// ----------------- 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; // Häufigere Updates für flüssigere Animation
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; // MAIN_SW
// Simulator-Optimierungen
bool forceRedraw = false;
bool lastSystemEnabled = false;
// Debounce für UP/DOWN
unsigned long lastUpPress = 0;
unsigned long lastDownPress = 0;
// Mode flow
bool greetingAck = false;
// Animation states
unsigned long animStartTime = 0;
bool animActive = false;
// ----------------- Explosion (non-blocking) -----------------
bool exploded = false;
unsigned long explosionStart = 0;
bool explosionToneOn = false;
unsigned long relay1OnTime = 0;
const unsigned long EXPLOSION_BUZZ_MS = 2000UL; // ursprünglich 2000ms
const unsigned long RELAY1_PULSE_MS = 7000UL; // ursprünglich 7000ms
// ----------------- 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;
}
}
// TFT Status setzen
void tftSetStatus(const char* status) {
strncpy(tftStatus, status, sizeof(tftStatus) - 1);
tftStatus[sizeof(tftStatus) - 1] = '\0';
}
// Grafische Hilfsfunktionen
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) {
// Hintergrund
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) {
// Panel-Hintergrund
fillRoundRect(5, y, 310, 24, 5, COLOR_PANEL);
drawRoundRect(5, y, 310, 24, 5, color);
// Titel
tft.setTextColor(COLOR_TEXT_DIM);
tft.setTextSize(1);
tft.setCursor(10, y + 4);
tft.print(title);
// Wert
tft.setTextColor(color);
tft.setTextSize(1);
tft.setCursor(10, y + 14);
tft.print(value);
}
// Animierte Punkte für "Lade"-Effekt
void drawLoadingDots(int16_t x, int16_t y, unsigned long time, uint16_t color) {
int dots = ((time / 300) % 4);
// Kleinen Bereich für die Punkte löschen (ohne Panel-Hintergrund zu zerstören)
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) {
// greetingDrawn Flag zurücksetzen wenn System ausgeschaltet
static bool greetingDrawn = false;
greetingDrawn = false;
tft.fillScreen(COLOR_BG);
// Zentriertes Panel für Einschalt-Nachricht
fillRoundRect(20, 60, 280, 120, 10, COLOR_PANEL);
drawRoundRect(20, 60, 280, 120, 10, COLOR_WARNING);
// Icon (vereinfacht - Kreis mit Linie)
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) {
// Statische Elemente nur einmal oder bei forceRedraw zeichnen
static bool greetingDrawn = false;
if (forceRedraw || lastSystemEnabled != systemEnabled || !greetingDrawn) {
tft.fillScreen(COLOR_BG);
// Header mit Titel
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");
// Status Panel
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;
}
// Animierte Punkte separat aktualisieren
drawLoadingDots(250, 105, now, COLOR_INFO);
return;
}
// --- EXPLOSION SCREEN - nur minimale Änderungen für bessere Optik ---
if (exploded) {
unsigned long explosionTime = now - explosionStart;
// Blinkender Hintergrund (schneller in den ersten 2 Sekunden)
bool fastBlink = explosionTime < 2000;
int blinkSpeed = fastBlink ? 150 : 400;
bool flash = ((explosionTime / blinkSpeed) % 2) == 0;
tft.fillScreen(flash ? COLOR_DANGER : ILI9341_BLACK);
if (flash) {
// Warnrahmen um den Bildschirm
tft.drawRect(0, 0, 320, 240, COLOR_TEXT);
tft.drawRect(2, 2, 316, 236, COLOR_TEXT);
// Großer BOOM Text
tft.setTextColor(COLOR_TEXT);
tft.setTextSize(4);
tft.setCursor(70, 70);
tft.print("BOOM!");
// Explosion Text
tft.setTextSize(2);
tft.setCursor(50, 120);
tft.print("EXPLOSION!");
// Info Text
tft.setTextSize(1);
tft.setCursor(75, 160);
tft.print("Bombe explodiert!");
// Explosions-Kreise (nur wenn blink aktiv)
tft.drawCircle(160, 100, 60, COLOR_WARNING);
tft.drawCircle(160, 100, 80, COLOR_TEXT);
}
// Verbesserte NeoPixel Animation
if (explosionTime < 3000) {
// Wilde Chaos-Phase
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; // Rot
case 1: color = ring1.Color(255, 80, 0); break; // Orange
case 2: color = ring1.Color(255, 255, 0); break; // Gelb
case 3: color = ring1.Color(255, 255, 255); break; // Weiß
case 4: color = ring1.Color(255, 50, 150); break; // Pink
}
ring1.setPixelColor(i, color);
}
ring1.show();
// Ring2: Rotierende Explosion mit mehr LEDs
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 {
// Langsameres Pulsieren nach 3 Sekunden
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();
// Ring2: Langsame Welle
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;
}
// Header mit Status
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 Progress Bar (modernisiert)
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);
// LED Label
tft.fillRect(12, 28, 100, 6, COLOR_BG);
tft.setTextColor(COLOR_TEXT_DIM);
tft.setTextSize(1);
tft.setCursor(12, 28);
tft.print("LEDs");
// LED Werte (rechts)
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); // Etwas weiter links positioniert
tft.print(ledText);
lastLedBarW = ledBarW;
lastDisplayedLedsRemaining = ledsRemaining;
lastDisplayedTotalLeds = totalLeds;
}
// Timer Progress Bar
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);
// Timer Label
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");
// Timer Werte (rechts)
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;
}
// Status-Info Panel (ersetzt große Zeitanzeige)
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 läuft");
} 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 mit Hinweisen
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 ready - waiting for MAIN_SW");
}
// ----------------- Loop (unverändert bis auf TFT-Updates + non-blocking explosion) -----------------
void loop() {
unsigned long now = millis();
// Hauptschalter
bool currentSystemEnabled = (digitalRead(MAIN_SW) == LOW);
if (currentSystemEnabled != systemEnabled) {
forceRedraw = true;
}
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; // Explosion auch zurücksetzen
if (tftPresent && now - lastTftUpdate >= TFT_UPDATE_MS) {
tftDraw();
lastTftUpdate = now;
}
delay(50);
return;
}
// Begrüßung
if (systemEnabled && !greetingAck) {
if (digitalRead(BTN_START) == LOW) {
greetingAck = true;
// greetingDrawn Flag zurücksetzen für nächstes Mal
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;
}
// -------------------- Non-blocking Explosion Handler --------------------
if (exploded) {
// keep display & leds updated while explosion sequence runs
unsigned long nowE = now;
// 1) Start buzzer for EXPLOSION_BUZZ_MS (non-blocking)
if (!explosionToneOn && nowE - explosionStart < EXPLOSION_BUZZ_MS) {
tone(BUZZER, 400); // start tone (no delay)
explosionToneOn = true;
}
// 2) After buzzer duration, stop tone and activate RELAY1
if (explosionToneOn && nowE - explosionStart >= EXPLOSION_BUZZ_MS) {
noTone(BUZZER);
explosionToneOn = false;
digitalWrite(RELAY1, HIGH);
relay1OnTime = nowE;
}
// 3) After RELAY1_PULSE_MS, turn relay off and finish explosion sequence
if (relay1OnTime != 0 && nowE - relay1OnTime >= RELAY1_PULSE_MS) {
digitalWrite(RELAY1, LOW);
// Explosion beendet — Reset / Rückkehr in Konfigmodus
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;
}
// Ensure TFT updates occur frequently during explosion
if (tftPresent && now - lastTftUpdate >= TFT_UPDATE_MS) {
tftDraw();
lastTftUpdate = now;
}
// skip normal timer modifications while exploded (running is false anyway)
// but continue the rest of loop for button handling and display updates
}
// --- Restlicher Timer-Code (unverändert mit einer kleinen Anpassung: Explosion-Start non-blocking) ---
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 läuft");
} else {
running = false; digitalWrite(RELAY2, LOW); tftSetStatus("Timer gestoppt");
}
beepBlocking(1000, CONFIRM_BEEP_MS);
// NOTE: Do NOT use pulseRelay() here for explosion — we only use pulseRelay for other sequences.
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) {
// START NON-BLOCKING EXPLOSION (previously this used blocking beepBlocking/pulseRelay)
running = false;
digitalWrite(RELAY2, LOW);
tftSetStatus("BOOM! Bombe explodiert!");
exploded = true;
explosionStart = millis();
explosionToneOn = false;
relay1OnTime = 0;
// keep timeValue set to segment value for display
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