#include #include #include #include #include // ---------------- CONFIG ---------------- #define LED_PIN_1 6 #define LED_PIN_2 7 #define LED_PIN_3 8 #define LED_PIN_4 9 #define NUM_LEDS 5 // 0..4 -- el LED 0 NUNCA se enciende #define BUTTON_HOUR 11 #define BUTTON_MINUTE_5 12 #define BUTTON_MINUTE_1 4 #define BUTTON_BRIGHTNESS 5 #define BUTTON_MODE 10 Adafruit_NeoPixel strips[4] = { Adafruit_NeoPixel(NUM_LEDS, LED_PIN_1, NEO_GRB + NEO_KHZ800), Adafruit_NeoPixel(NUM_LEDS, LED_PIN_2, NEO_GRB + NEO_KHZ800), Adafruit_NeoPixel(NUM_LEDS, LED_PIN_3, NEO_GRB + NEO_KHZ800), Adafruit_NeoPixel(NUM_LEDS, LED_PIN_4, NEO_GRB + NEO_KHZ800) }; RTC_DS3231 rtc; // ---------------- ESTADOS ---------------- enum State { SHOW_HOUR, WAIT_HOUR_OFF, SHOW_MIN_BLOCK, WAIT_MIN_OFF, SHOW_MIN_EXTRA, WAIT_RESTART }; State currentState = SHOW_HOUR; enum Mode { CLOCK_MODE, LAMP_MODE }; Mode currentMode = CLOCK_MODE; enum AdjustType { NONE, HOUR, MIN5, MIN1 }; AdjustType adjustType = NONE; // ---------------- VARIABLES ---------------- unsigned long stateStartTime = 0; unsigned long blinkStart = 0; bool blinking = false; bool ledsOn = false; int extraBlinkCount = 0; int lastSecond = -1; int lastMinute = -1; bool lastHourButtonState = HIGH; bool lastMin5ButtonState = HIGH; bool lastMin1ButtonState = HIGH; bool lastBrightnessButtonState = HIGH; bool lastModeButtonState = HIGH; bool adjusting = false; unsigned long lastAdjustTime = 0; int hourIndex = 1; // 1..4 (LED index usado) bool isPM = false; int minuteIndex = 1; // 1..4 int minuteRemainder = 0; int yellowStep = 0; // 0..4 para MIN1 // Flag para indicar que un botón ha sido pulsado (para interrumpir parpadeos) volatile bool buttonPressedFlag = false; // Pending flags para procesar inmediatamente en handleButtons() volatile bool pendingHourPress = false; volatile bool pendingMin5Press = false; volatile bool pendingMin1Press = false; volatile bool pendingBrightPress = false; unsigned long lastButtonRealPress = 0; // ---------- BRILLO ---------- int brightnessLevels[] = {250, 192, 165, 130, 75, 30, 12, 4, 0}; int brightnessIndex = 0; int numBrightnessLevels = sizeof(brightnessLevels) / sizeof(brightnessLevels[0]); bool inBrightnessMode = false; unsigned long brightnessModeStart = 0; // EEPROM addresses #define EEPROM_ADDR_BRIGHTNESS 0 #define EEPROM_ADDR_HOUR 1 #define EEPROM_ADDR_MINUTE 2 // ---------------- LAMP MODE VARS ---------------- float lampHue = 0.0f; // 0..360 float lampHueSpeed = 0.4f; // velocidad de cambio de tono float breathPhase = 0.0f; float breathSpeed = 0.012f; // velocidad de respiración unsigned long lastLampMillis = 0; const unsigned long lampInterval = 30; // ms entre frames // ----------------- SETUP ----------------- void setup() { for (int i = 0; i < 4; i++) { strips[i].begin(); strips[i].setBrightness(255); strips[i].show(); } pinMode(BUTTON_HOUR, INPUT_PULLUP); pinMode(BUTTON_MINUTE_5, INPUT_PULLUP); pinMode(BUTTON_MINUTE_1, INPUT_PULLUP); pinMode(BUTTON_BRIGHTNESS, INPUT_PULLUP); pinMode(BUTTON_MODE, INPUT_PULLUP); Serial.begin(9600); // --- WATCHDOG: limpiar flags y activarlo --- MCUSR = 0; // limpia flags de reset wdt_disable(); // evita loops si el bootloader dejó el WDT activo wdt_enable(WDTO_8S); // watchdog con timeout ~8s if (!rtc.begin()) { Serial.println("No se encontró el RTC"); while (1); } // Leer brillo guardado brightnessIndex = EEPROM.read(EEPROM_ADDR_BRIGHTNESS); if (brightnessIndex < 0 || brightnessIndex >= numBrightnessLevels) brightnessIndex = 0; for (int i = 0; i < 4; i++) strips[i].setBrightness(brightnessLevels[brightnessIndex]); // Si RTC perdió energía, restaurar desde EEPROM o compilación if (rtc.lostPower()) { int savedHour = EEPROM.read(EEPROM_ADDR_HOUR); int savedMinute = EEPROM.read(EEPROM_ADDR_MINUTE); if (savedHour >= 0 && savedHour < 24 && savedMinute >= 0 && savedMinute < 60) { rtc.adjust(DateTime(2025, 1, 1, savedHour, savedMinute, 0)); Serial.println("RTC restaurado desde EEPROM"); } else { rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); Serial.println("RTC restaurado con fecha de compilación"); } } DateTime now = rtc.now(); prepareDisplayValues(now.hour(), now.minute()); stateStartTime = millis(); lastSecond = now.second(); lastMinute = now.minute(); } // ----------------- Helpers para interrupción amistosa ----------------- // Lee botones y detecta flanco de pulsación sin sobrescribir last* states. // IMPORTANTE: no actualizamos last* aquí para que handleButtons pueda detectar la transición void readButtonsImmediate() { bool currHour = digitalRead(BUTTON_HOUR); bool currMin5 = digitalRead(BUTTON_MINUTE_5); bool currMin1 = digitalRead(BUTTON_MINUTE_1); bool currBright = digitalRead(BUTTON_BRIGHTNESS); bool currMode = digitalRead(BUTTON_MODE); // --- DEBOUNCE REAL SOLO PARA HORA --- if (lastHourButtonState == HIGH && currHour == LOW) { unsigned long now = millis(); if (now - lastButtonRealPress > 120) { lastButtonRealPress = now; pendingHourPress = true; } } // Los otros botones solo despiertan el sistema if (lastMin5ButtonState == HIGH && currMin5 == LOW) buttonPressedFlag = true; if (lastMin1ButtonState == HIGH && currMin1 == LOW) buttonPressedFlag = true; if (lastBrightnessButtonState == HIGH && currBright == LOW) buttonPressedFlag = true; if (lastModeButtonState == HIGH && currMode == LOW) buttonPressedFlag = true; } // Forward declaration for immediate processing inside waitWithButtonCheck void handleButtons(DateTime now); // Espera del tipo delay() pero comprobando botones (si se pulsa, retorna true) // NUEVO: si detecta pulsación, llama IMMEDIATAMENTE a handleButtons(now) para procesarla // Además: si detecta MODE en la espera, lo cambia AL INSTANTE bool waitWithButtonCheck(unsigned long ms) { unsigned long start = millis(); while (millis() - start < ms) { // Aseguramos que el watchdog no reinicie la placa durante esperas largas wdt_reset(); // 1) Actualizamos estados inmediatos (captura flancos con debounce del HOUR) readButtonsImmediate(); // 2) Volvemos a leer pines para detección de flancos en esta iteración bool currHour = digitalRead(BUTTON_HOUR); bool currMin5 = digitalRead(BUTTON_MINUTE_5); bool currMin1 = digitalRead(BUTTON_MINUTE_1); bool currBright = digitalRead(BUTTON_BRIGHTNESS); bool currMode = digitalRead(BUTTON_MODE); // 3) Detección inmediata de MODE → prioridad absoluta if (lastModeButtonState == HIGH && currMode == LOW) { currentMode = (currentMode == CLOCK_MODE) ? LAMP_MODE : CLOCK_MODE; clearAll(); showAll(); lastLampMillis = millis(); adjusting = false; inBrightnessMode = false; // actualizar last* (ya lo hace readButtonsImmediate, pero lo repetimos por seguridad) lastHourButtonState = currHour; lastMin5ButtonState = currMin5; lastMin1ButtonState = currMin1; lastBrightnessButtonState = currBright; lastModeButtonState = currMode; return true; } // 4) Otros botones: activar pending flags correctamente if (lastHourButtonState == HIGH && currHour == LOW) { pendingHourPress = true; return true; } if (lastMin5ButtonState == HIGH && currMin5 == LOW) { pendingMin5Press = true; return true; } if (lastMin1ButtonState == HIGH && currMin1 == LOW) { pendingMin1Press = true; return true; } if (lastBrightnessButtonState == HIGH && currBright == LOW) { pendingBrightPress = true; return true; } // 5) Actualizar los last* para permitir flancos la próxima iteración lastHourButtonState = currHour; lastMin5ButtonState = currMin5; lastMin1ButtonState = currMin1; lastBrightnessButtonState = currBright; lastModeButtonState = currMode; delay(10); } return false; } // ----------------- LOOP ----------------- void loop() { handleButtons(rtc.now()); // primero procesar botones // --- MODO LÁMPARA --- if (currentMode == LAMP_MODE) { // Ejecutar animación no bloqueante runLampMode_nonBlocking(); // imprimir SIEMPRE la hora aunque estés en lámpara (monitor serie) { DateTime t = rtc.now(); if (t.second() != lastSecond) { lastSecond = t.second(); printTime(t); } } // Muy importante: resetear el watchdog aquí antes de volver a la espera/return wdt_reset(); // No hacemos return prolongado sin resetear watchdog: return; } DateTime now = rtc.now(); // --- imprimir SIEMPRE cada segundo --- if (now.second() != lastSecond) { lastSecond = now.second(); printTime(now); } // --- MODO BRILLO --- if (inBrightnessMode) { if (millis() - brightnessModeStart >= 2000) { inBrightnessMode = false; currentState = SHOW_HOUR; stateStartTime = millis(); } // Asegurar watchdog mientras estamos en este modo wdt_reset(); return; } // --- actualización automática del reloj cada minuto --- if (!adjusting) { int nowMinute = now.minute(); if (nowMinute != lastMinute) { if (nowMinute == 0) runHourChangeAnimation(); lastMinute = nowMinute; // Actualizamos los índices que se usarán para la próxima iteración, // pero NO forzamos un restart del state actual para evitar cortar la animación en curso. prepareDisplayValues(now.hour(), nowMinute); // Si quieres forzar reinicio inmediato en situaciones concretas // puedes descomentar la siguiente línea, pero por defecto la dejamos // comentada para evitar interrupciones: // currentState = SHOW_HOUR; // stateStartTime = millis(); } } // --- modo ajuste de MIN1 --- if (adjusting && adjustType == MIN1) { clearAll(); if (minuteRemainder == 0) { setAllAtHeight(1, strips[0].Color(255, 255, 0)); } else { for (int i = 1; i <= minuteRemainder; i++) setAllAtHeight(i, strips[0].Color(255, 255, 255)); } showAll(); if (millis() - lastAdjustTime > 2000) { adjusting = false; adjustType = NONE; } wdt_reset(); return; } // --- ESTADOS --- switch (currentState) { case SHOW_HOUR: displayHourPattern(); break; case SHOW_MIN_BLOCK: displayMinuteBlocks(); break; case SHOW_MIN_EXTRA: displayMinuteResiduals(); break; case WAIT_HOUR_OFF: case WAIT_MIN_OFF: case WAIT_RESTART: if (millis() - stateStartTime >= 1000) currentState = SHOW_HOUR; break; } // --- imprimir SIEMPRE al final --- { DateTime t = rtc.now(); if (t.second() != lastSecond) { lastSecond = t.second(); printTime(t); } } wdt_reset(); } // ---------------- FUNCIONES DE PARPADEO ----------------- // IMPORTANTE: usamos waitWithButtonCheck() en lugar de delay para permitir interrupción inmediata void displayHourPattern() { clearAll(); int blocks = hourIndex / 4; int remainder = hourIndex % 4; uint32_t color = isPM ? strips[0].Color(255,0,0) : strips[0].Color(0,0,255); for (int b = 0; b < blocks; b++) { for (int k = 1; k <= 4; k++) setAllAtHeight(k, color); showAll(); if (waitWithButtonCheck(1500)) { buttonPressedFlag = false; currentState = SHOW_HOUR; // aborta animación return; } clearAll(); showAll(); if (waitWithButtonCheck(1000)) { buttonPressedFlag = false; currentState = SHOW_HOUR; // aborta animación return; } } for (int r = 1; r <= remainder; r++) setAllAtHeight(r, color); if (remainder>0) { showAll(); if (waitWithButtonCheck(1500)) { buttonPressedFlag = false; currentState = SHOW_HOUR; // aborta animación return; } clearAll(); showAll(); if (waitWithButtonCheck(1000)) { buttonPressedFlag = false; currentState = SHOW_HOUR; // aborta animación return; } } currentState = SHOW_MIN_BLOCK; stateStartTime = millis(); } void displayMinuteBlocks() { clearAll(); int blocks = minuteIndex / 4; int remainder = minuteIndex % 4; for (int b = 0; b < blocks; b++) { for (int k = 1; k <= 4; k++) setAllAtHeight(k, strips[0].Color(0,255,0)); showAll(); if (waitWithButtonCheck(1500)) { currentState = SHOW_HOUR; return; } clearAll(); showAll(); if (waitWithButtonCheck(1000)) { currentState = SHOW_HOUR; return; } } for (int r = 1; r <= remainder; r++) setAllAtHeight(r, strips[0].Color(0,255,0)); if (remainder>0) { showAll(); if (waitWithButtonCheck(1500)) { return; } clearAll(); showAll(); if (waitWithButtonCheck(1000)) { return; } } currentState = SHOW_MIN_EXTRA; stateStartTime = millis(); } void displayMinuteResiduals() { clearAll(); for (int i = 1; i <= minuteRemainder; i++) setAllAtHeight(i, strips[0].Color(255,255,255)); if (minuteRemainder>0) { showAll(); if (waitWithButtonCheck(1500)) { currentState = SHOW_HOUR; return; } clearAll(); showAll(); if (waitWithButtonCheck(1000)) { currentState = SHOW_HOUR; return; } } currentState = WAIT_RESTART; stateStartTime = millis(); } // ----------------- BOTONES ----------------- // handleButtons contiene la lógica completa, con la modificación pedida void handleButtons(DateTime now) { // leemos estado actual de pines bool currHour = digitalRead(BUTTON_HOUR); bool currMin5 = digitalRead(BUTTON_MINUTE_5); bool currMin1 = digitalRead(BUTTON_MINUTE_1); bool currBright = digitalRead(BUTTON_BRIGHTNESS); bool currMode = digitalRead(BUTTON_MODE); unsigned long nowMS = millis(); // ---- DETECCIÓN SIMPLE Y FIABLE DE BOTÓN MODE ---- if (lastModeButtonState == HIGH && currMode == LOW) { currentMode = (currentMode == CLOCK_MODE) ? LAMP_MODE : CLOCK_MODE; clearAll(); showAll(); lastLampMillis = millis(); // reinicia animación de lámpara // actualizar last* para evitar rebotes lastModeButtonState = currMode; lastHourButtonState = currHour; lastMin5ButtonState = currMin5; lastMin1ButtonState = currMin1; lastBrightnessButtonState = currBright; return; // ***IMPORTANTE*** evita que siga procesando } // --- Primero: si hay pending presses detectadas durante animación, procesarlas PRIORITARIAMENTE --- if (pendingHourPress) { // Si han pasado >2s desde el último ajuste -> reset a 00, si no -> +1h if (nowMS - lastAdjustTime > 2000) { int newHour = 0; int newMinute = rtc.now().minute(); rtc.adjust(DateTime(rtc.now().year(), rtc.now().month(), rtc.now().day(), newHour, newMinute, 0)); EEPROM.update(EEPROM_ADDR_HOUR, newHour); EEPROM.update(EEPROM_ADDR_MINUTE, newMinute); hourIndex = newHour % 12; if (hourIndex == 0) hourIndex = 12; isPM = newHour >= 12; adjusting = true; adjustType = HOUR; lastAdjustTime = nowMS; } else { int newHour = rtc.now().hour() + 1; if (newHour >= 24) newHour = 0; rtc.adjust(DateTime(rtc.now().year(), rtc.now().month(), rtc.now().day(), newHour, rtc.now().minute(), 0)); EEPROM.update(EEPROM_ADDR_HOUR, newHour); EEPROM.update(EEPROM_ADDR_MINUTE, rtc.now().minute()); hourIndex = newHour % 12; if (hourIndex == 0) hourIndex = 12; isPM = newHour >= 12; adjusting = true; adjustType = HOUR; lastAdjustTime = nowMS; } // limpiar y salir para evitar doble procesamiento pendingHourPress = false; buttonPressedFlag = false; // actualizar last* antes de salir lastHourButtonState = currHour; lastMin5ButtonState = currMin5; lastMin1ButtonState = currMin1; lastBrightnessButtonState = currBright; lastModeButtonState = currMode; return; } if (pendingMin5Press) { int currentHour = rtc.now().hour(); int currentMinute = rtc.now().minute(); int blockMinute = (currentMinute / 5) * 5; if (nowMS - lastAdjustTime > 2000) { int newMinute = 0; rtc.adjust(DateTime(rtc.now().year(), rtc.now().month(), rtc.now().day(), currentHour, newMinute, 0)); EEPROM.update(EEPROM_ADDR_MINUTE, newMinute); minuteIndex = 12; // 00 -> block 12 minuteRemainder = 0; adjusting = true; adjustType = MIN5; lastAdjustTime = nowMS; } else { int newMinute = blockMinute + 5; if (newMinute >= 60) newMinute = 0; rtc.adjust(DateTime(rtc.now().year(), rtc.now().month(), rtc.now().day(), currentHour, newMinute, 0)); EEPROM.update(EEPROM_ADDR_MINUTE, newMinute); minuteIndex = (newMinute / 5); if (minuteIndex == 0) minuteIndex = 12; minuteRemainder = 0; adjusting = true; adjustType = MIN5; lastAdjustTime = nowMS; } pendingMin5Press = false; buttonPressedFlag = false; // actualizar last* antes de salir lastHourButtonState = currHour; lastMin5ButtonState = currMin5; lastMin1ButtonState = currMin1; lastBrightnessButtonState = currBright; lastModeButtonState = currMode; return; } if (pendingMin1Press) { unsigned long currentTime = millis(); int currentHour = rtc.now().hour(); int currentMinute = rtc.now().minute(); int blockMinute = (currentMinute / 5) * 5; if (currentTime - lastAdjustTime > 2000) { yellowStep = 0; currentMinute = blockMinute; } else { yellowStep++; if (yellowStep > 4) yellowStep = 0; currentMinute = blockMinute + yellowStep; if (currentMinute >= 60) currentMinute -= 60; } rtc.adjust(DateTime(now.year(), now.month(), now.day(), currentHour, currentMinute, 0)); EEPROM.update(EEPROM_ADDR_HOUR, currentHour); EEPROM.update(EEPROM_ADDR_MINUTE, currentMinute); minuteRemainder = yellowStep; adjusting = true; adjustType = MIN1; lastAdjustTime = currentTime; prepareDisplayValues(currentHour, currentMinute); pendingMin1Press = false; buttonPressedFlag = false; // actualizar last* antes de salir lastHourButtonState = currHour; lastMin5ButtonState = currMin5; lastMin1ButtonState = currMin1; lastBrightnessButtonState = currBright; lastModeButtonState = currMode; return; } if (pendingBrightPress) { brightnessIndex++; if (brightnessIndex >= numBrightnessLevels) brightnessIndex = 0; for (int i = 0; i < 4; i++) strips[i].setBrightness(brightnessLevels[brightnessIndex]); EEPROM.update(EEPROM_ADDR_BRIGHTNESS, brightnessIndex); inBrightnessMode = true; brightnessModeStart = millis(); clearAll(); uint32_t color = isPM ? strips[0].Color(255,0,0) : strips[0].Color(0,0,255); for (int h = 1; h < NUM_LEDS; h++) setAllAtHeight(h, color); showAll(); pendingBrightPress = false; buttonPressedFlag = false; // actualizar last* antes de salir lastHourButtonState = currHour; lastMin5ButtonState = currMin5; lastMin1ButtonState = currMin1; lastBrightnessButtonState = currBright; lastModeButtonState = currMode; return; } // --- Segundo: procesar transiciones normales (cuando no vinieron por pending) --- // HORA (ruta normal cuando no vino por pending) if (!pendingHourPress && lastHourButtonState == HIGH && currHour == LOW) { if (nowMS - lastAdjustTime > 2000) { int newHour = 0; int newMinute = rtc.now().minute(); rtc.adjust(DateTime(rtc.now().year(), rtc.now().month(), rtc.now().day(), newHour, newMinute, 0)); EEPROM.update(EEPROM_ADDR_HOUR, newHour); EEPROM.update(EEPROM_ADDR_MINUTE, newMinute); hourIndex = newHour % 12; if (hourIndex == 0) hourIndex = 12; isPM = newHour >= 12; adjusting = true; adjustType = HOUR; lastAdjustTime = nowMS; } else { int newHour = rtc.now().hour() + 1; if (newHour >= 24) newHour = 0; rtc.adjust(DateTime(rtc.now().year(), rtc.now().month(), rtc.now().day(), newHour, rtc.now().minute(), 0)); EEPROM.update(EEPROM_ADDR_HOUR, newHour); EEPROM.update(EEPROM_ADDR_MINUTE, rtc.now().minute()); hourIndex = newHour % 12; if (hourIndex == 0) hourIndex = 12; isPM = newHour >= 12; adjusting = true; adjustType = HOUR; lastAdjustTime = nowMS; } } // MIN5 (ruta normal) if (!pendingMin5Press && lastMin5ButtonState == HIGH && currMin5 == LOW) { int currentHour = rtc.now().hour(); int currentMinute = rtc.now().minute(); int blockMinute = (currentMinute / 5) * 5; if (nowMS - lastAdjustTime > 2000) { int newMinute = 0; rtc.adjust(DateTime(rtc.now().year(), rtc.now().month(), rtc.now().day(), currentHour, newMinute, 0)); EEPROM.update(EEPROM_ADDR_MINUTE, newMinute); minuteIndex = 12; minuteRemainder = 0; adjusting = true; adjustType = MIN5; lastAdjustTime = nowMS; } else { int newMinute = blockMinute + 5; if (newMinute >= 60) newMinute = 0; rtc.adjust(DateTime(rtc.now().year(), rtc.now().month(), rtc.now().day(), currentHour, newMinute, 0)); EEPROM.update(EEPROM_ADDR_MINUTE, newMinute); minuteIndex = (newMinute / 5); if (minuteIndex == 0) minuteIndex = 12; minuteRemainder = 0; adjusting = true; adjustType = MIN5; lastAdjustTime = nowMS; } } // MIN1 (ruta normal) if (!pendingMin1Press && lastMin1ButtonState == HIGH && currMin1 == LOW) { unsigned long currentTime = millis(); int currentHour = rtc.now().hour(); int currentMinute = rtc.now().minute(); int blockMinute = (currentMinute / 5) * 5; if (currentTime - lastAdjustTime > 2000) { yellowStep = 0; currentMinute = blockMinute; } else { yellowStep++; if (yellowStep > 4) yellowStep = 0; currentMinute = blockMinute + yellowStep; if (currentMinute >= 60) currentMinute -= 60; } rtc.adjust(DateTime(now.year(), now.month(), now.day(), currentHour, currentMinute, 0)); EEPROM.update(EEPROM_ADDR_HOUR, currentHour); EEPROM.update(EEPROM_ADDR_MINUTE, currentMinute); minuteRemainder = yellowStep; adjusting = true; adjustType = MIN1; lastAdjustTime = currentTime; prepareDisplayValues(currentHour, currentMinute); } // BRILLO (ruta normal) if (!pendingBrightPress && lastBrightnessButtonState == HIGH && currBright == LOW) { brightnessIndex++; if (brightnessIndex >= numBrightnessLevels) brightnessIndex = 0; for (int i = 0; i < 4; i++) strips[i].setBrightness(brightnessLevels[brightnessIndex]); EEPROM.update(EEPROM_ADDR_BRIGHTNESS, brightnessIndex); inBrightnessMode = true; brightnessModeStart = millis(); clearAll(); uint32_t color = isPM ? strips[0].Color(255,0,0) : strips[0].Color(0,0,255); for (int h = 1; h < NUM_LEDS; h++) setAllAtHeight(h, color); showAll(); } // --- Actualizamos last* al final para mantener la lógica de detección de flancos --- lastHourButtonState = currHour; lastMin5ButtonState = currMin5; lastMin1ButtonState = currMin1; lastBrightnessButtonState = currBright; lastModeButtonState = currMode; } // ----------------- FUNCIONES AUXILIARES ----------------- void prepareDisplayValues(int hour, int minute) { hourIndex = hour % 12; if (hourIndex == 0) hourIndex = 12; isPM = hour >= 12; minuteIndex = minute / 5; // 0..11 if (minuteIndex == 0) minuteIndex = 0; minuteRemainder = minute % 5; // 0..4 } void setAllAtHeight(int ledIndex, uint32_t color) { if (ledIndex < 1 || ledIndex >= NUM_LEDS) return; for (int s = 0; s < 4; s++) strips[s].setPixelColor(ledIndex, color); } void clearAll() { for (int s = 0; s < 4; s++) { for (int j = 0; j < NUM_LEDS; j++) strips[s].setPixelColor(j, 0); } } void showAll() { for (int s = 0; s < 4; s++) strips[s].show(); } void runHourChangeAnimation() { uint32_t color = isPM ? strips[0].Color(255,0,0) : strips[0].Color(0,0,255); for (int rep = 0; rep < 2; rep++) { // SUBIR 1 → 4 for (int p = 1; p < NUM_LEDS; p++) { clearAll(); setAllAtHeight(p, color); showAll(); if (waitWithButtonCheck(150)) return; } // BAJAR 3 → 1 for (int p = NUM_LEDS - 2; p >= 1; p--) { clearAll(); setAllAtHeight(p, color); showAll(); if (waitWithButtonCheck(150)) return; } } clearAll(); showAll(); } // ----------------- MODO LÁMPARA (NO BLOQUEANTE) ----------------- void runLampMode_nonBlocking() { static float baseHue = 0; // Hue principal 0..360 const float hueSpeed = 0.7f; // Velocidad del arco iris const unsigned long updateTime = 25; // Velocidad de refresco (ms) static unsigned long lastUpdate = 0; unsigned long now = millis(); if (now - lastUpdate < updateTime) return; lastUpdate = now; // Asegurar watchdog en cada frame wdt_reset(); // Avanzar el arco iris baseHue += hueSpeed; if (baseHue >= 360.0f) baseHue -= 360.0f; for (int s = 0; s < 4; s++) { // Las 4 tiras deben ser iguales for (int i = 1; i < NUM_LEDS; i++) { // LED 1..4, LED 0 apagado // 4 LEDs → 4 fases del arco iris separadas por 90° float hue = fmod(baseHue + (i - 1) * 90.0f, 360.0f); uint32_t rgb = hsvToRgbInt(hue, 1.0f, 1.0f); // Saturación 1, brillo 1 strips[s].setPixelColor(i, rgb); } // LED 0 apagado strips[s].setPixelColor(0, 0); strips[s].show(); } } uint32_t hsvToRgbInt(float h, float s, float v) { float r, g, b; if (s <= 0.0f) { r = g = b = v; } else { float hh = h; if (hh >= 360.0f) hh = 0.0f; hh /= 60.0f; long i = (long)hh; float ff = hh - i; float p = v * (1.0f - s); float q = v * (1.0f - (s * ff)); float t = v * (1.0f - (s * (1.0f - ff))); switch(i) { case 0: r = v; g = t; b = p; break; case 1: r = q; g = v; b = p; break; case 2: r = p; g = v; b = t; break; case 3: r = p; g = q; b = v; break; case 4: r = t; g = p; b = v; break; default: r = v; g = p; b = q; break; } } uint8_t R = (uint8_t)(r * 255.0f); uint8_t G = (uint8_t)(g * 255.0f); uint8_t B = (uint8_t)(b * 255.0f); return ((uint32_t)R << 16) | ((uint32_t)G << 8) | (uint32_t)B; } // ----------------- UTIL ----------------- void printTime(DateTime t) { Serial.print("Hora: "); if (t.hour() < 10) Serial.print('0'); Serial.print(t.hour()); Serial.print(":"); if (t.minute() < 10) Serial.print('0'); Serial.print(t.minute()); Serial.print(":"); if (t.second() < 10) Serial.print('0'); Serial.println(t.second()); }