#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <TM1637Display.h>
#include <Adafruit_NeoPixel.h>
// =============================================================================
// PIN-DEFINITIONEN
// =============================================================================
#define TFT_CS 15
#define TFT_DC 2
#define TFT_RST 4
#define BUTTON_PIN 13
#define BUTTON_UP 32
#define BUTTON_DOWN 33
#define TM1637_CLK 21
#define TM1637_DIO 22
#define BUZZER_PIN 25
#define LED_RING_PIN 26
#define LED_RING_2_PIN 27
#define BUTTON_LED_PIN 14
#define RELAY_PIN 12 // Timer läuft
#define RELAY2_PIN 16 // Timer wechselt und Ende
#define BATTERY_PIN 34
#define LED_COUNT 35
#define RELAY3_PIN 17 // Rauch
// =============================================================================
// LAYOUT-KONSTANTEN
// =============================================================================
// Startbildschirm
#define START_HEADER_X 15
#define START_HEADER_Y 15
#define START_HEADER_W 290
#define START_HEADER_H 45
#define START_CREDIT_Y 75
#define START_CREDIT_H 35
#define START_INFO_Y 125
#define START_INFO_H 50
#define START_BUTTON_X 100
#define START_BUTTON_Y 190
#define START_BUTTON_W 120
#define START_BUTTON_H 35
// Konfigurationsbildschirm
#define CONFIG_STATUS_X 10
#define CONFIG_STATUS_Y 10
#define CONFIG_STATUS_W 230
#define CONFIG_STATUS_H 30
#define CONFIG_BATTERY_X 250
#define CONFIG_BATTERY_Y 12
#define CONFIG_BATTERY_W 60
#define CONFIG_BATTERY_H 20
#define CONFIG_LED_X 10
#define CONFIG_LED_Y 50
#define CONFIG_LED_W 300
#define CONFIG_LED_H 40
#define CONFIG_TIMER_X 10
#define CONFIG_TIMER_Y 100
#define CONFIG_TIMER_W 300
#define CONFIG_TIMER_H 80
#define CONFIG_BUTTON_Y 195
#define CONFIG_BUTTON_W 140
#define CONFIG_BUTTON_H 40
// 3D-Schatten
#define SHADOW_OFFSET 3
#define SHADOW_COLOR 0x18C3
// =============================================================================
// HARDWARE-OBJEKTE
// =============================================================================
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
TM1637Display display(TM1637_CLK, TM1637_DIO);
Adafruit_NeoPixel ledRing(LED_COUNT, LED_RING_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel ledRing2(LED_COUNT, LED_RING_2_PIN, NEO_GRB + NEO_KHZ800);
// =============================================================================
// AKKU-KONSTANTEN
// =============================================================================
const float VOLTAGE_DIVIDER_RATIO = (200000.0 + 47000.0) / 47000.0;
const float CALIBRATION_FACTOR = 1.15;
const float BATTERY_MAX = 12.6;
const float BATTERY_MIN = 9.0;
const int BATTERY_UPDATE_INTERVAL = 5000;
// =============================================================================
// FARB-DEFINITIONEN
// =============================================================================
#define COLOR_BG 0x0000
#define COLOR_PANEL 0x2945
#define COLOR_ACCENT 0xFD20
#define COLOR_SUCCESS 0x07E0
#define COLOR_WARNING 0xFFE0
#define COLOR_DANGER 0xF800
#define COLOR_INFO 0x07FF
#define COLOR_TEXT 0xFFFF
#define COLOR_TEXT_DIM 0x8410
#define COLOR_CREDIT 0x87F0
#define COLOR_ORANGE 0xFD20
#define COLOR_YELLOW 0xFFE0
// =============================================================================
// FORWARD DECLARATIONS (Funktionsprototypen)
// =============================================================================
void setStatus(const char* status);
void shortBeep(int freq, int duration);
void drawConfigScreen();
void updateButtonLED();
void updateRelay();
void updateRelay2();
void updateRelay3();
void startPreCountdown();
void skipPreCountdown();
void updateLedRing();
void updateLedRing2();
void playExplosionSequence();
void startRing2Animation(bool filling);
void stopRing2Animation();
void drawHoldProgress(int progress);
void drawStatusBox();
void startTachoCheck();
void startTimer();
// =============================================================================
// TIMING-KONSTANTEN
// =============================================================================
const unsigned long TIMER_DURATION = 60000;
const long SEGMENT_UPDATE_INTERVAL = 10;
const long TFT_UPDATE_INTERVAL = 100;
const long BLINK_INTERVAL = 500;
const long DEBOUNCE_DELAY = 50;
const long INITIAL_REPEAT_DELAY = 500;
const long REPEAT_INTERVAL = 100;
const long HOLD_DURATION = 3000;
const long TACHO_CHECK_DURATION = 2000;
const long RELAY2_PULSE_DURATION = 500;
const long WARNING_THRESHOLD = 10000;
const long PRE_COUNTDOWN_DURATION = 10; // 99 Sekunden Vorlauf
const long LED_FADE_STEPS = 50;
// =============================================================================
// LED-KONSTANTEN
// =============================================================================
const uint8_t LED_BRIGHTNESS = 100;
const uint8_t MIN_LEDS = 1;
const uint8_t MAX_LEDS = LED_COUNT;
// =============================================================================
// BUZZER-FREQUENZEN
// =============================================================================
const int BEEP_FREQ_NORMAL = 1000;
const int BEEP_FREQ_START = 1500;
const int BEEP_FREQ_PAUSE = 1200;
const int BEEP_FREQ_LED_DOWN = 1200;
const int BEEP_FREQ_FINAL = 2000;
const int BEEP_FREQ_UP = 800;
const int BEEP_FREQ_DOWN = 600;
const int BEEP_FREQ_HOLD_PROGRESS = 600;
const int BEEP_FREQ_WARNING = 1800;
// =============================================================================
// ZUSTANDSMASCHINE
// =============================================================================
enum SystemState {
STATE_START_SCREEN,
STATE_CONFIG_SCREEN,
STATE_PRE_COUNTDOWN,
STATE_TIMER_RUNNING,
STATE_TIMER_PAUSED,
STATE_EXPLOSION
};
SystemState currentState = STATE_START_SCREEN;
SystemState previousState = STATE_START_SCREEN;
// =============================================================================
// SYSTEM-VARIABLEN
// =============================================================================
bool timerRunning = false;
bool timerStarted = false;
bool preCountdownActive = false;
unsigned long preCountdownStartTime = 0;
int preCountdownSeconds = PRE_COUNTDOWN_DURATION;
int lastPreCountdownSecond = -1;
unsigned long timerStartTime = 0;
unsigned long timerPauseTime = 0;
unsigned long timerDuration = TIMER_DURATION;
unsigned long lastSegmentUpdate = 0;
unsigned long lastTftUpdate = 0;
unsigned long lastBlinkTime = 0;
unsigned long lastButtonBlinkTime = 0;
unsigned long lastWarningTime = 0;
unsigned long lastSecondBeeped = 99;
int activeLeds = 10;
int initialLeds = 10;
bool blinkState = false;
bool buttonBlinkState = false;
int currentBlinkingLed = 0;
bool finalRound = false;
bool warningActive = false;
int currentRound = 1;
unsigned long currentRemainingTime = TIMER_DURATION;
bool ring2AnimationActive = false;
unsigned long ring2AnimationStartTime = 0;
bool ring2Filling = true;
bool tachoCheckActive = false;
unsigned long tachoCheckStartTime = 0;
bool tachoCheckDone = false;
char statusText[20] = "Bereit";
bool forceRedraw = true;
bool firstStart = true;
bool relay2Active = false;
unsigned long relay2StartTime = 0;
bool relay2ExplosionMode = false;
bool relay3Active = false;
unsigned long relay3StartTime = 0;
const long RELAY3_DURATION = 2000; // 2 Sekunden Rauch
float batteryVoltage = 0.0;
int batteryPercentage = 0;
unsigned long lastBatteryUpdate = 0;
bool lowBatteryWarningShown = false;
// =============================================================================
// TACHO-CHECK BLITZ-VARIABLEN (NUR ROTER BLITZ)
// =============================================================================
bool redFlashActive = false;
unsigned long redFlashStartTime = 0;
const long RED_FLASH_DURATION = 500; // 150ms Dauer für weißen Blitz (kurz und knackig!)
// =============================================================================
// STATISTIKEN
// =============================================================================
struct Statistics {
uint16_t totalRoundsPlayed;
uint16_t totalExplosions;
unsigned long longestSurvivalTime;
uint16_t currentSessionRounds;
} stats = {0, 0, 0, 0};
// =============================================================================
// BUTTON-HANDLING
// =============================================================================
struct ButtonState {
bool lastState;
bool pressed;
unsigned long lastDebounceTime;
unsigned long pressTime;
};
ButtonState mainButton = {HIGH, false, 0, 0};
ButtonState upButton = {HIGH, false, 0, 0};
ButtonState downButton = {HIGH, false, 0, 0};
bool mainButtonHeld = false;
bool holdProgressBeeped1 = false;
bool holdProgressBeeped2 = false;
bool showingHoldScreen = false;
// =============================================================================
// LED FADE VARIABLEN
// =============================================================================
uint8_t targetLedBrightness[LED_COUNT];
uint8_t currentLedBrightness[LED_COUNT];
// =============================================================================
// FINALRUNDEN-BLINK VARIABLEN
// =============================================================================
bool finalBlinkActive = false;
unsigned long lastFinalBlinkTime = 0;
bool finalBlinkState = false;
unsigned long currentBlinkInterval = 500;
// =============================================================================
// EXPLOSIONS-ANIMATION
// =============================================================================
struct ExplosionParticle {
float x, y;
float vx, vy;
uint16_t color;
bool active;
};
ExplosionParticle particles[50];
int numParticles = 50;
// =============================================================================
// RELAIS-STEUERUNG
// =============================================================================
void updateRelay() {
if (timerRunning) {
digitalWrite(RELAY_PIN, HIGH);
} else {
digitalWrite(RELAY_PIN, LOW);
}
}
void updateRelay2() {
if (relay2ExplosionMode) {
digitalWrite(RELAY2_PIN, HIGH);
} else if (relay2Active) {
unsigned long currentMillis = millis();
if (currentMillis - relay2StartTime < RELAY2_PULSE_DURATION) {
digitalWrite(RELAY2_PIN, HIGH);
} else {
digitalWrite(RELAY2_PIN, LOW);
relay2Active = false;
}
} else {
digitalWrite(RELAY2_PIN, LOW);
}
}
void updateRelay3() {
if (relay3Active) {
unsigned long currentMillis = millis();
if (currentMillis - relay3StartTime < RELAY3_DURATION) {
digitalWrite(RELAY3_PIN, HIGH);
} else {
digitalWrite(RELAY3_PIN, LOW);
relay3Active = false;
}
} else {
digitalWrite(RELAY3_PIN, LOW);
}
}
void triggerRelay3() {
relay3Active = true;
relay3StartTime = millis();
}
void triggerRelay2Pulse() {
relay2Active = true;
relay2StartTime = millis();
}
void startRelay2Explosion() {
relay2ExplosionMode = true;
digitalWrite(RELAY2_PIN, HIGH);
}
void stopRelay2Explosion() {
relay2ExplosionMode = false;
digitalWrite(RELAY2_PIN, LOW);
relay2Active = false;
}
// =============================================================================
// AKKU-FUNKTIONEN
// =============================================================================
float readBatteryVoltage() {
float sum = 0;
for(int i = 0; i < 10; i++) {
int rawValue = analogRead(BATTERY_PIN);
float adcVoltage = (rawValue / 4095.0) * 3.3;
float actualVoltage = adcVoltage * VOLTAGE_DIVIDER_RATIO;
actualVoltage = actualVoltage * CALIBRATION_FACTOR;
sum += actualVoltage;
delay(2);
}
return sum / 10.0;
}
int calculateBatteryPercentage(float voltage) {
if (voltage >= BATTERY_MAX) return 100;
if (voltage <= BATTERY_MIN) return 0;
float percentage = ((voltage - BATTERY_MIN) / (BATTERY_MAX - BATTERY_MIN)) * 100;
return constrain((int)percentage, 0, 100);
}
uint16_t getBatteryColor(int percentage) {
if (percentage > 50) return COLOR_SUCCESS;
if (percentage > 20) return COLOR_WARNING;
return COLOR_DANGER;
}
void drawBatteryStatus() {
tft.fillRect(CONFIG_BATTERY_X - 2, CONFIG_BATTERY_Y - 2,
CONFIG_BATTERY_W + 10, CONFIG_BATTERY_H + 4, COLOR_BG);
tft.drawRoundRect(CONFIG_BATTERY_X, CONFIG_BATTERY_Y,
CONFIG_BATTERY_W, CONFIG_BATTERY_H, 3, COLOR_TEXT);
tft.fillRect(CONFIG_BATTERY_X + CONFIG_BATTERY_W, CONFIG_BATTERY_Y + 5, 3, 10, COLOR_TEXT);
int fillWidth = (CONFIG_BATTERY_W - 4) * batteryPercentage / 100;
fillWidth = constrain(fillWidth, 0, CONFIG_BATTERY_W - 4);
uint16_t fillColor = getBatteryColor(batteryPercentage);
tft.fillRoundRect(CONFIG_BATTERY_X + 2, CONFIG_BATTERY_Y + 2,
fillWidth, CONFIG_BATTERY_H - 4, 2, fillColor);
tft.setTextSize(1);
tft.setTextColor(COLOR_TEXT);
char voltageText[10];
sprintf(voltageText, "%.1fV", batteryVoltage);
int textWidth = 6 * 5;
int textX = CONFIG_BATTERY_X + (CONFIG_BATTERY_W - textWidth) / 2;
int textY = CONFIG_BATTERY_Y + 6;
tft.fillRect(textX - 2, textY - 1, textWidth + 4, 8, COLOR_BG);
tft.setCursor(textX, textY);
tft.print(voltageText);
}
void updateBatteryStatus() {
unsigned long currentMillis = millis();
if (currentMillis - lastBatteryUpdate >= BATTERY_UPDATE_INTERVAL) {
lastBatteryUpdate = currentMillis;
batteryVoltage = readBatteryVoltage();
batteryPercentage = calculateBatteryPercentage(batteryVoltage);
if (batteryPercentage <= 15 && !lowBatteryWarningShown) {
lowBatteryWarningShown = true;
setStatus("Akku niedrig!");
shortBeep(BEEP_FREQ_DOWN, 300);
} else if (batteryPercentage > 15) {
lowBatteryWarningShown = false;
}
if (currentState == STATE_CONFIG_SCREEN ||
currentState == STATE_TIMER_RUNNING ||
currentState == STATE_TIMER_PAUSED) {
drawBatteryStatus();
}
}
}
// =============================================================================
// STATISTIK-FUNKTIONEN
// =============================================================================
void updateStatistics() {
if (currentRound > stats.currentSessionRounds) {
stats.currentSessionRounds = currentRound;
}
unsigned long survivalTime = (currentRound - 1) * TIMER_DURATION +
(TIMER_DURATION - currentRemainingTime);
if (survivalTime > stats.longestSurvivalTime) {
stats.longestSurvivalTime = survivalTime;
}
}
// =============================================================================
// TIMER FUNKTIONEN
// =============================================================================
void startTimer() {
// Wenn es der allererste Start ist UND Pre-Countdown noch nicht lief: Pre-Countdown zuerst!
if (firstStart && !timerStarted && !preCountdownActive) {
startPreCountdown();
return;
}
// Wenn Pre-Countdown vorbei ist, firstStart zurücksetzen
if (preCountdownActive) {
preCountdownActive = false;
}
timerRunning = true;
timerStarted = true;
timerStartTime = millis();
activeLeds = initialLeds;
currentBlinkingLed = activeLeds - 1;
currentRound = 1;
finalRound = false;
currentRemainingTime = timerDuration;
lastSecondBeeped = 99;
firstStart = false;
warningActive = false;
stats.currentSessionRounds = 0;
display.showNumberDecEx(6000, 0b01000000, true);
setStatus("Timer laeuft");
shortBeep(BEEP_FREQ_START, 200);
drawConfigScreen();
updateButtonLED();
updateRelay();
triggerRelay2Pulse();
}
void pauseTimer() {
timerRunning = false;
timerPauseTime = millis();
lastSecondBeeped = 99;
warningActive = false;
// Warnung-Ränder löschen
tft.fillRect(0, 0, 320, 5, COLOR_BG);
tft.fillRect(0, 235, 320, 5, COLOR_BG);
tft.fillRect(0, 0, 5, 240, COLOR_BG);
tft.fillRect(315, 0, 5, 240, COLOR_BG);
setStatus("Pausiert");
shortBeep(BEEP_FREQ_PAUSE, 200);
drawConfigScreen();
updateRelay();
triggerRelay2Pulse();
}
void resumeTimer() {
timerRunning = true;
unsigned long pauseDuration = millis() - timerPauseTime;
timerStartTime += pauseDuration;
setStatus("Timer laeuft");
shortBeep(BEEP_FREQ_START, 200);
drawConfigScreen();
updateRelay();
triggerRelay2Pulse();
}
void updateSegmentDisplay() {
if (!timerRunning) return;
unsigned long currentMillis = millis();
unsigned long elapsed = currentMillis - timerStartTime;
unsigned long remaining = (timerDuration > elapsed) ? (timerDuration - elapsed) : 0;
currentRemainingTime = remaining;
unsigned long seconds = remaining / 1000;
unsigned long hundredths = (remaining % 1000) / 10;
int displayValue = (seconds * 100) + hundredths;
display.showNumberDecEx(displayValue, 0b01000000, true);
if (remaining > 0 && seconds != lastSecondBeeped) {
lastSecondBeeped = seconds;
tone(BUZZER_PIN, BEEP_FREQ_NORMAL, 50);
}
if (remaining == 0) {
if (activeLeds > 0) {
activeLeds--;
currentRound++;
stats.totalRoundsPlayed++;
stats.currentSessionRounds = currentRound;
shortBeep(BEEP_FREQ_LED_DOWN, 200);
updateLedRing();
drawConfigScreen();
if (activeLeds == 0) {
finalRound = true;
finalBlinkActive = true;
currentBlinkingLed = -1;
// Sofort alle LEDs rot machen als Test
ledRing.clear();
ledRing2.clear();
for (int i = 0; i < LED_COUNT; i++) {
ledRing.setPixelColor(i, ledRing.Color(255, 0, 0));
ledRing2.setPixelColor(i, ledRing2.Color(255, 0, 0));
}
ledRing.show();
ledRing2.show();
shortBeep(2000, 100);
triggerRelay3(); // RAUCH für 2 Sekunden!
}
timerStartTime = currentMillis;
currentRemainingTime = timerDuration;
lastSecondBeeped = 99;
} else if (finalRound) {
timerRunning = false;
timerStarted = false;
finalRound = false;
warningActive = false;
updateStatistics();
playExplosionSequence();
display.showNumberDecEx(6000, 0b01000000, true);
activeLeds = initialLeds;
currentBlinkingLed = activeLeds - 1;
currentRound = 1;
currentRemainingTime = timerDuration;
setStatus("Bereit");
firstStart = true;
tachoCheckDone = false;
updateButtonLED();
updateRelay();
drawConfigScreen();
}
}
}
// =============================================================================
// PRE-COUNTDOWN FUNKTIONEN (Bahnhofs-Flip-Animation)
// =============================================================================
void drawFlipNumber(int number, uint16_t color) {
tft.fillScreen(COLOR_BG);
// Pulsierender Hintergrund-Effekt
if (number <= 3) {
// Rote Vignette in den letzten 3 Sekunden
for (int i = 0; i < 30; i += 5) {
uint16_t vignetteColor = tft.color565(80 - i*2, 0, 0);
tft.drawRect(i, i, 320 - i*2, 240 - i*2, vignetteColor);
}
}
// Großer Rahmen - noch größer und zentrierter!
int boxX = 20;
int boxY = 30;
int boxW = 280;
int boxH = 160;
// Doppelter 3D-Schatten für mehr Tiefe
tft.fillRoundRect(boxX + SHADOW_OFFSET + 2, boxY + SHADOW_OFFSET + 2, boxW, boxH, 15, 0x0841);
tft.fillRoundRect(boxX + SHADOW_OFFSET, boxY + SHADOW_OFFSET, boxW, boxH, 15, SHADOW_COLOR);
// Farbverlauf-Effekt im Panel
uint16_t panelColor = (number <= 3) ? 0x2104 : COLOR_PANEL;
tft.fillRoundRect(boxX, boxY, boxW, boxH, 15, panelColor);
// Dicker Rahmen (3 Pixel)
for (int i = 0; i < 3; i++) {
tft.drawRoundRect(boxX - i, boxY - i, boxW + i*2, boxH + i*2, 15, color);
}
// Zahl - NOCH GRÖSSER!
tft.setTextSize(12);
tft.setTextColor(color);
char numStr[3];
sprintf(numStr, "%02d", number);
// Bessere Zentrierung für größere Schrift
int charWidth = 72; // Textsize 12
int textW = charWidth * 2;
int textX = boxX + (boxW - textW) / 2;
int textY = boxY + (boxH - 96) / 2;
// Schatten für die Zahl
tft.setCursor(textX + 3, textY + 3);
tft.setTextColor(COLOR_BG);
tft.print(numStr);
// Hauptzahl
tft.setCursor(textX, textY);
tft.setTextColor(color);
tft.print(numStr);
// Info-Text - GRÖSSER und auffälliger!
tft.setTextSize(3);
tft.setTextColor(color);
int infoY = 200;
// Zentrierter Text
const char* infoText = "SPIEL STARTET";
int infoTextW = strlen(infoText) * 18; // 6px * 3
int infoTextX = (320 - infoTextW) / 2;
// Text-Schatten
tft.setCursor(infoTextX + 2, infoY + 2);
tft.setTextColor(SHADOW_COLOR);
tft.print(infoText);
tft.setCursor(infoTextX, infoY);
tft.setTextColor(color);
tft.print(infoText);
// Hinweis zum Überspringen
tft.setTextSize(1);
if (number <= 3) {
tft.setTextColor(COLOR_DANGER);
} else {
tft.setTextColor(COLOR_TEXT_DIM);
}
const char* skipText = "[START] druecken zum Ueberspringen";
int skipTextW = strlen(skipText) * 6;
int skipTextX = (320 - skipTextW) / 2;
tft.setCursor(skipTextX, 225);
tft.print(skipText);
// Fortschrittsbalken unten
int barY = 235;
int barW = 280;
int barX = 20;
int progress = ((PRE_COUNTDOWN_DURATION - number) * barW) / PRE_COUNTDOWN_DURATION;
tft.fillRect(barX, barY, barW, 3, COLOR_PANEL);
tft.fillRect(barX, barY, progress, 3, color);
}
void drawHoldCountdown(int progress) {
static int lastProgress = -1;
static bool screenInitialized = false;
// Reset-Modus wenn progress < 0
if (progress < 0) {
screenInitialized = false;
lastProgress = -1;
return;
}
// Beim ersten Aufruf: Kompletten Screen zeichnen
if (!screenInitialized || lastProgress == -1) {
tft.fillScreen(COLOR_BG);
// Großer Rahmen - etwas höher für mehr Platz
int boxX = 20;
int boxY = 30;
int boxW = 280;
int boxH = 180;
// 3D-Schatten
tft.fillRoundRect(boxX + SHADOW_OFFSET, boxY + SHADOW_OFFSET, boxW, boxH, 15, SHADOW_COLOR);
// Farbverlauf je nach Aktion
uint16_t boxColor;
uint16_t accentColor;
const char* actionText;
if (timerRunning) {
boxColor = 0x2104;
accentColor = COLOR_WARNING;
actionText = "PAUSIEREN";
} else if (timerStarted) {
boxColor = 0x0841;
accentColor = COLOR_SUCCESS;
actionText = "FORTSETZEN";
} else {
boxColor = 0x0841;
accentColor = COLOR_SUCCESS;
actionText = "STARTEN";
}
tft.fillRoundRect(boxX, boxY, boxW, boxH, 15, boxColor);
// Dicker Rahmen
for (int i = 0; i < 3; i++) {
tft.drawRoundRect(boxX - i, boxY - i, boxW + i*2, boxH + i*2, 15, accentColor);
}
// Action-Text ganz oben - GRÖßER
tft.setTextSize(3);
tft.setTextColor(accentColor);
int actionTextW = strlen(actionText) * 18;
int actionTextX = boxX + (boxW - actionTextW) / 2;
// Text-Schatten für bessere Lesbarkeit
tft.setCursor(actionTextX + 2, boxY + 12);
tft.setTextColor(COLOR_BG);
tft.print(actionText);
tft.setCursor(actionTextX, boxY + 10);
tft.setTextColor(accentColor);
tft.print(actionText);
// Trennlinie
tft.drawLine(boxX + 20, boxY + 45, boxX + boxW - 20, boxY + 45, COLOR_TEXT_DIM);
// Fortschrittsbalken-Bereich (statt Kreis)
int barX = boxX + 20;
int barY = boxY + 90;
int barW = boxW - 40;
int barH = 30;
// Balken-Hintergrund
tft.fillRoundRect(barX, barY, barW, barH, 5, COLOR_BG);
tft.drawRoundRect(barX, barY, barW, barH, 5, COLOR_TEXT_DIM);
// Label über dem Balken
tft.setTextSize(1);
tft.setTextColor(COLOR_TEXT_DIM);
tft.setCursor(barX, barY - 12);
tft.print(F("Halte den Button..."));
// Hinweis unten
tft.setTextSize(2);
tft.setTextColor(COLOR_TEXT_DIM);
const char* hintText = "3 Sekunden";
int hintTextW = strlen(hintText) * 12;
int hintTextX = boxX + (boxW - hintTextW) / 2;
tft.setCursor(hintTextX, boxY + barY - boxY + 45);
tft.print(hintText);
screenInitialized = true;
}
// Nur wenn sich Progress geändert hat: Update
if (progress != lastProgress) {
int boxX = 20;
int boxY = 30;
int boxW = 280;
uint16_t accentColor;
if (timerRunning) {
accentColor = COLOR_WARNING;
} else {
accentColor = COLOR_SUCCESS;
}
// Fortschrittsbalken aktualisieren
int barX = boxX + 20;
int barY = boxY + 90;
int barW = boxW - 40;
int barH = 30;
// Balken neu zeichnen
tft.fillRoundRect(barX, barY, barW, barH, 5, COLOR_BG);
tft.drawRoundRect(barX, barY, barW, barH, 5, COLOR_TEXT_DIM);
// Fortschritt füllen
if (progress > 0) {
int fillW = ((barW - 4) * progress) / 100;
fillW = constrain(fillW, 0, barW - 4);
if (fillW > 0) {
tft.fillRoundRect(barX + 2, barY + 2, fillW, barH - 4, 3, accentColor);
}
}
// Prozent-Text IN DER MITTE des Balkens
tft.setTextSize(2);
tft.setTextColor(COLOR_TEXT);
char percentText[5];
sprintf(percentText, "%d%%", progress);
int textW = strlen(percentText) * 12;
int textX = barX + (barW - textW) / 2;
int textY = barY + (barH - 16) / 2;
// Text-Schatten
tft.setCursor(textX + 1, textY + 1);
tft.setTextColor(COLOR_BG);
tft.print(percentText);
tft.setCursor(textX, textY);
tft.setTextColor(COLOR_TEXT);
tft.print(percentText);
lastProgress = progress;
}
}
void startPreCountdown() {
currentState = STATE_PRE_COUNTDOWN;
preCountdownActive = true;
preCountdownStartTime = millis();
preCountdownSeconds = PRE_COUNTDOWN_DURATION;
lastPreCountdownSecond = -1;
drawFlipNumber(preCountdownSeconds, COLOR_INFO);
shortBeep(1200, 100);
}
void updatePreCountdown() {
if (!preCountdownActive) return;
unsigned long currentMillis = millis();
unsigned long elapsed = currentMillis - preCountdownStartTime;
int currentSecond = PRE_COUNTDOWN_DURATION - (elapsed / 1000);
if (currentSecond < 0) {
// Pre-Countdown vorbei - Timer starten!
preCountdownActive = false;
firstStart = false; // WICHTIG!
startTimer();
return;
}
// Neue Sekunde?
if (currentSecond != lastPreCountdownSecond) {
lastPreCountdownSecond = currentSecond;
// Farbe: Letzte 3 Sekunden ROT
uint16_t numberColor;
if (currentSecond <= 3) {
numberColor = COLOR_DANGER;
// Dramatischer Ton für 3-2-1
shortBeep(1800 + (3 - currentSecond) * 200, 150);
// RELAY2 triggern für die letzten 3 Sekunden
if (currentSecond == 3 || currentSecond == 2 || currentSecond == 1) {
triggerRelay2Pulse(); // 100ms Standard-Puls
} else if (currentSecond == 0) {
// Bei 0: Längerer Puls (500ms wird automatisch durch RELAY2_PULSE_DURATION gesteuert)
triggerRelay2Pulse();
}
} else {
numberColor = COLOR_INFO;
// Normaler Tick
shortBeep(1000, 50);
}
// Flip-Animation (vereinfacht)
drawFlipNumber(currentSecond, numberColor);
}
}
void skipPreCountdown() {
if (preCountdownActive) {
preCountdownActive = false;
firstStart = false; // WICHTIG!
shortBeep(1500, 100);
startTimer();
}
}
// =============================================================================
// VEREINHEITLICHTES BUTTON-HANDLING
// =============================================================================
bool readButton(int pin, ButtonState &btnState) {
bool reading = digitalRead(pin);
unsigned long currentMillis = millis();
if (reading != btnState.lastState) {
btnState.lastDebounceTime = currentMillis;
}
if ((currentMillis - btnState.lastDebounceTime) > DEBOUNCE_DELAY) {
if (reading == LOW && !btnState.pressed) {
btnState.pressed = true;
btnState.pressTime = currentMillis;
btnState.lastState = reading;
return true;
} else if (reading == HIGH && btnState.pressed) {
btnState.pressed = false;
}
}
btnState.lastState = reading;
return false;
}
void handleMainButton() {
unsigned long currentMillis = millis();
bool reading = digitalRead(BUTTON_PIN);
static unsigned long lastHoldDrawTime = 0; // NEU: Statische Variable
if (reading == LOW && mainButton.lastState == HIGH) {
mainButton.lastDebounceTime = currentMillis;
mainButton.pressTime = currentMillis;
mainButtonHeld = false;
holdProgressBeeped1 = false;
holdProgressBeeped2 = false;
mainButton.pressed = true;
lastHoldDrawTime = 0;
// NEU: Statische Variablen in drawHoldCountdown zurücksetzen
static int resetProgress = -1;
// PRE-COUNTDOWN hat HÖCHSTE PRIORITÄT
if (currentState == STATE_PRE_COUNTDOWN) {
skipPreCountdown();
mainButton.pressed = false;
mainButton.lastState = reading;
return;
}
if (timerStarted) {
if (timerRunning) {
startRing2Animation(false);
} else {
startRing2Animation(true);
}
}
if (firstStart && currentState == STATE_START_SCREEN) {
currentState = STATE_CONFIG_SCREEN;
drawConfigScreen();
mainButton.pressed = false;
}
else if (firstStart && !timerStarted) {
startTimer();
mainButton.pressed = false;
}
}
if (reading == LOW && mainButton.pressed) {
unsigned long holdTime = currentMillis - mainButton.pressTime;
// NEUE HOLD-ANZEIGE ab 500ms - ABER NUR ALLE 50ms NEU ZEICHNEN
if (holdTime > 500 && !mainButtonHeld) {
showingHoldScreen = true; // NEU: Flag setzen
// NEU: Nur alle 50ms neu zeichnen
if (currentMillis - lastHoldDrawTime >= 100) {
lastHoldDrawTime = currentMillis;
int progress = (holdTime * 100) / HOLD_DURATION;
progress = constrain(progress, 0, 100);
// GROSSE BOX ANZEIGEN
drawHoldCountdown(progress);
}
// Ton-Feedback
if (holdTime >= 1000 && holdTime < 1100 && !holdProgressBeeped1) {
shortBeep(BEEP_FREQ_HOLD_PROGRESS, 100);
holdProgressBeeped1 = true;
}
if (holdTime >= 2000 && holdTime < 2100 && !holdProgressBeeped2) {
shortBeep(BEEP_FREQ_HOLD_PROGRESS, 100);
holdProgressBeeped2 = true;
}
}
if (ring2AnimationActive) {
updateLedRing2();
}
if (holdTime >= HOLD_DURATION && !mainButtonHeld) {
mainButtonHeld = true;
if (currentState == STATE_PRE_COUNTDOWN) {
return;
}
if (currentState == STATE_START_SCREEN) {
currentState = STATE_CONFIG_SCREEN;
drawConfigScreen();
} else {
if (!timerStarted) {
startTimer();
} else {
if (timerRunning) {
pauseTimer();
} else {
resumeTimer();
}
}
}
stopRing2Animation();
updateLedRing2();
}
}
if (reading == HIGH && mainButton.lastState == LOW) {
lastHoldDrawTime = 0;
showingHoldScreen = false;
// NEU: Flag für drawHoldCountdown zurücksetzen
// Wir übergeben einen negativen Wert um Reset zu signalisieren
drawHoldCountdown(-1);
// Screen zurücksetzen wenn losgelassen
if (mainButton.pressed && !mainButtonHeld) {
drawConfigScreen();
shortBeep(BEEP_FREQ_HOLD_PROGRESS, 100);
}
if (ring2AnimationActive) {
stopRing2Animation();
updateLedRing2();
}
mainButton.pressed = false;
mainButtonHeld = false;
holdProgressBeeped1 = false;
holdProgressBeeped2 = false;
}
mainButton.lastState = reading;
}
void handleUpButton() {
if (timerStarted) return;
if (readButton(BUTTON_UP, upButton)) {
if (initialLeds < MAX_LEDS) {
initialLeds++;
activeLeds = initialLeds;
currentBlinkingLed = activeLeds - 1;
updateLedRing();
shortBeep(BEEP_FREQ_UP, 80);
drawConfigScreen();
}
}
// Kontinuierliches Erhöhen
if (upButton.pressed && digitalRead(BUTTON_UP) == LOW) {
unsigned long currentMillis = millis();
if (currentMillis - upButton.pressTime > INITIAL_REPEAT_DELAY) {
if ((currentMillis - upButton.lastDebounceTime) > REPEAT_INTERVAL) {
if (initialLeds < MAX_LEDS) {
initialLeds++;
activeLeds = initialLeds;
currentBlinkingLed = activeLeds - 1;
updateLedRing();
shortBeep(BEEP_FREQ_UP, 80);
drawConfigScreen();
upButton.lastDebounceTime = currentMillis;
}
}
}
}
}
void handleDownButton() {
if (timerStarted) return;
if (readButton(BUTTON_DOWN, downButton)) {
if (initialLeds > MIN_LEDS) {
initialLeds--;
activeLeds = initialLeds;
currentBlinkingLed = activeLeds - 1;
updateLedRing();
shortBeep(BEEP_FREQ_DOWN, 80);
drawConfigScreen();
}
}
// Kontinuierliches Verringern
if (downButton.pressed && digitalRead(BUTTON_DOWN) == LOW) {
unsigned long currentMillis = millis();
if (currentMillis - downButton.pressTime > INITIAL_REPEAT_DELAY) {
if ((currentMillis - downButton.lastDebounceTime) > REPEAT_INTERVAL) {
if (initialLeds > MIN_LEDS) {
initialLeds--;
activeLeds = initialLeds;
currentBlinkingLed = activeLeds - 1;
updateLedRing();
shortBeep(BEEP_FREQ_DOWN, 80);
drawConfigScreen();
downButton.lastDebounceTime = currentMillis;
}
}
}
}
}
// =============================================================================
// 3D-SCHATTEN HILFSFUNKTIONEN
// =============================================================================
void draw3DRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r,
uint16_t fillColor, uint16_t borderColor) {
// Schatten
tft.fillRoundRect(x + SHADOW_OFFSET, y + SHADOW_OFFSET, w, h, r, SHADOW_COLOR);
// Hauptform
tft.fillRoundRect(x, y, w, h, r, fillColor);
tft.drawRoundRect(x, y, w, h, r, borderColor);
}
void draw3DButton(int16_t x, int16_t y, int16_t w, int16_t h,
uint16_t fillColor, const char* text, uint8_t textSize) {
// Schatten
tft.fillRoundRect(x + SHADOW_OFFSET, y + SHADOW_OFFSET, w, h, 5, SHADOW_COLOR);
// Button
tft.fillRoundRect(x, y, w, h, 5, fillColor);
tft.drawRoundRect(x, y, w, h, 5, COLOR_TEXT);
// Text zentrieren
tft.setTextColor(COLOR_TEXT);
tft.setTextSize(textSize);
int textWidth = strlen(text) * 6 * textSize;
int textX = x + (w - textWidth) / 2;
int textY = y + (h - 8 * textSize) / 2;
tft.setCursor(textX, textY);
tft.print(text);
}
// =============================================================================
// LED FADE FUNKTIONEN
// =============================================================================
void initLedFade() {
for (int i = 0; i < LED_COUNT; i++) {
targetLedBrightness[i] = 0;
currentLedBrightness[i] = 0;
}
}
void setLedTarget(int index, uint8_t brightness) {
if (index >= 0 && index < LED_COUNT) {
targetLedBrightness[index] = brightness;
}
}
void updateLedFade() {
bool needsUpdate = false;
for (int i = 0; i < LED_COUNT; i++) {
if (currentLedBrightness[i] < targetLedBrightness[i]) {
currentLedBrightness[i] += min(5, targetLedBrightness[i] - currentLedBrightness[i]);
needsUpdate = true;
} else if (currentLedBrightness[i] > targetLedBrightness[i]) {
currentLedBrightness[i] -= min(5, currentLedBrightness[i] - targetLedBrightness[i]);
needsUpdate = true;
}
}
if (needsUpdate) {
for (int i = 0; i < activeLeds; i++) {
uint32_t baseColor;
if (activeLeds > LED_COUNT * 2 / 3) {
baseColor = ledRing.Color(0, currentLedBrightness[i], 0);
} else if (activeLeds > LED_COUNT / 3) {
baseColor = ledRing.Color(currentLedBrightness[i], currentLedBrightness[i], 0);
} else {
baseColor = ledRing.Color(currentLedBrightness[i], 0, 0);
}
ledRing.setPixelColor(i, baseColor);
}
ledRing.show();
}
}
// =============================================================================
// COUNTDOWN-WARNUNG (Letzte 10 Sekunden)
// =============================================================================
void updateCountdownWarning() {
if (currentRemainingTime <= WARNING_THRESHOLD && currentRemainingTime > 0) {
unsigned long currentMillis = millis();
if (!warningActive) {
warningActive = true;
lastWarningTime = currentMillis;
}
// Pulsierender roter Hintergrund
if (currentMillis - lastWarningTime >= 500) {
lastWarningTime = currentMillis;
// Rote Vignette am Bildschirmrand
uint8_t intensity = 50 + (currentRemainingTime % 1000) / 10;
uint16_t warningColor = tft.color565(intensity, 0, 0);
// Oberer und unterer Rand
tft.fillRect(0, 0, 320, 5, warningColor);
tft.fillRect(0, 235, 320, 5, warningColor);
// Seitliche Ränder
tft.fillRect(0, 0, 5, 240, warningColor);
tft.fillRect(315, 0, 5, 240, warningColor);
// Warning-Ton
if (currentRemainingTime % 2000 < 100) {
tone(BUZZER_PIN, BEEP_FREQ_WARNING, 50);
}
}
} else if (warningActive) {
warningActive = false;
// Ränder löschen
tft.fillRect(0, 0, 320, 5, COLOR_BG);
tft.fillRect(0, 235, 320, 5, COLOR_BG);
tft.fillRect(0, 0, 5, 240, COLOR_BG);
tft.fillRect(315, 0, 5, 240, COLOR_BG);
}
}
// =============================================================================
// EXPLOSIONS-FUNKTIONEN
// =============================================================================
void initExplosionParticles() {
for (int i = 0; i < numParticles; i++) {
particles[i].x = 160;
particles[i].y = 120;
float angle = random(0, 628) / 100.0;
float speed = random(20, 80) / 10.0;
particles[i].vx = cos(angle) * speed;
particles[i].vy = sin(angle) * speed;
int colorChoice = random(0, 5);
switch(colorChoice) {
case 0: particles[i].color = COLOR_DANGER; break;
case 1: particles[i].color = COLOR_ORANGE; break;
case 2: particles[i].color = COLOR_YELLOW; break;
case 3: particles[i].color = COLOR_WARNING; break;
default: particles[i].color = COLOR_TEXT; break;
}
particles[i].active = true;
}
}
void drawExplosionFrame(int frame, int maxFrames) {
if (frame == 0) {
tft.fillScreen(COLOR_BG);
initExplosionParticles();
}
if (frame < 5) {
uint16_t flashColor = tft.color565(255 - frame * 50, 200 - frame * 40, 0);
tft.fillScreen(flashColor);
} else {
tft.fillScreen(COLOR_BG);
}
if (frame < 15) {
int radius1 = frame * 15;
int radius2 = frame * 10;
int radius3 = frame * 5;
tft.fillCircle(160, 120, radius1, COLOR_DANGER);
tft.fillCircle(160, 120, radius2, COLOR_ORANGE);
tft.fillCircle(160, 120, radius3, COLOR_YELLOW);
}
for (int i = 0; i < numParticles; i++) {
if (particles[i].active) {
particles[i].x += particles[i].vx;
particles[i].y += particles[i].vy;
particles[i].vy += 0.3;
if (particles[i].x >= 0 && particles[i].x < 320 &&
particles[i].y >= 0 && particles[i].y < 240) {
tft.fillCircle((int)particles[i].x, (int)particles[i].y, 2, particles[i].color);
} else {
particles[i].active = false;
}
}
}
if (frame >= 5 && frame < 25) {
int textSize = 5;
if (frame < 10) {
textSize = frame - 4;
}
if (frame > 20) {
textSize = 25 - frame;
}
textSize = constrain(textSize, 1, 5);
tft.setTextSize(textSize);
int textWidth = 6 * 4 * textSize;
int textX = (320 - textWidth) / 2;
int textY = 100;
tft.setCursor(textX + 2, textY + 2);
tft.setTextColor(COLOR_BG);
tft.print(F("BOOM"));
tft.setCursor(textX, textY);
tft.setTextColor(COLOR_DANGER);
tft.print(F("BOOM"));
}
if (frame > 10) {
for (int i = 0; i < 5; i++) {
int smokeY = 120 + (frame - 10) * 3 - i * 20;
int smokeX = 160 + random(-30, 30);
int smokeSize = 10 + i * 3;
if (smokeY < 240 && smokeY > 0) {
tft.fillCircle(smokeX, smokeY, smokeSize, COLOR_TEXT_DIM);
}
}
}
}
void explosionSoundEffect(int stage) {
switch(stage) {
case 0: tone(BUZZER_PIN, 100, 150); break;
case 1: tone(BUZZER_PIN, 150, 100); break;
case 2: tone(BUZZER_PIN, 80, 120); break;
case 3: tone(BUZZER_PIN, 200, 80); break;
case 4: tone(BUZZER_PIN, 120, 100); break;
default:
if (stage % 2 == 0) {
tone(BUZZER_PIN, BEEP_FREQ_FINAL, 100);
}
break;
}
}
void confettiEffect(int frame) {
ledRing.clear();
ledRing2.clear();
for (int i = 0; i < LED_COUNT; i++) {
if (random(0, 100) < 60) {
uint32_t color;
int colorChoice = random(0, 6);
switch(colorChoice) {
case 0: color = ledRing.Color(255, 0, 0); break;
case 1: color = ledRing.Color(0, 255, 0); break;
case 2: color = ledRing.Color(0, 0, 255); break;
case 3: color = ledRing.Color(255, 255, 0); break;
case 4: color = ledRing.Color(255, 0, 255); break;
case 5: color = ledRing.Color(0, 255, 255); break;
}
ledRing.setPixelColor(i, color);
ledRing2.setPixelColor(i, color);
}
}
if (frame < 10 && frame % 2 == 0) {
for (int i = 0; i < LED_COUNT; i++) {
ledRing.setPixelColor(i, ledRing.Color(255, 255, 255));
ledRing2.setPixelColor(i, ledRing2.Color(255, 255, 255));
}
}
ledRing.show();
ledRing2.show();
}
void playExplosionSequence() {
currentState = STATE_EXPLOSION;
const int totalFrames = 40;
const int frameDelay = 50;
stats.totalExplosions++;
updateStatistics();
startRelay2Explosion();
ledRing.setBrightness(255);
ledRing2.setBrightness(255);
for (int frame = 0; frame < totalFrames; frame++) {
drawExplosionFrame(frame, totalFrames);
confettiEffect(frame);
if (frame % 4 < 2) {
display.showNumberDec(8888, true);
} else {
display.clear();
}
if (frame < 10) {
explosionSoundEffect(frame);
} else if (frame % 3 == 0) {
explosionSoundEffect(frame);
}
delay(frameDelay);
}
for (int i = 0; i < 3; i++) {
tone(BUZZER_PIN, BEEP_FREQ_FINAL, 200);
display.showNumberDec(8888, true);
delay(200);
noTone(BUZZER_PIN);
display.clear();
delay(200);
}
ledRing.setBrightness(LED_BRIGHTNESS);
ledRing2.setBrightness(LED_BRIGHTNESS);
stopRelay2Explosion();
}
// =============================================================================
// HILFSFUNKTIONEN
// =============================================================================
inline uint16_t getTimeColor(int seconds) {
if (seconds > 40) return COLOR_SUCCESS;
if (seconds > 20) return COLOR_WARNING;
return COLOR_DANGER;
}
inline uint16_t getLEDColor(int current, int max) {
float ratio = (float)current / max;
if (ratio > 0.66f) return COLOR_SUCCESS;
if (ratio > 0.33f) return COLOR_WARNING;
return COLOR_DANGER;
}
void setStatus(const char* status) {
strncpy(statusText, status, sizeof(statusText) - 1);
statusText[sizeof(statusText) - 1] = '\0';
if (currentState == STATE_CONFIG_SCREEN ||
currentState == STATE_TIMER_RUNNING ||
currentState == STATE_TIMER_PAUSED) {
drawStatusBox();
}
}
inline void shortBeep(int freq, int duration) {
tone(BUZZER_PIN, freq, duration);
}
void updateButtonLED() {
if (!timerStarted && currentState == STATE_CONFIG_SCREEN) {
digitalWrite(BUTTON_LED_PIN, HIGH);
} else {
digitalWrite(BUTTON_LED_PIN, LOW);
}
}
void drawProgressBar(int16_t x, int16_t y, int16_t w, int16_t h,
int progress, int maxVal, uint16_t color, const char* label) {
tft.fillRoundRect(x, y, w, h, 3, COLOR_PANEL);
tft.drawRoundRect(x, y, w, h, 3, COLOR_TEXT_DIM);
if (maxVal > 0 && progress > 0) {
int16_t fillW = (w * progress) / maxVal;
fillW = constrain(fillW, 0, w - 2);
if (fillW > 0) {
tft.fillRoundRect(x + 1, y + 1, fillW, h - 2, 2, color);
}
}
tft.fillRect(x, y - 10, w, 10, COLOR_PANEL);
tft.setTextColor(COLOR_TEXT_DIM);
tft.setTextSize(1);
tft.setCursor(x, y - 10);
tft.print(label);
tft.fillRect(x + w - 30, y - 10, 30, 10, COLOR_PANEL);
char valueText[10];
snprintf(valueText, sizeof(valueText), "%d/%d", progress, maxVal);
tft.setTextColor(color);
tft.setCursor(x + w - 30, y - 10);
tft.print(valueText);
}
void drawTimeProgressBar(int16_t x, int16_t y, int16_t w, int16_t h,
int seconds, const char* label) {
tft.fillRoundRect(x, y, w, h, 3, COLOR_PANEL);
tft.drawRoundRect(x, y, w, h, 3, COLOR_TEXT_DIM);
if (seconds > 0) {
int16_t fillW = (w * seconds) / 60;
fillW = constrain(fillW, 0, w - 2);
if (fillW > 0) {
uint16_t timeColor = getTimeColor(seconds);
tft.fillRoundRect(x + 1, y + 1, fillW, h - 2, 2, timeColor);
}
}
tft.fillRect(x, y - 10, w, 10, COLOR_PANEL);
tft.setTextColor(COLOR_TEXT_DIM);
tft.setTextSize(1);
tft.setCursor(x, y - 10);
tft.print(label);
tft.fillRect(x + w - 30, y - 10, 30, 10, COLOR_PANEL);
char valueText[10];
snprintf(valueText, sizeof(valueText), "%02ds", seconds);
uint16_t timeColor = getTimeColor(seconds);
tft.setTextColor(timeColor);
tft.setCursor(x + w - 30, y - 10);
tft.print(valueText);
}
// =============================================================================
// SCREEN-DRAWING FUNKTIONEN
// =============================================================================
void drawStartButton() {
if (buttonBlinkState) {
draw3DButton(START_BUTTON_X, START_BUTTON_Y, START_BUTTON_W,
START_BUTTON_H, COLOR_SUCCESS, "START", 2);
} else {
tft.fillRoundRect(START_BUTTON_X, START_BUTTON_Y,
START_BUTTON_W, START_BUTTON_H, 5, COLOR_BG);
tft.drawRoundRect(START_BUTTON_X, START_BUTTON_Y,
START_BUTTON_W, START_BUTTON_H, 5, COLOR_BG);
}
}
void drawStartScreen() {
tft.fillScreen(COLOR_BG);
currentState = STATE_START_SCREEN;
draw3DRoundRect(START_HEADER_X, START_HEADER_Y, START_HEADER_W,
START_HEADER_H, 8, COLOR_PANEL, COLOR_ACCENT);
tft.setTextColor(COLOR_ACCENT);
tft.setTextSize(2);
tft.setCursor(50, 28);
tft.print(F("HNGR13 BOMB TIMER"));
draw3DRoundRect(START_HEADER_X, START_CREDIT_Y, START_HEADER_W,
START_CREDIT_H, 6, COLOR_PANEL, COLOR_CREDIT);
tft.setTextColor(COLOR_CREDIT);
tft.setTextSize(1);
tft.setCursor(70, 85);
tft.print(F("Written by"));
tft.setTextSize(1);
tft.setCursor(85, 97);
tft.print(F("HNGR13 Hausteam"));
draw3DRoundRect(START_HEADER_X, START_INFO_Y, START_HEADER_W,
START_INFO_H, 6, COLOR_PANEL, COLOR_INFO);
tft.setTextColor(COLOR_TEXT);
tft.setTextSize(1);
tft.setCursor(25, 135);
tft.print(F("Bereit zum Start"));
tft.setTextColor(COLOR_INFO);
tft.setCursor(25, 150);
tft.print(F(">> START druecken zum Fortfahren"));
drawStartButton();
}
void drawStatusBox() {
draw3DRoundRect(CONFIG_STATUS_X, CONFIG_STATUS_Y, CONFIG_STATUS_W,
CONFIG_STATUS_H, 5, COLOR_PANEL, COLOR_INFO);
tft.setTextColor(COLOR_INFO);
tft.setTextSize(1);
tft.setCursor(CONFIG_STATUS_X + 5, CONFIG_STATUS_Y + 8);
tft.print(F("Status:"));
tft.fillRect(CONFIG_STATUS_X + 60, CONFIG_STATUS_Y + 8, 160, 8, COLOR_PANEL);
tft.setTextColor(COLOR_TEXT);
tft.setCursor(CONFIG_STATUS_X + 60, CONFIG_STATUS_Y + 8);
tft.print(statusText);
}
void drawLEDSetting() {
draw3DRoundRect(CONFIG_LED_X, CONFIG_LED_Y, CONFIG_LED_W,
CONFIG_LED_H, 5, COLOR_PANEL, COLOR_WARNING);
tft.setTextColor(COLOR_WARNING);
tft.setTextSize(1);
tft.setCursor(CONFIG_LED_X + 5, CONFIG_LED_Y + 7);
tft.print(F("LED EINSTELLUNG"));
if (!timerStarted) {
// Buttons und LED-Anzeige
draw3DButton(CONFIG_LED_X + 10, CONFIG_LED_Y + 20, 25, 15, COLOR_DANGER, "-", 1);
draw3DButton(CONFIG_LED_X + 110, CONFIG_LED_Y + 20, 25, 15, COLOR_SUCCESS, "+", 1);
tft.setTextColor(COLOR_TEXT);
tft.setTextSize(2);
tft.setCursor(CONFIG_LED_X + 45, CONFIG_LED_Y + 20);
tft.print(initialLeds);
tft.print(F("/35"));
// Statistik rechts daneben
tft.setTextColor(COLOR_TEXT_DIM);
tft.setTextSize(1);
tft.setCursor(CONFIG_LED_X + 145, CONFIG_LED_Y + 20);
tft.print(F("Explosionen:"));
tft.setTextColor(COLOR_DANGER);
tft.setCursor(CONFIG_LED_X + 240, CONFIG_LED_Y + 20);
tft.print(stats.totalExplosions);
tft.setTextColor(COLOR_TEXT_DIM);
tft.setCursor(CONFIG_LED_X + 145, CONFIG_LED_Y + 30);
tft.print(F("Max Zeit:"));
tft.setTextColor(COLOR_SUCCESS);
tft.setCursor(CONFIG_LED_X + 240, CONFIG_LED_Y + 30);
unsigned long maxSeconds = stats.longestSurvivalTime / 1000;
tft.print(maxSeconds);
tft.print(F("s"));
} else {
// Während Timer läuft
tft.setTextColor(COLOR_TEXT_DIM);
tft.setTextSize(1);
tft.setCursor(CONFIG_LED_X + 10, CONFIG_LED_Y + 20);
tft.print(F("LEDs: "));
tft.setTextColor(COLOR_TEXT);
tft.print(activeLeds);
tft.print(F("/35"));
// Statistik auch während Timer sichtbar
tft.setTextColor(COLOR_TEXT_DIM);
tft.setCursor(CONFIG_LED_X + 145, CONFIG_LED_Y + 20);
tft.print(F("Explosionen:"));
tft.setTextColor(COLOR_DANGER);
tft.setCursor(CONFIG_LED_X + 240, CONFIG_LED_Y + 20);
tft.print(stats.totalExplosions);
tft.setTextColor(COLOR_TEXT_DIM);
tft.setCursor(CONFIG_LED_X + 145, CONFIG_LED_Y + 30);
tft.print(F("Max Zeit:"));
tft.setTextColor(COLOR_SUCCESS);
tft.setCursor(CONFIG_LED_X + 240, CONFIG_LED_Y + 30);
unsigned long maxSeconds = stats.longestSurvivalTime / 1000;
tft.print(maxSeconds);
tft.print(F("s"));
}
}
void drawTimerDisplay() {
draw3DRoundRect(CONFIG_TIMER_X, CONFIG_TIMER_Y, CONFIG_TIMER_W,
CONFIG_TIMER_H, 5, COLOR_PANEL, COLOR_INFO);
tft.setTextColor(COLOR_INFO);
tft.setTextSize(1);
tft.setCursor(CONFIG_TIMER_X + 5, CONFIG_TIMER_Y + 7);
tft.print(F("TIMER ANZEIGE"));
uint16_t ledColor = getLEDColor(activeLeds, initialLeds);
drawProgressBar(CONFIG_TIMER_X + 5, CONFIG_TIMER_Y + 25, 270, 12,
activeLeds, initialLeds, ledColor, "Verbleibende LEDs");
int secondsRemaining = currentRemainingTime / 1000;
drawTimeProgressBar(CONFIG_TIMER_X + 5, CONFIG_TIMER_Y + 50, 270, 12,
secondsRemaining, "Verbleibende Zeit");
tft.setTextColor(COLOR_TEXT_DIM);
tft.setTextSize(1);
tft.setCursor(CONFIG_TIMER_X + 5, CONFIG_TIMER_Y + 70);
tft.print(F("Aktuelle Runde: "));
tft.setTextColor(COLOR_INFO);
tft.print(currentRound);
tft.setTextColor(COLOR_TEXT_DIM);
tft.print(F("/"));
tft.setTextColor(COLOR_INFO);
tft.print(initialLeds);
}
void updateTimerDisplay() {
int secondsRemaining = currentRemainingTime / 1000;
uint16_t ledColor = getLEDColor(activeLeds, initialLeds);
drawProgressBar(CONFIG_TIMER_X + 5, CONFIG_TIMER_Y + 25, 270, 12,
activeLeds, initialLeds, ledColor, "Verbleibende LEDs");
drawTimeProgressBar(CONFIG_TIMER_X + 5, CONFIG_TIMER_Y + 50, 270, 12,
secondsRemaining, "Verbleibende Zeit");
tft.fillRect(CONFIG_TIMER_X + 5, CONFIG_TIMER_Y + 70, 150, 8, COLOR_PANEL);
tft.setTextColor(COLOR_TEXT_DIM);
tft.setTextSize(1);
tft.setCursor(CONFIG_TIMER_X + 5, CONFIG_TIMER_Y + 70);
tft.print(F("Aktuelle Runde: "));
tft.setTextColor(COLOR_INFO);
tft.print(currentRound);
tft.setTextColor(COLOR_TEXT_DIM);
tft.print(F("/"));
tft.setTextColor(COLOR_INFO);
tft.print(initialLeds);
}
void drawControlButtons() {
const int buttonX = (320 - CONFIG_BUTTON_W) / 2;
if (!timerStarted) {
draw3DButton(buttonX, CONFIG_BUTTON_Y, CONFIG_BUTTON_W,
CONFIG_BUTTON_H, COLOR_SUCCESS, "START", 2);
} else if (timerRunning) {
draw3DButton(buttonX, CONFIG_BUTTON_Y, CONFIG_BUTTON_W,
CONFIG_BUTTON_H, COLOR_WARNING, "PAUSE", 2);
} else {
draw3DButton(buttonX, CONFIG_BUTTON_Y, CONFIG_BUTTON_W,
CONFIG_BUTTON_H, COLOR_SUCCESS, "WEITER", 2);
}
}
void drawConfigScreen() {
tft.fillScreen(COLOR_BG);
if (timerRunning) {
currentState = STATE_TIMER_RUNNING;
} else if (timerStarted) {
currentState = STATE_TIMER_PAUSED;
} else {
currentState = STATE_CONFIG_SCREEN;
}
forceRedraw = true;
drawStatusBox();
drawBatteryStatus();
drawLEDSetting();
drawTimerDisplay();
drawControlButtons();
display.setBrightness(0x0f);
ledRing.setBrightness(LED_BRIGHTNESS);
ledRing2.setBrightness(LED_BRIGHTNESS);
if (!tachoCheckDone) {
startTachoCheck();
tachoCheckDone = true;
} else {
updateLedRing();
ledRing2.clear();
ledRing2.show();
}
if (!timerStarted || firstStart) {
display.showNumberDecEx(6000, 0b01000000, true);
} else {
unsigned long seconds = currentRemainingTime / 1000;
unsigned long hundredths = (currentRemainingTime % 1000) / 10;
int displayValue = (seconds * 100) + hundredths;
display.showNumberDecEx(displayValue, 0b01000000, true);
}
updateButtonLED();
}
// =============================================================================
// LED RING FUNKTIONEN - TACHO-CHECK ANIMATION OHNE WEISSEN BLITZ
// =============================================================================
uint32_t getTachoColor(int position, int maxPosition) {
float ratio = (float)position / maxPosition;
uint8_t red, green, blue;
if (ratio < 0.5) {
float localRatio = ratio * 2.0;
red = (uint8_t)(localRatio * 255);
green = 255;
blue = 0;
} else {
float localRatio = (ratio - 0.5) * 2.0;
red = 255;
green = (uint8_t)(255 - (localRatio * 255));
blue = 0;
}
return ledRing.Color(red, green, blue);
}
void startTachoCheck() {
tachoCheckActive = true;
tachoCheckStartTime = millis();
}
// =============================================================================
// VEREINFACHTE TACHO-CHECK FUNKTION OHNE WEISSEN BLITZ
// =============================================================================
void updateTachoCheck() {
if (!tachoCheckActive) return;
unsigned long currentMillis = millis();
unsigned long elapsed = currentMillis - tachoCheckStartTime;
// Prüfe weißen Blitz (hat Priorität)
if (redFlashActive) {
unsigned long elapsed = currentMillis - redFlashStartTime;
if (elapsed >= RED_FLASH_DURATION) {
// Blitz vorbei - sofort AUS
redFlashActive = false;
tachoCheckActive = false;
ledRing.clear();
ledRing2.clear();
ledRing.show();
ledRing2.show();
ledRing.setBrightness(LED_BRIGHTNESS);
ledRing2.setBrightness(LED_BRIGHTNESS);
delay(50);
updateLedRing();
} else {
// FADE-IN: Helligkeit steigt schnell an
float progress = (float)elapsed / RED_FLASH_DURATION;
uint8_t brightness = (uint8_t)(progress * 255);
ledRing.setBrightness(brightness);
ledRing2.setBrightness(brightness);
for (int i = 0; i < LED_COUNT; i++) {
ledRing.setPixelColor(i, ledRing.Color(255, 255, 255));
ledRing2.setPixelColor(i, ledRing2.Color(255, 255, 255));
}
ledRing.show();
ledRing2.show();
}
return;
}
// Normale Animationsphasen
if (elapsed >= TACHO_CHECK_DURATION) {
// Starte roten Blitz statt sofort zu beenden
// Blitz starten
redFlashActive = true;
redFlashStartTime = currentMillis;
shortBeep(2500, 100); // Kurzer knackiger Ton
return;
}
// Phasenberechnung
float progress;
bool fillingPhase = false;
bool emptyingPhase = false;
bool displayPhase = false;
if (elapsed < 800) {
// Phase 1: Auffüllen
fillingPhase = true;
progress = (float)elapsed / 800.0;
} else if (elapsed < 1200) {
// Phase 2: Display
displayPhase = true;
progress = 1.0;
} else {
// Phase 3: Ablaufen
emptyingPhase = true;
progress = 1.0 - ((float)(elapsed - 1200) / 800.0);
}
int ledsToLight = (int)(progress * LED_COUNT);
ledsToLight = constrain(ledsToLight, 0, LED_COUNT);
ledRing.clear();
ledRing2.clear();
// Farben für verschiedene Phasen
uint32_t animColor;
if (fillingPhase) {
animColor = ledRing.Color(0, 255, 0); // Grün
} else if (displayPhase) {
animColor = ledRing.Color(255, 255, 255); // Weiß
} else {
animColor = ledRing.Color(255, 0, 0); // Rot
}
// LEDs setzen
for (int i = 0; i < ledsToLight; i++) {
if (fillingPhase || emptyingPhase) {
ledRing.setPixelColor(i, animColor);
ledRing2.setPixelColor(i, animColor);
} else {
// Display-Phase: Regenbogen-Effekt
uint32_t color = getTachoColor(i, LED_COUNT);
ledRing.setPixelColor(i, color);
ledRing2.setPixelColor(i, color);
}
}
// Ende der Leer-Phase: Roter Blitz
if (emptyingPhase && ledsToLight == 0 && !redFlashActive) {
redFlashActive = true;
redFlashStartTime = currentMillis;
for (int i = 0; i < LED_COUNT; i++) {
ledRing.setPixelColor(i, ledRing.Color(255, 0, 0));
ledRing2.setPixelColor(i, ledRing2.Color(255, 0, 0));
}
ledRing.show();
ledRing2.show();
shortBeep(BEEP_FREQ_DOWN, 200);
return;
}
// Normales Anzeigen
ledRing.show();
ledRing2.show();
// Start-Ton für Füllphase
if (fillingPhase && elapsed < 50) {
shortBeep(BEEP_FREQ_UP, 100);
}
}
void updateLedRing() {
// In der Finalrunde nicht überschreiben - wird von updateBlinkingLed() gesteuert
if (finalBlinkActive && finalRound) {
return;
}
ledRing.clear();
uint32_t baseColor;
if (activeLeds > LED_COUNT * 2 / 3) {
baseColor = ledRing.Color(0, 200, 0);
} else if (activeLeds > LED_COUNT / 3) {
baseColor = ledRing.Color(200, 200, 0);
} else {
baseColor = ledRing.Color(200, 0, 0);
}
for (int i = 0; i < activeLeds; i++) {
ledRing.setPixelColor(i, baseColor);
setLedTarget(i, 200);
}
if (currentBlinkingLed >= 0 && currentBlinkingLed < LED_COUNT && blinkState) {
ledRing.setPixelColor(currentBlinkingLed, ledRing.Color(255, 255, 255));
}
ledRing.show();
}
void updateLedRing2() {
ledRing2.clear();
// Ring2-Animation hat IMMER Vorrang (beim Button-Halten)
if (ring2AnimationActive) {
unsigned long currentMillis = millis();
unsigned long elapsed = currentMillis - ring2AnimationStartTime;
float progress = (float)elapsed / HOLD_DURATION;
progress = constrain(progress, 0.0f, 1.0f);
int ledsToLight;
if (ring2Filling) {
ledsToLight = (int)(progress * LED_COUNT);
} else {
ledsToLight = LED_COUNT - (int)(progress * LED_COUNT);
}
ledsToLight = constrain(ledsToLight, 0, LED_COUNT);
uint32_t animColor;
if (ring2Filling) {
animColor = ledRing2.Color(0, 0, 255);
} else {
animColor = ledRing2.Color(255, 100, 0);
}
for (int i = 0; i < ledsToLight; i++) {
ledRing2.setPixelColor(i, animColor);
}
}
// Wenn keine Button-Animation: Finalrunden-Blinken (synchron zu Ring1)
else if (finalBlinkActive && finalRound && timerRunning && finalBlinkState) {
for (int i = 0; i < LED_COUNT; i++) {
ledRing2.setPixelColor(i, ledRing2.Color(255, 0, 0));
}
}
ledRing2.show();
}
void startRing2Animation(bool filling) {
ring2AnimationActive = true;
ring2AnimationStartTime = millis();
ring2Filling = filling;
}
void stopRing2Animation() {
ring2AnimationActive = false;
}
void updateBlinkingLed() {
if (tachoCheckActive) return;
unsigned long currentMillis = millis();
// Finalrunden-Blinken mit dynamischer Frequenz
if (finalBlinkActive && finalRound && timerRunning) {
// Berechne Blinkintervall basierend auf verbleibender Zeit
unsigned long remainingSeconds = currentRemainingTime / 1000;
// Exponentiell steigende Frequenz
if (remainingSeconds > 50) {
currentBlinkInterval = 500;
} else if (remainingSeconds > 40) {
currentBlinkInterval = 400;
} else if (remainingSeconds > 30) {
currentBlinkInterval = 300;
} else if (remainingSeconds > 20) {
currentBlinkInterval = 200;
} else if (remainingSeconds > 15) {
currentBlinkInterval = 150;
} else if (remainingSeconds > 10) {
currentBlinkInterval = 100;
} else if (remainingSeconds > 5) {
currentBlinkInterval = 70;
} else {
currentBlinkInterval = 50;
}
if (currentMillis - lastFinalBlinkTime >= currentBlinkInterval) {
lastFinalBlinkTime = currentMillis;
finalBlinkState = !finalBlinkState;
// Beide Ringe komplett rot blinken lassen
ledRing.clear();
if (finalBlinkState) {
// ROT AN
for (int i = 0; i < LED_COUNT; i++) {
ledRing.setPixelColor(i, ledRing.Color(255, 0, 0));
}
// Dramatischer Piep-Ton in den letzten 10 Sekunden
if (remainingSeconds <= 10) {
int beepFreq = 1500 + (10 - remainingSeconds) * 100;
tone(BUZZER_PIN, beepFreq, currentBlinkInterval / 2);
}
}
ledRing.show();
// Ring2 wird separat in updateLedRing2() gehandhabt
}
return; // Kein normales Blinken während Finalrunde
}
// Normales Blinken (nicht in Finalrunde)
if (currentMillis - lastBlinkTime >= BLINK_INTERVAL) {
lastBlinkTime = currentMillis;
blinkState = !blinkState;
updateLedRing();
}
}
void updateButtonBlink() {
unsigned long currentMillis = millis();
if (currentMillis - lastButtonBlinkTime >= BLINK_INTERVAL) {
lastButtonBlinkTime = currentMillis;
buttonBlinkState = !buttonBlinkState;
if (currentState == STATE_START_SCREEN) {
drawStartButton();
}
}
}
// =============================================================================
// MELODIE-FUNKTIONEN
// =============================================================================
#define NOTE_C5 523
#define NOTE_D5 587
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_G5 784
#define NOTE_A5 880
#define NOTE_B5 988
#define NOTE_C6 1047
void playWelcomeMelody() {
int melody[] = {
NOTE_C5, NOTE_E5, NOTE_G5, NOTE_C6,
NOTE_G5, NOTE_E5, NOTE_C5,
NOTE_C5, NOTE_G5, NOTE_C6
};
int noteDurations[] = {
8, 8, 8, 4,
8, 8, 4,
8, 8, 2
};
int numNotes = sizeof(melody) / sizeof(melody[0]);
for (int i = 0; i < numNotes; i++) {
int noteDuration = 1000 / noteDurations[i];
tone(BUZZER_PIN, melody[i], noteDuration);
delay(noteDuration * 1.3);
noTone(BUZZER_PIN);
}
}
// =============================================================================
// SETUP & LOOP
// =============================================================================
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(BUTTON_UP, INPUT_PULLUP);
pinMode(BUTTON_DOWN, INPUT_PULLUP);
pinMode(BUTTON_LED_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(RELAY_PIN, OUTPUT);
pinMode(RELAY2_PIN, OUTPUT);
pinMode(RELAY3_PIN, OUTPUT);
pinMode(BATTERY_PIN, INPUT);
digitalWrite(RELAY_PIN, LOW);
digitalWrite(RELAY2_PIN, LOW);
digitalWrite(RELAY3_PIN, LOW);
digitalWrite(BUZZER_PIN, LOW);
digitalWrite(BUTTON_LED_PIN, LOW);
pinMode(TFT_RST, OUTPUT);
digitalWrite(TFT_RST, HIGH);
delay(100);
digitalWrite(TFT_RST, LOW);
delay(100);
digitalWrite(TFT_RST, HIGH);
delay(100);
tft.begin();
tft.setRotation(1);
tft.fillScreen(COLOR_BG);
display.setBrightness(0x0f);
display.clear();
ledRing.begin();
ledRing.setBrightness(0);
ledRing.clear();
ledRing.show();
ledRing2.begin();
ledRing2.setBrightness(0);
ledRing2.clear();
ledRing2.show();
initLedFade();
randomSeed(analogRead(0));
Serial.println(F("HNGR13 Bomb Timer - Verbesserte Version"));
Serial.println(F("Features: Zustandsmaschine, 3D-Schatten, Statistiken"));
Serial.print(F("Kalibrierungsfaktor: "));
Serial.println(CALIBRATION_FACTOR);
batteryVoltage = readBatteryVoltage();
batteryPercentage = calculateBatteryPercentage(batteryVoltage);
lastBatteryUpdate = millis();
drawStartScreen();
playWelcomeMelody();
}
void loop() {
unsigned long currentMillis = millis();
// Tacho-Check Animation hat höchste Priorität
if (tachoCheckActive) {
updateTachoCheck();
updateBatteryStatus();
updateRelay();
updateRelay2();
return;
}
// Pre-Countdown hat höchste Priorität
if (currentState == STATE_PRE_COUNTDOWN) {
updatePreCountdown();
handleMainButton();
updateRelay();
updateRelay2();
updateRelay3();
return;
}
// NEU: Wenn Hold-Screen angezeigt wird, KEINE anderen Updates!
if (showingHoldScreen) {
handleMainButton(); // Nur Button-Handling
if (ring2AnimationActive) {
updateLedRing2();
}
updateRelay();
updateRelay2();
updateRelay3();
return; // Keine anderen Screen-Updates!
}
// Normale Operation wenn kein Tacho-Check aktiv
switch(currentState) {
case STATE_TIMER_RUNNING:
if (currentMillis - lastSegmentUpdate >= SEGMENT_UPDATE_INTERVAL) {
lastSegmentUpdate = currentMillis;
updateSegmentDisplay();
}
if (currentMillis - lastTftUpdate >= TFT_UPDATE_INTERVAL) {
lastTftUpdate = currentMillis;
updateTimerDisplay();
updateStatistics();
}
updateCountdownWarning();
break;
case STATE_TIMER_PAUSED:
case STATE_CONFIG_SCREEN:
// Keine extra Updates
break;
case STATE_START_SCREEN:
break;
case STATE_EXPLOSION:
return;
}
// LED Ring blinken
if ((currentState == STATE_CONFIG_SCREEN ||
currentState == STATE_TIMER_RUNNING ||
currentState == STATE_TIMER_PAUSED) && !tachoCheckActive) {
// In der Finalrunde KEIN updateLedFade, sonst überschreibt es das Blinken
if (!finalBlinkActive) {
updateLedFade();
}
updateBlinkingLed();
}
// Button Blinken
if (currentState == STATE_START_SCREEN) {
updateButtonBlink();
}
// Akku-Status aktualisieren
updateBatteryStatus();
// Button Handling
handleMainButton();
handleUpButton();
handleDownButton();
// Animation für zweiten Ring
if (ring2AnimationActive && !tachoCheckActive) {
updateLedRing2();
} else if (finalBlinkActive && finalRound && timerRunning) {
// In Finalrunde auch Ring2 updaten wenn keine Button-Animation läuft
updateLedRing2();
}
// Peripherie Updates
updateButtonLED();
updateRelay();
updateRelay2();
updateRelay3();
}