// =================================================================================
// PARTE 1 DE 4 - PROGRAMA UNIFICADO COMPLETO: OLA HORIZONTAL DE BARRAS RÍGIDAS
// =================================================================================
#include <Arduino.h>
#include <Wire.h>
#include <RTClib.h>
RTC_DS1307 rtc;
#define DATA_PIN 23
#define CLOCK_PIN 18
#define LATCH_PIN 5
const int PIN_BOTON_HORA = 12;
const int PIN_BOTON_MIN = 14;
const int PIN_BOTON_HOME = 27;
#define NUM_MOTORES 24
#define TOTAL_MOTORES 48
const unsigned long TIEMPO_TRANSICION_MS = 2000;
const uint8_t seqMin[] = { 0b1010, 0b0110, 0b0101, 0b1001 };
const uint8_t seqHora[] = { 0b1010, 0b0110, 0b0101, 0b1001 };
uint8_t motorState[TOTAL_MOTORES] = {0};
int posMin[NUM_MOTORES] = {0};
int posHora[NUM_MOTORES] = {0};
int ultimaHoraMostrada = -1;
int ultimoMinutoMostrado = -1;
int minutoDeUltimaCoreografia = -1;
// FUENTES CONFIGURADAS CON DIMENSIONES NUMÉRICAS FIJAS EXPLÍCITAS
const int fuentesHora[10][6] = {
{3, 6, 3, 9, 6, 9}, {8, 8, 8, 6, 6, 12}, {3, 3, 3, 9, 9, 9}, {3, 3, 3, 9, 9, 9}, {6, 3, 8, 6, 9, 12},
{3, 3, 3, 9, 9, 9}, {3, 6, 3, 9, 6, 9}, {3, 8, 8, 9, 6, 12}, {3, 3, 3, 9, 9, 9}, {3, 12, 3, 9, 6, 9}
};
const int fuentesMin[10][6] = {
{30, 0, 0, 30, 0, 0}, {40, 40, 40, 30, 0, 0}, {15, 30, 0, 30, 0, 45}, {15, 15, 15, 30, 0, 0}, {30, 0, 40, 30, 30, 0},
{30, 0, 15, 45, 30, 0}, {30, 0, 0, 45, 45, 0}, {15, 40, 40, 30, 0, 0}, {30, 0, 0, 30, 0, 0}, {30, 15, 15, 30, 0, 0}
};
unsigned long ultimoRefrescoSerie = 0;
bool estadoAnteriorHora = HIGH;
bool estadoAnteriorMin = HIGH;
bool estadoAnteriorHome = HIGH;
void update595(uint8_t bytes[]) {
digitalWrite(LATCH_PIN, LOW);
for (int sr = 23; sr >= 0; sr--) {
shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, bytes[sr]);
}
digitalWrite(LATCH_PIN, HIGH);
}
void refreshOutputs() {
uint8_t sr[NUM_MOTORES] = {0};
for (int m = 0; m < NUM_MOTORES; m++) {
sr[m] |= seqHora[motorState[m + NUM_MOTORES] & 0x03];
sr[m] |= (seqMin[motorState[m] & 0x03] << 4);
}
update595(sr);
}
void stepMin(int reloj, int dir) {
int motor = reloj;
if (dir > 0) motorState[motor]++; else motorState[motor]--;
posMin[reloj] = (posMin[reloj] + dir + 200) % 200;
}
void stepHora(int reloj, int dir) {
int motor = reloj + NUM_MOTORES;
if (dir > 0) motorState[motor]++; else motorState[motor]--;
posHora[reloj] = (posHora[reloj] + dir + 200) % 200;
}
int horaAPasos(int h) {
if (h == 12 || h == 0) return 0;
if (h == 3) return 50;
if (h == 6) return 100;
if (h == 9) return 150;
return map(h, 0, 12, 0, 200) % 200;
}
int minAPasos(int m) {
if (m == 0) return 0;
if (m == 15) return 50;
if (m == 30) return 100;
if (m == 45) return 150;
return map(m, 0, 60, 0, 200) % 200;
}
// =================================================================================
// PARTE 2 DE 4 - FUNCIONES DE MOVIMIENTO BASE
// =================================================================================
void moverMatrizAlUnisono(const int targetH[NUM_MOTORES], const int targetM[NUM_MOTORES]) {
int pasosHora[NUM_MOTORES], pasosMin[NUM_MOTORES];
int dirHora[NUM_MOTORES], dirMin[NUM_MOTORES];
int pasosDadosH[NUM_MOTORES], pasosDadosM[NUM_MOTORES];
for (int i = 0; i < NUM_MOTORES; i++) {
pasosDadosH[i] = 0;
pasosDadosM[i] = 0;
}
for (int r = 0; r < NUM_MOTORES; r++) {
int deltaH = (targetH[r] - posHora[r] + 200) % 200;
int deltaM = (targetM[r] - posMin[r] + 200) % 200;
if (deltaH <= 100) { pasosHora[r] = deltaH; dirHora[r] = +1; } else { pasosHora[r] = 200 - deltaH; dirHora[r] = -1; }
if (deltaM <= 100) { pasosMin[r] = deltaM; dirMin[r] = +1; } else { pasosMin[r] = 200 - deltaM; dirMin[r] = -1; }
}
unsigned long tiempoInicio = millis();
unsigned long tiempoActual = 0;
while (tiempoActual < TIEMPO_TRANSICION_MS) {
tiempoActual = millis() - tiempoInicio;
bool huboMovimiento = false;
for (int r = 0; r < NUM_MOTORES; r++) {
if (pasosHora[r] > 0 && pasosDadosH[r] < pasosHora[r]) {
if (tiempoActual >= (TIEMPO_TRANSICION_MS * (pasosDadosH[r] + 1)) / pasosHora[r]) {
stepHora(r, dirHora[r]); pasosDadosH[r]++; huboMovimiento = true;
}
}
if (pasosMin[r] > 0 && pasosDadosM[r] < pasosMin[r]) {
if (tiempoActual >= (TIEMPO_TRANSICION_MS * (pasosDadosM[r] + 1)) / pasosMin[r]) {
stepMin(r, dirMin[r]); pasosDadosM[r]++; huboMovimiento = true;
}
}
}
if (huboMovimiento) refreshOutputs();
delay(1);
}
for (int r = 0; r < NUM_MOTORES; r++) {
while (pasosDadosH[r] < pasosHora[r]) { stepHora(r, dirHora[r]); pasosDadosH[r]++; }
while (pasosDadosM[r] < pasosMin[r]) { stepMin(r, dirMin[r]); pasosDadosM[r]++; }
}
refreshOutputs();
}
void actualizarRelojCompleto(int h, int m) {
if (h != ultimaHoraMostrada || m != ultimoMinutoMostrado) {
ultimaHoraMostrada = h;
ultimoMinutoMostrado = m;
Serial.print("-> [SISTEMA] Mostrando hora estable -> ");
if(h < 10) Serial.print("0"); Serial.print(h); Serial.print(":");
if(m < 10) Serial.print("0"); Serial.println(m);
int d0 = h / 10;
int d1 = h % 10;
int d2 = m / 10;
int d3 = m % 10;
int targetsH[NUM_MOTORES], targetsM[NUM_MOTORES];
for (int r = 0; r < 6; r++) {
targetsH[r] = horaAPasos(fuentesHora[d0][r]); targetsM[r] = minAPasos(fuentesMin[d0][r]);
targetsH[r+6] = horaAPasos(fuentesHora[d1][r]); targetsM[r+6] = minAPasos(fuentesMin[d1][r]);
targetsH[r+12] = horaAPasos(fuentesHora[d2][r]); targetsM[r+12] = minAPasos(fuentesMin[d2][r]);
targetsH[r+18] = horaAPasos(fuentesHora[d3][r]); targetsM[r+18] = minAPasos(fuentesMin[d3][r]);
}
moverMatrizAlUnisono(targetsH, targetsM);
}
}
// =================================================================================
// PARTE 3 DE 4 - COREOGRAFÍA "ABANICOS INTERCALADO" (F1: ROMBOS A/B + F2: DELAY 50)
// =================================================================================
void ejecutarNuevaCoreografia(int horaSiguiente, int minutoSiguiente) {
unsigned long cronometroInternoSerie = millis();
// --- CONFIGURACIÓN DE LOS DESTINOS FINALES DEL NUEVO MINUTO ---
int d0 = horaSiguiente / 10; int d1 = horaSiguiente % 10;
int d2 = minutoSiguiente / 10; int d3 = minutoSiguiente % 10;
int targetsH[NUM_MOTORES], targetsM[NUM_MOTORES];
for (int r = 0; r < 6; r++) {
targetsH[r] = horaAPasos(fuentesHora[d0][r]); targetsM[r] = minAPasos(fuentesMin[d0][r]);
targetsH[r+6] = horaAPasos(fuentesHora[d1][r]); targetsM[r+6] = minAPasos(fuentesMin[d1][r]);
targetsH[r+12] = horaAPasos(fuentesHora[d2][r]); targetsM[r+12] = minAPasos(fuentesMin[d2][r]);
targetsH[r+18] = horaAPasos(fuentesHora[d3][r]); targetsM[r+18] = minAPasos(fuentesMin[d3][r]);
}
// --- FASE 1: VIAJE AL UNÍSONO INTERCALADO EN GEOMETRÍA ROMBOS A/B (4.2s) ---
const unsigned long DURACION_FASE_1_MS = 4200;
int pasosH1[NUM_MOTORES], pasosM1[NUM_MOTORES];
int dadosH1[NUM_MOTORES], dadosM1[NUM_MOTORES];
int dirH2[NUM_MOTORES], dirM2[NUM_MOTORES];
int posF1FinalH[NUM_MOTORES], posF1FinalM[NUM_MOTORES];
for (int i = 0; i < NUM_MOTORES; i++) { dadosH1[i] = 0; dadosM1[i] = 0; }
for (int r = 0; r < NUM_MOTORES; r++) {
int ubicacionFila = r % 3; // 0 = Arriba, 1 = Centro, 2 = Abajo
int columnaHorizontal = r / 3; // Columna de 0 a 7 de izquierda a derecha
int posMetaH = 0, posMetaM = 0;
// Sentidos de Fase 2 fijos: Horas Horario (+1), Minutos Antihorario (-1)
dirH2[r] = 1;
dirM2[r] = -1;
// LÓGICA DE INTERCALADO DINÁMICO A/B (A = 0 pasos, B = 100 pasos)
if (ubicacionFila == 0 || ubicacionFila == 2) {
// Filas superior e inferior: A, B, A, B, A, B, A, B
if (columnaHorizontal % 2 == 0) { posMetaH = 0; posMetaM = 0; } // Posición A (12:00)
else { posMetaH = 100; posMetaM = 100; } // Posición B (06:30)
}
else {
// Fila intermedia contrapuesta: B, A, B, A, B, A, B, A
if (columnaHorizontal % 2 == 0) { posMetaH = 100; posMetaM = 100; } // Posición B (06:30)
else { posMetaH = 0; posMetaM = 0; } // Posición A (12:00)
}
posF1FinalH[r] = posMetaH;
posF1FinalM[r] = posMetaM;
pasosH1[r] = (posMetaH - posHora[r] + 200) % 200;
pasosM1[r] = (posMetaM - posMin[r] + 200) % 200;
if (pasosH1[r] == 0) pasosH1[r] = 200;
if (pasosM1[r] == 0) pasosM1[r] = 200;
}
unsigned long inicioFase1 = millis();
unsigned long transcurridoFase1 = 0;
while (transcurridoFase1 < DURACION_FASE_1_MS) {
if (digitalRead(PIN_BOTON_HORA) == LOW || digitalRead(PIN_BOTON_MIN) == LOW) return;
transcurridoFase1 = millis() - inicioFase1;
bool moviendo = false;
for (int r = 0; r < NUM_MOTORES; r++) {
if (pasosH1[r] > 0 && dadosH1[r] < pasosH1[r]) {
if (transcurridoFase1 >= (DURACION_FASE_1_MS * (dadosH1[r] + 1)) / pasosH1[r]) {
stepHora(r, 1); dadosH1[r]++; moviendo = true;
}
}
if (pasosM1[r] > 0 && dadosM1[r] < pasosM1[r]) {
if (transcurridoFase1 >= (DURACION_FASE_1_MS * (dadosM1[r] + 1)) / pasosM1[r]) {
stepMin(r, 1); dadosM1[r]++; moviendo = true;
}
}
}
if (moviendo) refreshOutputs();
if (millis() - cronometroInternoSerie >= 1000) {
cronometroInternoSerie = millis();
DateTime s_ahora = rtc.now();
Serial.print("Tiempo Real RTC Sincronizado (ABANICOS F1) -> ");
if(s_ahora.hour() < 10) Serial.print("0"); Serial.print(s_ahora.hour()); Serial.print(":");
if(s_ahora.minute() < 10) Serial.print("0"); Serial.print(s_ahora.minute()); Serial.print(":");
if(s_ahora.second() < 10) Serial.print("0"); Serial.println(s_ahora.second());
}
delay(1);
}
for (int r = 0; r < NUM_MOTORES; r++) {
while (dadosH1[r] < pasosH1[r]) { stepHora(r, 1); dadosH1[r]++; }
while (dadosM1[r] < pasosM1[r]) { stepMin(r, 1); dadosM1[r]++; }
}
refreshOutputs();
// --- FASE 2: 4 VUELTAS EN VAIVÉN EN SENTIDOS OPUESTOS CON DELAY(50) ---
for (int paso = 0; paso < 800; paso++) {
if (digitalRead(PIN_BOTON_HORA) == LOW || digitalRead(PIN_BOTON_MIN) == LOW) return;
for (int r = 0; r < NUM_MOTORES; r++) {
if (paso < 400) {
stepHora(r, dirH2[r]);
stepMin(r, dirM2[r]);
} else {
stepHora(r, dirH2[r] * -1);
stepMin(r, dirM2[r] * -1);
}
}
refreshOutputs();
if (millis() - cronometroInternoSerie >= 1000) {
cronometroInternoSerie = millis();
DateTime s_ahora = rtc.now();
Serial.print("Tiempo Real RTC Sincronizado (ABANICOS F2) -> ");
if(s_ahora.hour() < 10) Serial.print("0"); Serial.print(s_ahora.hour()); Serial.print(":");
if(s_ahora.minute() < 10) Serial.print("0"); Serial.print(s_ahora.minute()); Serial.print(":");
if(s_ahora.second() < 10) Serial.print("0"); Serial.println(s_ahora.second());
}
delay(50);
}
// --- FASE 3: DESPLIEGUE CON INERCIA VISUAL CONTINUA DE 6.5 SEGUNDOS ---
const unsigned long DURACION_FASE_3_MS = 6500;
int pasosH3[NUM_MOTORES], pasosM3[NUM_MOTORES];
int dadosH3[NUM_MOTORES], dadosM3[NUM_MOTORES];
int maxPasosF3 = 0;
for (int i = 0; i < NUM_MOTORES; i++) { dadosH3[i] = 0; dadosM3[i] = 0; }
for (int r = 0; r < NUM_MOTORES; r++) {
posHora[r] = posF1FinalH[r];
posMin[r] = posF1FinalM[r];
pasosH3[r] = (posHora[r] - targetsH[r] + 200) % 200;
pasosM3[r] = (targetsM[r] - posMin[r] + 200) % 200;
if (pasosH3[r] == 0) pasosH3[r] = 200;
if (pasosM3[r] == 0) pasosM3[r] = 200;
if (pasosH3[r] > maxPasosF3) maxPasosF3 = pasosH3[r];
if (pasosM3[r] > maxPasosF3) maxPasosF3 = pasosM3[r];
}
if (maxPasosF3 == 0) maxPasosF3 = 1;
unsigned long inicioF3 = millis();
unsigned long transcurridoF3 = 0;
int ultimoPasoComunF3 = 0;
while (transcurridoF3 < DURACION_FASE_3_MS) {
if (digitalRead(PIN_BOTON_HORA) == LOW || digitalRead(PIN_BOTON_MIN) == LOW) return;
transcurridoF3 = millis() - inicioF3;
int pasoComunDeseado = (transcurridoF3 * maxPasosF3) / DURACION_FASE_3_MS;
if (pasoComunDeseado > ultimoPasoComunF3 && ultimoPasoComunF3 < maxPasosF3) {
ultimoPasoComunF3++;
bool moviendo = false;
for (int r = 0; r < NUM_MOTORES; r++) {
int metaH = (ultimoPasoComunF3 * pasosH3[r]) / maxPasosF3;
int metaM = (ultimoPasoComunF3 * pasosM3[r]) / maxPasosF3;
if (dadosH3[r] < metaH && dadosH3[r] < pasosH3[r]) {
stepHora(r, -1); dadosH3[r]++; moviendo = true;
}
if (dadosM3[r] < metaM && dadosM3[r] < pasosM3[r]) {
stepMin(r, 1); dadosM3[r]++; moviendo = true;
}
}
if (moviendo) refreshOutputs();
}
if (millis() - cronometroInternoSerie >= 1000) {
cronometroInternoSerie = millis();
DateTime s_ahora = rtc.now();
Serial.print("Tiempo Real RTC Sincronizado (ABANICOS F3) -> ");
if(s_ahora.hour() < 10) Serial.print("0"); Serial.print(s_ahora.hour()); Serial.print(":");
if(s_ahora.minute() < 10) Serial.print("0"); Serial.print(s_ahora.minute()); Serial.print(":");
if(s_ahora.second() < 10) Serial.print("0"); Serial.println(s_ahora.second());
}
delay(1);
}
for (int r = 0; r < NUM_MOTORES; r++) {
while (dadosH3[r] < pasosH3[r]) { stepHora(r, -1); dadosH3[r]++; }
while (dadosM3[r] < pasosM3[r]) { stepMin(r, 1); dadosM3[r]++; }
}
refreshOutputs();
ultimaHoraMostrada = horaSiguiente;
ultimoMinutoMostrado = minutoSiguiente;
}
// =================================================================================
// PARTE 4 DE 4 - FUNCIONES DE INICIO, LOOP PRINCIPAL Y MÁQUINA DE AJUSTE ULTRA RÁPIDO (150ms)
// =================================================================================
int horaAjusteManual = 0; // Guarda el contador de horas limpio para el modo ajuste
void ejecutarHomingManual() {
Serial.println("\n-> [SISTEMA] ¡Iniciando Homing general en los 24 motores a las 6:00...");
int targetsHoming[NUM_MOTORES];
int targetsMinit[NUM_MOTORES];
for (int r = 0; r < NUM_MOTORES; r++) {
targetsHoming[r] = 100;
targetsMinit[r] = 100;
}
moverMatrizAlUnisono(targetsHoming, targetsMinit);
for (int r = 0; r < NUM_MOTORES; r++) {
posHora[r] = 100;
posMin[r] = 100;
}
ultimaHoraMostrada = -1;
ultimoMinutoMostrado = -1;
minutoDeUltimaCoreografia = -1;
}
void setup() {
Serial.begin(115200);
delay(1500);
pinMode(DATA_PIN, OUTPUT);
pinMode(CLOCK_PIN, OUTPUT);
pinMode(LATCH_PIN, OUTPUT);
pinMode(PIN_BOTON_HORA, INPUT_PULLUP);
pinMode(PIN_BOTON_MIN, INPUT_PULLUP);
pinMode(PIN_BOTON_HOME, INPUT_PULLUP);
Wire.begin(21, 22);
rtc.begin();
DateTime pcTime = DateTime(F(__DATE__), F(__TIME__));
rtc.adjust(pcTime + TimeSpan(0, 2, 0, 0));
refreshOutputs();
Serial.println("\n====================================");
Serial.println("--- RELOJ CLOCKCLOCK 24 ACTIVADO ---");
Serial.println("====================================");
DateTime ahora = rtc.now();
actualizarRelojCompleto(ahora.hour(), ahora.minute());
}
// Variables globales de control para la máquina de estados de ajuste manual
unsigned long timestampUltimoPulso = 0;
bool modoAjusteActivo = false;
void loop() {
DateTime ahora = rtc.now();
// --- DETECCION DE PULSACIONES DE BOTONES CON INTERRUPCIÓN DE FLUJO ---
bool leerBotonHora = (digitalRead(PIN_BOTON_HORA) == LOW);
bool leerBotonMin = (digitalRead(PIN_BOTON_MIN) == LOW);
if (leerBotonHora && estadoAnteriorHora == HIGH) {
delay(50); // Antirebote
timestampUltimoPulso = millis();
if (!modoAjusteActivo) {
// PRIMER PULSO: Forzamos la hora a 0, minutos a 00 y segundos a 00 en el RTC de forma estricta
horaAjusteManual = 0;
rtc.adjust(DateTime(ahora.year(), ahora.month(), ahora.day(), 0, 0, 0));
modoAjusteActivo = true;
Serial.println("\n[MODO AJUSTE] ¡Primer pulso de Hora! Forzando RTC y manecillas a las 00:00");
} else {
// SIGUIENTES PULSOS: Avanza de 1 en 1 partiendo desde 0, manteniendo minutos y segundos en 00
horaAjusteManual = (horaAjusteManual + 1) % 24;
rtc.adjust(DateTime(ahora.year(), ahora.month(), ahora.day(), horaAjusteManual, 0, 0));
Serial.print("\n[MODO AJUSTE] Incrementando Hora a: "); Serial.println(horaAjusteManual);
}
// Transición ultra rápida limpia hacia la posición asignada
DateTime cambio = rtc.now();
minutoDeUltimaCoreografia = -1;
unsigned long originalTransition = TIEMPO_TRANSICION_MS;
const_cast<unsigned long&>(TIEMPO_TRANSICION_MS) = 150; // VELOCIDAD MÁXIMA EXTREMA (150ms)
actualizarRelojCompleto(cambio.hour(), cambio.minute());
const_cast<unsigned long&>(TIEMPO_TRANSICION_MS) = originalTransition;
}
estadoAnteriorHora = leerBotonHora;
if (leerBotonMin && estadoAnteriorMin == HIGH) {
delay(50); // Antirebote
timestampUltimoPulso = millis();
if (!modoAjusteActivo) {
// PRIMER PULSO: Forzamos segundos a 00 y minutos a 00
rtc.adjust(DateTime(ahora.year(), ahora.month(), ahora.day(), ahora.hour(), 0, 0));
modoAjusteActivo = true;
Serial.println("\n[MODO AJUSTE] ¡Primer pulso de minutos! Forzando meta rápida a 00:00");
} else {
// SIGUIENTES PULSOS: Incrementa minuto, segundos en 00
int nuevoMinuto = (ahora.minute() + 1) % 60;
rtc.adjust(DateTime(ahora.year(), ahora.month(), ahora.day(), ahora.hour(), nuevoMinuto, 0));
Serial.println("\n[MODO AJUSTE] Incrementando +1 Minuto");
}
// Transición ultra rápida forzando refresco inmediato en la pantalla
DateTime cambio = rtc.now();
minutoDeUltimaCoreografia = -1;
unsigned long originalTransition = TIEMPO_TRANSICION_MS;
const_cast<unsigned long&>(TIEMPO_TRANSICION_MS) = 150; // VELOCIDAD MÁXIMA EXTREMA (150ms)
actualizarRelojCompleto(cambio.hour(), cambio.minute());
const_cast<unsigned long&>(TIEMPO_TRANSICION_MS) = originalTransition; // Restauramos tiempo normal
}
estadoAnteriorMin = leerBotonMin;
// --- MANEJO DE LA GUARDIA DE 3 SEGUNDOS DE INACTIVIDAD ---
if (modoAjusteActivo && (millis() - timestampUltimoPulso >= 3000)) {
modoAjusteActivo = false;
minutoDeUltimaCoreografia = ahora.minute(); // Bloqueamos el minuto actual para evitar coreografías rotas inmediatamente
Serial.println("\n[SISTEMA] Fin de inactividad de 3s. Saliendo de modo ajuste, el programa se inicia...");
}
// --- FLUJO NORMAL DE IMPRESIÓN Y CONTROL DEL RELOJ ---
if (!modoAjusteActivo) {
if (millis() - ultimoRefrescoSerie >= 1000) {
ultimoRefrescoSerie = millis();
// BLINDAJE: Solo actualizamos la matriz en los segundos estables (antes del segundo :07)
if (ahora.second() < 7 || ahora.minute() == minutoDeUltimaCoreografia) {
actualizarRelojCompleto(ahora.hour(), ahora.minute());
}
Serial.print("Tiempo Real RTC Sincronizado -> ");
if(ahora.hour() < 10) Serial.print("0"); Serial.print(ahora.hour()); Serial.print(":");
if(ahora.minute() < 10) Serial.print("0"); Serial.print(ahora.minute()); Serial.print(":");
if(ahora.second() < 10) Serial.print("0"); Serial.println(ahora.second());
}
// --- DISPARO DE COREOGRAFÍA CORREGIDO AL SEGUNDO 07 EXACTO ---
if (ahora.second() == 7 && ahora.minute() != minutoDeUltimaCoreografia) {
minutoDeUltimaCoreografia = ahora.minute();
int siguienteMinuto = (ahora.minute() + 1) % 60;
int siguienteHora = ahora.hour();
if (siguienteMinuto == 0) siguienteHora = (ahora.hour() + 1) % 24;
ejecutarNuevaCoreografia(siguienteHora, siguienteMinuto);
}
}
// --- BOTÓN MANUAL DE HOME ---
bool estadoActualHome = digitalRead(PIN_BOTON_HOME);
if (estadoActualHome == LOW && estadoAnteriorHome == HIGH) {
delay(50);
ejecutarHomingManual();
}
estadoAnteriorHome = estadoActualHome;
}