#include "TimeKeeper.hpp" #include "../OutputManager/OutputManager.hpp" #include "../ConfigManager/ConfigManager.hpp" #include "../Networking/Networking.hpp" #include "../Player/Player.hpp" // 🔥 Include for Player class definition #include "SD.h" #include void Timekeeper::begin() { LOG_INFO("Timekeeper initialized - clock outputs managed by ConfigManager"); // Initialize RTC if (!rtc.begin()) { LOG_ERROR("Couldn't find RTC"); // Continue anyway, but log the error } else { LOG_INFO("RTC initialized successfully"); // Check if RTC lost power if (!rtc.isrunning()) { LOG_WARNING("RTC is NOT running! Setting time..."); // Set to compile time as fallback rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); } } // Load today's events loadTodaysEvents(); // Create SINGLE consolidated task (saves 8KB RAM!) xTaskCreatePinnedToCore(mainTimekeeperTask, "TimeKeeper", 4096, this, 2, &mainTaskHandle, 1); LOG_INFO("TimeKeeper initialized with SIMPLE sync approach (like your Arduino code)"); } void Timekeeper::setOutputManager(OutputManager* outputManager) { _outputManager = outputManager; LOG_INFO("Timekeeper connected to OutputManager - CLEAN ARCHITECTURE!"); } void Timekeeper::setConfigManager(ConfigManager* configManager) { _configManager = configManager; LOG_INFO("Timekeeper connected to ConfigManager"); } void Timekeeper::setNetworking(Networking* networking) { _networking = networking; LOG_INFO("Timekeeper connected to Networking"); } void Timekeeper::setPlayer(Player* player) { _player = player; LOG_INFO("Timekeeper connected to Player for playback coordination"); } void Timekeeper::interruptActiveAlert() { if (alertInProgress.load()) { LOG_INFO("⚡ ALERT INTERRUPTED by user playback - marking as complete"); alertInProgress.store(false); // Alert will stop naturally on next check in fireAlertBell loop } } void Timekeeper::setRelayWriteFunction(void (*func)(int, int)) { relayWriteFunc = func; LOG_WARNING("Using LEGACY relay function - consider upgrading to OutputManager"); } void Timekeeper::setClockOutputs(int relay1, int relay2) { LOG_WARNING("⚠️ setClockOutputs() is DEPRECATED! Use ConfigManager.setClockOutput1/2() instead"); LOG_WARNING("⚠️ Clock outputs should be configured via MQTT/WebSocket commands"); // For backward compatibility, still set the config if ConfigManager is available if (_configManager) { _configManager->setClockOutput1(relay1); _configManager->setClockOutput2(relay2); LOG_INFO("Clock outputs updated via legacy method: C1=%d, C2=%d", relay1, relay2); } else { LOG_ERROR("ConfigManager not available - cannot set clock outputs"); } } void Timekeeper::setTime(unsigned long timestamp) { if (!rtc.begin()) { LOG_ERROR("RTC not available - cannot set time"); return; } // Get timezone configuration auto& timeConfig = _configManager->getTimeConfig(); // Apply timezone offset to UTC timestamp long totalOffset = timeConfig.gmtOffsetSec + timeConfig.daylightOffsetSec; unsigned long localTimestamp = timestamp + totalOffset; // Convert local timestamp to DateTime object DateTime newTime(localTimestamp); // Set the RTC with local time rtc.adjust(newTime); LOG_INFO("RTC time set to LOCAL: %04d-%02d-%02d %02d:%02d:%02d (UTC timestamp: %lu + %ld offset = %lu)", newTime.year(), newTime.month(), newTime.day(), newTime.hour(), newTime.minute(), newTime.second(), timestamp, totalOffset, localTimestamp); // Reload today's events since the date might have changed loadTodaysEvents(); } void Timekeeper::setTimeWithLocalTimestamp(unsigned long localTimestamp) { if (!rtc.begin()) { LOG_ERROR("RTC not available - cannot set time"); return; } // Convert local timestamp directly to DateTime object (no timezone conversion needed) DateTime newTime(localTimestamp); // Set the RTC with local time rtc.adjust(newTime); LOG_INFO("RTC time set to LOCAL: %04d-%02d-%02d %02d:%02d:%02d (local timestamp: %lu)", newTime.year(), newTime.month(), newTime.day(), newTime.hour(), newTime.minute(), newTime.second(), localTimestamp); // Reload today's events since the date might have changed loadTodaysEvents(); } unsigned long Timekeeper::getTime() { if (!rtc.isrunning()) { LOG_ERROR("RTC not running - cannot get time"); return 0; } DateTime now = rtc.now(); unsigned long timestamp = now.unixtime(); LOG_DEBUG("Current RTC time: %04d-%02d-%02d %02d:%02d:%02d (timestamp: %lu)", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second(), timestamp); return timestamp; } void Timekeeper::syncTimeWithNTP() { // Check if we have network connection and required dependencies if (!_networking || !_configManager) { LOG_WARNING("Cannot sync time: Networking or ConfigManager not set - using RTC time"); return; } if (!_networking->isConnected()) { LOG_INFO("No network connection - skipping NTP sync, using RTC time"); return; } LOG_INFO("⏰ Starting non-blocking NTP sync..."); // Get config from ConfigManager auto& timeConfig = _configManager->getTimeConfig(); // Configure NTP with settings from config configTime(timeConfig.gmtOffsetSec, timeConfig.daylightOffsetSec, timeConfig.ntpServer.c_str()); // 🔥 NON-BLOCKING: Try to get time with reasonable timeout for network response struct tm timeInfo; if (getLocalTime(&timeInfo, 5000)) { // 5 second timeout for NTP response // Success! Update RTC with synchronized time rtc.adjust(DateTime(timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday, timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec)); LOG_INFO("✅ NTP sync successful: %04d-%02d-%02d %02d:%02d:%02d", timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday, timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec); // Reload today's events since the time might have changed significantly loadTodaysEvents(); } else { // No internet or NTP server unreachable - this is NORMAL for local networks LOG_INFO("⚠️ NTP sync skipped (no internet) - using RTC time. This is normal for local networks."); } } // ════════════════════════════════════════════════════════════════════════════ // CONSOLIDATED TimeKeeper Task - SIMPLE approach like your Arduino code // ════════════════════════════════════════════════════════════════════════════ void Timekeeper::mainTimekeeperTask(void* parameter) { Timekeeper* keeper = static_cast(parameter); LOG_INFO("🕒 SIMPLE TimeKeeper task started - based on your Arduino code approach"); unsigned long lastRtcCheck = 0; unsigned long lastScheduleCheck = 0; while (true) { unsigned long now = millis(); // 🕐 SIMPLE PHYSICAL CLOCK SYNC (check every loop, fire if needed - your Arduino approach) keeper->checkAndSyncPhysicalClock(); // 🔔 ALERT MANAGEMENT (every second for precise timing) keeper->checkClockAlerts(); // 💡 BACKLIGHT AUTOMATION (every 10 seconds) if (now - lastRtcCheck >= 10000) { keeper->checkBacklightAutomation(); } // 📅 SCHEDULE CHECK (every second) if (now - lastScheduleCheck >= 1000) { keeper->checkScheduledEvents(); lastScheduleCheck = now; } // 🔧 RTC MAINTENANCE (every 10 seconds) if (now - lastRtcCheck >= 10000) { // RTC health check DateTime rtcNow = keeper->rtc.now(); if (keeper->rtc.isrunning()) { // Check for midnight - reload events for new day if (rtcNow.hour() == 0 && rtcNow.minute() == 0 && rtcNow.second() < 10) { LOG_INFO("🌙 Midnight detected - reloading events"); keeper->loadTodaysEvents(); keeper->loadNextDayEvents(); } // Hourly maintenance if (rtcNow.minute() == 0 && rtcNow.second() < 10) { LOG_DEBUG("🕐 Hourly check at %02d:00", rtcNow.hour()); } } else { static uint8_t rtcWarningCounter = 0; if (rtcWarningCounter++ % 6 == 0) { // Log every minute LOG_WARNING("⚠️ RTC not running!"); } } lastRtcCheck = now; } vTaskDelay(pdMS_TO_TICKS(1000)); } } // ════════════════════════════════════════════════════════════════════════════ // SIMPLE CLOCK SYNC IMPLEMENTATION - Based on your Arduino code // ════════════════════════════════════════════════════════════════════════════ void Timekeeper::checkAndSyncPhysicalClock() { // Check if clock is enabled in config if (!_configManager || !_configManager->getClockEnabled()) { return; // Clock is disabled - skip all clock functionality } // Check if there is any Time Difference between the Physical Clock and the Actual Time and if yes, run the Motor if (!_outputManager || !rtc.isrunning() || clockUpdatesPaused) { return; } // Get current real time (your updateCurrentTime() equivalent) DateTime now = rtc.now(); int8_t realHour = now.hour() % 12; // Convert to 12-hour format for clock face if (realHour == 0) realHour = 12; // 12 AM/PM shows as 12, not 0 int8_t realMinute = now.minute(); // Get physical clock state (your clock.hour/clock.minute equivalent) int8_t physicalHour = _configManager->getPhysicalClockHour(); int8_t physicalMinute = _configManager->getPhysicalClockMinute(); // Calculate time difference (your exact logic!) int16_t timeDifference = (realHour * 60 + realMinute) - (physicalHour * 60 + physicalMinute); LOG_VERBOSE("⏰ CHECK: Real time %02d:%02d vs Physical %02d:%02d - DIFF: %d mins", realHour, realMinute, physicalHour, physicalMinute, timeDifference); // Handle 12-hour rollover (if negative, add 12 hours) if (timeDifference < 0) { timeDifference += 12 * 60; // Add 12 hours to handle rollover LOG_VERBOSE("⏰ DIFF: Adjusted for rollover, new difference %d minutes", timeDifference); } // If there's a difference, advance the clock by one minute (your runMotor equivalent) if (timeDifference >= 1) { advancePhysicalClockOneMinute(); LOG_DEBUG("⏰ SYNC: Advanced physical clock by 1 minute to %02d:%02d (real: %02d:%02d, diff: %lu mins)", _configManager->getPhysicalClockHour(), _configManager->getPhysicalClockMinute(), realHour, realMinute, timeDifference); } } void Timekeeper::advancePhysicalClockOneMinute() { const auto& clockConfig = _configManager->getClockConfig(); if (clockConfig.c1output == 255 || clockConfig.c2output == 255) { return; } bool useC1 = _configManager->getNextOutputIsC1(); uint8_t outputToFire = useC1 ? (clockConfig.c1output - 1) : (clockConfig.c2output - 1); LOG_DEBUG("🔥 ADVANCE: Firing %s (output %d) for %dms", useC1 ? "C1" : "C2", outputToFire + 1, clockConfig.pulseDuration); _outputManager->fireOutputForDuration(outputToFire, clockConfig.pulseDuration); vTaskDelay(pdMS_TO_TICKS(clockConfig.pulseDuration + clockConfig.pauseDuration)); // cool-off motor _configManager->setNextOutputIsC1(!useC1); updatePhysicalClockTime(); } void Timekeeper::updatePhysicalClockTime() { uint8_t currentHour = _configManager->getPhysicalClockHour(); uint8_t currentMinute = _configManager->getPhysicalClockMinute(); currentMinute++; if (currentMinute >= 60) { currentMinute = 0; currentHour++; if (currentHour > 12) { // 12-hour clock (your code used 24, but clock face is 12) currentHour = 1; } } _configManager->setPhysicalClockHour(currentHour); _configManager->setPhysicalClockMinute(currentMinute); _configManager->setLastSyncTime(millis() / 1000); _configManager->saveClockState(); LOG_DEBUG("📅 STATE: Physical clock advanced to %d:%02d", currentHour, currentMinute); } // ════════════════════════════════════════════════════════════════════════════ // EVENT MANAGEMENT // ════════════════════════════════════════════════════════════════════════════ void Timekeeper::loadTodaysEvents() { // Clear existing events todaysEvents.clear(); // Get current date/time from RTC DateTime now = rtc.now(); if (!rtc.isrunning()) { LOG_ERROR("RTC not running - cannot load events"); return; } int currentYear = now.year(); int currentMonth = now.month(); int currentDay = now.day(); int currentDayOfWeek = now.dayOfTheWeek(); // 0=Sunday, 1=Monday, etc. LOG_INFO("Loading events for: %04d-%02d-%02d (day %d)", currentYear, currentMonth, currentDay, currentDayOfWeek); // Open and parse events file File file = SD.open("/events/events.json"); if (!file) { LOG_ERROR("Failed to open events.json"); return; } // Use static allocation instead of dynamic to avoid heap fragmentation static StaticJsonDocument<8192> doc; doc.clear(); DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { LOG_ERROR("JSON parsing failed: %s", error.c_str()); return; } JsonArray events = doc["events"]; int eventsLoaded = 0; for (JsonObject event : events) { if (!event["enabled"].as()) { continue; // Skip disabled events } String type = event["type"].as(); bool shouldAdd = false; if (type == "single") { // Check if event date matches today String eventDateTime = event["datetime"].as(); if (isSameDate(eventDateTime, currentYear, currentMonth, currentDay)) { shouldAdd = true; } } else if (type == "weekly") { // Check if today's day of week is in the event's days JsonArray daysOfWeek = event["days_of_week"]; for (JsonVariant dayVar : daysOfWeek) { int day = dayVar.as(); if (day == currentDayOfWeek) { shouldAdd = true; break; } } } else if (type == "monthly") { // Check if today's date is in the event's days of month JsonArray daysOfMonth = event["days_of_month"]; for (JsonVariant dayVar : daysOfMonth) { int day = dayVar.as(); if (day == currentDay) { shouldAdd = true; break; } } } if (shouldAdd) { addToTodaysSchedule(event); eventsLoaded++; } } // Sort events by time sortEventsByTime(); LOG_INFO("Loaded %d events for today", eventsLoaded); } bool Timekeeper::isSameDate(String eventDateTime, int year, int month, int day) { // Parse "2025-12-25T09:00:00" format if (eventDateTime.length() < 10) return false; int eventYear = eventDateTime.substring(0, 4).toInt(); int eventMonth = eventDateTime.substring(5, 7).toInt(); int eventDay = eventDateTime.substring(8, 10).toInt(); return (eventYear == year && eventMonth == month && eventDay == day); } void Timekeeper::addToTodaysSchedule(JsonObject event) { ScheduledEvent schedEvent; // Extract time based on event type if (event["type"] == "single") { // Extract time from datetime: "2025-12-25T09:00:00" -> "09:00:00" String datetime = event["datetime"].as(); if (datetime.length() >= 19) { schedEvent.timeStr = datetime.substring(11, 19); } } else { // Weekly/Monthly events have separate time field schedEvent.timeStr = event["time"].as(); } // Store the entire event data (create a copy) schedEvent.eventData = event; todaysEvents.push_back(schedEvent); LOG_DEBUG("Added event '%s' at %s", event["name"].as().c_str(), schedEvent.timeStr.c_str()); } void Timekeeper::sortEventsByTime() { std::sort(todaysEvents.begin(), todaysEvents.end(), [](const ScheduledEvent& a, const ScheduledEvent& b) { return a.timeStr < b.timeStr; }); } String Timekeeper::getCurrentTimeString() { DateTime now = rtc.now(); if (!rtc.isrunning()) { return "00:00:00"; } char timeStr[9]; sprintf(timeStr, "%02d:%02d:%02d", now.hour(), now.minute(), now.second()); return String(timeStr); } void Timekeeper::checkScheduledEvents() { String currentTime = getCurrentTimeString(); // Only check the seconds part for exact matching String currentTimeMinute = currentTime.substring(0, 5); // "HH:MM" for (auto& event : todaysEvents) { String eventTimeMinute = event.timeStr.substring(0, 5); // "HH:MM" if (eventTimeMinute == currentTimeMinute) { // Check if we haven't already triggered this event if (!event.triggered) { triggerEvent(event); event.triggered = true; } } // Reset trigger flag when we're past the minute if (eventTimeMinute < currentTimeMinute) { event.triggered = false; } } } void Timekeeper::triggerEvent(ScheduledEvent& event) { JsonObject eventData = event.eventData; LOG_INFO("TRIGGERING EVENT: %s at %s", eventData["name"].as().c_str(), event.timeStr.c_str()); // Here you would trigger your melody playback // You might want to call a function from your main program // or send a message to another task // Example of what you might do: JsonObject melody = eventData["melody"]; String melodyUID = melody["uid"].as(); String melodyName = melody["name"].as(); LOG_INFO("Playing melody: %s (UID: %s)", melodyName.c_str(), melodyUID.c_str()); // TODO: Add your melody trigger code here // playMelody(melody); } void Timekeeper::loadNextDayEvents() { // This function would load tomorrow's events for smooth midnight transition // Implementation similar to loadTodaysEvents() but for tomorrow's date LOG_DEBUG("Pre-loading tomorrow's events..."); // TODO: Implement if needed for smoother transitions } // ════════════════════════════════════════════════════════════════════════════ // CLOCK ALERTS IMPLEMENTATION // ════════════════════════════════════════════════════════════════════════════ void Timekeeper::checkClockAlerts() { // Check if clock is enabled in config if (!_configManager || !_configManager->getClockEnabled()) { return; // Clock is disabled - skip all alert functionality } // Check if we have required dependencies if (!_outputManager || !rtc.isrunning()) { return; } // 🔥 CRITICAL: Check if Player is busy - if so, SKIP alert completely if (_player && _player->isPlaying) { // Player is active (playing, paused, stopping, etc.) - skip alert entirely // Mark this alert as processed to prevent it from firing when playback ends DateTime now = rtc.now(); int currentMinute = now.minute(); if (currentMinute == 0) { lastHour = now.hour(); // Mark hour as processed } else if (currentMinute == 30) { lastMinute = 30; // Mark half-hour as processed } else if (currentMinute == 15 || currentMinute == 45) { lastMinute = currentMinute; // Mark quarter-hour as processed } LOG_DEBUG("⏭️ SKIPPING clock alert - Player is busy (playing/paused)"); return; } // Get current time DateTime now = rtc.now(); int currentHour = now.hour(); int currentMinute = now.minute(); int currentSecond = now.second(); // Only trigger alerts in first 30 seconds of the minute // The lastHour/lastMinute tracking prevents duplicate triggers if (currentSecond > 30) { return; } // Get clock configuration const auto& clockConfig = _configManager->getClockConfig(); // Check if alerts are disabled if (clockConfig.alertType == "OFF") { return; } // Check if we're in a silence period if (isInSilencePeriod()) { return; } // 🕐 HOURLY ALERTS (at xx:00) if (currentMinute == 0 && currentHour != lastHour) { triggerHourlyAlert(currentHour); lastHour = currentHour; } // 🕕 HALF-HOUR ALERTS (at xx:30) if (currentMinute == 30 && lastMinute != 30) { if (clockConfig.halfBell != 255) { // 255 = disabled LOG_INFO("🕕 Half-hour alert at %02d:30", currentHour); fireAlertBell(clockConfig.halfBell, 1); } lastMinute = 30; } // 🕒 QUARTER-HOUR ALERTS (at xx:15 and xx:45) if ((currentMinute == 15 || currentMinute == 45) && lastMinute != currentMinute) { if (clockConfig.quarterBell != 255) { // 255 = disabled LOG_INFO("🕒 Quarter-hour alert at %02d:%02d", currentHour, currentMinute); fireAlertBell(clockConfig.quarterBell, 1); } lastMinute = currentMinute; } // Reset minute tracking for other minutes if (currentMinute != 0 && currentMinute != 15 && currentMinute != 30 && currentMinute != 45) { if (lastMinute != currentMinute) { lastMinute = currentMinute; } } } void Timekeeper::triggerHourlyAlert(int hour) { const auto& clockConfig = _configManager->getClockConfig(); // Check if hourly bell is configured if (clockConfig.hourBell == 255) { // 255 = disabled return; } if (clockConfig.alertType == "SINGLE") { // Single ding for any hour LOG_INFO("🕐 Hourly alert (SINGLE) at %02d:00", hour); fireAlertBell(clockConfig.hourBell, 1); } else if (clockConfig.alertType == "HOURS") { // Ring the number of times equal to the hour (1-12) int bellCount = hour; if (bellCount == 0) bellCount = 12; // Midnight = 12 bells if (bellCount > 12) bellCount = bellCount - 12; // 24h to 12h conversion LOG_INFO("🕐 Hourly alert (HOURS) at %02d:00 - %d rings", hour, bellCount); fireAlertBell(clockConfig.hourBell, bellCount); } } void Timekeeper::fireAlertBell(uint8_t bellNumber, int count) { if (!_outputManager || bellNumber == 255) { return; } const auto& clockConfig = _configManager->getClockConfig(); // Mark alert as in progress alertInProgress.store(true); for (int i = 0; i < count; i++) { // 🔥 Check for interruption by user playback if (!alertInProgress.load()) { LOG_INFO("⚡ Alert interrupted at ring %d/%d - stopping immediately", i + 1, count); return; } // Get bell duration from bell configuration uint16_t bellDuration = _configManager->getBellDuration(bellNumber); LOG_DEBUG("🔔 Alert bell #%d ring %d/%d (duration: %dms)", bellNumber + 1, i + 1, count, bellDuration); // Fire the bell using OutputManager _outputManager->fireOutputForDuration(bellNumber, bellDuration); // Wait between rings (only if there's more than one ring) if (i < count - 1) { vTaskDelay(pdMS_TO_TICKS(clockConfig.alertRingInterval)); } } // Mark alert as complete alertInProgress.store(false); } void Timekeeper::checkBacklightAutomation() { // Check if clock is enabled in config if (!_configManager || !_configManager->getClockEnabled()) { return; // Clock is disabled - skip all backlight functionality } // Check if we have required dependencies if (!_outputManager || !rtc.isrunning()) { return; } const auto& clockConfig = _configManager->getClockConfig(); // Check if backlight automation is enabled if (!clockConfig.backlight || clockConfig.backlightOutput == 255) { return; // Backlight automation disabled } // Get current time DateTime now = rtc.now(); char currentTimeStr[6]; sprintf(currentTimeStr, "%02d:%02d", now.hour(), now.minute()); String currentTime = String(currentTimeStr); // Check if it's time to turn backlight ON if (currentTime == clockConfig.backlightOnTime && !backlightState) { LOG_INFO("💡 Turning backlight ON at %s (output #%d)", currentTime.c_str(), clockConfig.backlightOutput + 1); _outputManager->fireOutput(clockConfig.backlightOutput); backlightState = true; } // Check if it's time to turn backlight OFF else if (currentTime == clockConfig.backlightOffTime && backlightState) { LOG_INFO("💡 Turning backlight OFF at %s (output #%d)", currentTime.c_str(), clockConfig.backlightOutput + 1); _outputManager->extinguishOutput(clockConfig.backlightOutput); backlightState = false; } } bool Timekeeper::isInSilencePeriod() { if (!_configManager || !rtc.isrunning()) { return false; } const auto& clockConfig = _configManager->getClockConfig(); // Get current time DateTime now = rtc.now(); char currentTimeStr[6]; sprintf(currentTimeStr, "%02d:%02d", now.hour(), now.minute()); String currentTime = String(currentTimeStr); // Check daytime silence period if (clockConfig.daytimeSilenceEnabled) { bool inDaytime = isTimeInRange(currentTime, clockConfig.daytimeSilenceOnTime, clockConfig.daytimeSilenceOffTime); LOG_DEBUG("🔇 Daytime silence check: current=%s, range=%s-%s, inRange=%s", currentTime.c_str(), clockConfig.daytimeSilenceOnTime.c_str(), clockConfig.daytimeSilenceOffTime.c_str(), inDaytime ? "YES" : "NO"); if (inDaytime) { return true; } } // Check nighttime silence period if (clockConfig.nighttimeSilenceEnabled) { bool inNighttime = isTimeInRange(currentTime, clockConfig.nighttimeSilenceOnTime, clockConfig.nighttimeSilenceOffTime); LOG_DEBUG("🌙 Nighttime silence check: current=%s, range=%s-%s, inRange=%s", currentTime.c_str(), clockConfig.nighttimeSilenceOnTime.c_str(), clockConfig.nighttimeSilenceOffTime.c_str(), inNighttime ? "YES" : "NO"); if (inNighttime) { return true; } } return false; } bool Timekeeper::isTimeInRange(const String& currentTime, const String& startTime, const String& endTime) const { // Handle the case where the time range crosses midnight (e.g., 22:00 to 07:00) if (startTime > endTime) { // Range crosses midnight - current time is in range if it's after start OR before end return (currentTime >= startTime || currentTime <= endTime); } else { // Normal range - current time is in range if it's between start and end return (currentTime >= startTime && currentTime <= endTime); } } // ════════════════════════════════════════════════════════════════════════════ // HEALTH CHECK IMPLEMENTATION // ════════════════════════════════════════════════════════════════════════════ bool Timekeeper::isHealthy() { // Check if RTC is running if (!rtc.isrunning()) { LOG_DEBUG("TimeKeeper: Unhealthy - RTC not running"); return false; } // Check if main task is created and running if (mainTaskHandle == NULL) { LOG_DEBUG("TimeKeeper: Unhealthy - Main task not created"); return false; } // Check if task is still alive eTaskState taskState = eTaskGetState(mainTaskHandle); if (taskState == eDeleted || taskState == eInvalid) { LOG_DEBUG("TimeKeeper: Unhealthy - Main task deleted or invalid"); return false; } // Check if required dependencies are set if (!_configManager) { LOG_DEBUG("TimeKeeper: Unhealthy - ConfigManager not set"); return false; } if (!_outputManager) { LOG_DEBUG("TimeKeeper: Unhealthy - OutputManager not set"); return false; } // Check if time is reasonable (not stuck at epoch or way in the future) DateTime now = rtc.now(); if (now.year() < 2020 || now.year() > 2100) { LOG_DEBUG("TimeKeeper: Unhealthy - RTC time unreasonable: %d", now.year()); return false; } return true; }