#include "timekeeper.hpp" #include "SD.h" void Timekeeper::begin() { // Clock outputs start as unassigned (-1) clockRelay1 = -1; clockRelay2 = -1; clockEnabled = false; Serial.println("Timekeeper initialized - clock outputs not assigned"); // Initialize RTC if (!rtc.begin()) { Serial.println("Couldn't find RTC"); // Continue anyway, but log the error } else { Serial.println("RTC initialized successfully"); // Check if RTC lost power if (!rtc.isrunning()) { Serial.println("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 tasks with appropriate priorities // Priority 3 = Highest (clock), 2 = Medium (scheduler), 1 = Lowest (rtc) xTaskCreate(clockTask, "ClockTask", 2048, this, 3, &clockTaskHandle); xTaskCreate(schedulerTask, "SchedulerTask", 4096, this, 2, &schedulerTaskHandle); xTaskCreate(rtcTask, "RTCTask", 4096, this, 1, &rtcTaskHandle); Serial.println("Timekeeper initialized with tasks"); } void Timekeeper::setRelayWriteFunction(void (*func)(int, int)) { relayWriteFunc = func; } void Timekeeper::setClockOutputs(int relay1, int relay2) { // Validate relay numbers (assuming 0-5 are valid) if (relay1 < 0 || relay1 > 5 || relay2 < 0 || relay2 > 5) { Serial.println("Invalid relay numbers! Must be 0-5"); return; } if (relay1 == relay2) { Serial.println("Clock relays cannot be the same!"); return; } clockRelay1 = relay1; clockRelay2 = relay2; clockEnabled = true; // Enable clock functionality Serial.printf("Clock outputs set to: Relay %d and Relay %d - Clock ENABLED\n", relay1, relay2); } void Timekeeper::setTime(unsigned long timestamp) { if (!rtc.begin()) { Serial.println("RTC not available - cannot set time"); return; } // Convert Unix timestamp to DateTime object DateTime newTime(timestamp); // Set the RTC rtc.adjust(newTime); Serial.printf("RTC time set to: %04d-%02d-%02d %02d:%02d:%02d (timestamp: %lu)\n", newTime.year(), newTime.month(), newTime.day(), newTime.hour(), newTime.minute(), newTime.second(), timestamp); // Reload today's events since the date might have changed loadTodaysEvents(); } unsigned long Timekeeper::getTime() { if (!rtc.isrunning()) { Serial.println("RTC not running - cannot get time"); return 0; } DateTime now = rtc.now(); unsigned long timestamp = now.unixtime(); Serial.printf("Current RTC time: %04d-%02d-%02d %02d:%02d:%02d (timestamp: %lu)\n", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second(), timestamp); return timestamp; } void Timekeeper::loadTodaysEvents() { // Clear existing events todaysEvents.clear(); // Get current date/time from RTC DateTime now = rtc.now(); if (!rtc.isrunning()) { Serial.println("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. Serial.printf("Loading events for: %04d-%02d-%02d (day %d)\n", currentYear, currentMonth, currentDay, currentDayOfWeek); // Open and parse events file File file = SD.open("/events/events.json"); if (!file) { Serial.println("Failed to open events.json"); return; } DynamicJsonDocument doc(8192); DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { Serial.print("JSON parsing failed: "); Serial.println(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(); Serial.printf("Loaded %d events for today\n", 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); Serial.printf("Added event '%s' at %s\n", 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); } // Static task functions void Timekeeper::clockTask(void* parameter) { Timekeeper* keeper = static_cast(parameter); Serial.println("Clock task started"); while(1) { unsigned long now = millis(); // Check if a minute has passed (60000ms = 60 seconds) if (now - keeper->lastClockPulse >= 60000) { keeper->updateClock(); keeper->lastClockPulse = now; } // Check every 100ms for precision vTaskDelay(100 / portTICK_PERIOD_MS); } } void Timekeeper::schedulerTask(void* parameter) { Timekeeper* keeper = static_cast(parameter); Serial.println("Scheduler task started"); while(1) { keeper->checkScheduledEvents(); // Check every second vTaskDelay(1000 / portTICK_PERIOD_MS); } } void Timekeeper::rtcTask(void* parameter) { Timekeeper* keeper = static_cast(parameter); Serial.println("RTC task started"); while(1) { DateTime now = keeper->rtc.now(); if (keeper->rtc.isrunning()) { // Check if it's midnight - reload events for new day if (now.hour() == 0 && now.minute() == 0 && now.second() < 10) { Serial.println("Midnight detected - reloading events"); keeper->loadTodaysEvents(); keeper->loadNextDayEvents(); } // Check hourly for maintenance tasks if (now.minute() == 0 && now.second() < 10) { Serial.printf("Hourly check at %02d:00\n", now.hour()); // Add any hourly maintenance here: // - Log status // - Check for schedule updates } } else { Serial.println("Warning: RTC not running!"); } // Check every 10 seconds vTaskDelay(10000 / portTICK_PERIOD_MS); } } void Timekeeper::updateClock() { // Check if clock is enabled and outputs are assigned if (!clockEnabled || clockRelay1 == -1 || clockRelay2 == -1) { return; // Silently skip if clock not configured } DateTime now = rtc.now(); if (!rtc.isrunning()) { Serial.println("RTC not running - cannot update clock"); return; } if (relayWriteFunc == nullptr) { Serial.println("Relay write function not set - cannot update clock"); return; } // Alternate between the two relays int activeRelay = currentClockRelay ? clockRelay2 : clockRelay1; Serial.printf("Clock pulse: Relay %d at %02d:%02d:%02d\n", activeRelay, now.hour(), now.minute(), now.second()); // Pulse for 5 seconds relayWriteFunc(activeRelay, LOW); // Assuming LOW activates relay vTaskDelay(5000 / portTICK_PERIOD_MS); relayWriteFunc(activeRelay, HIGH); // HIGH deactivates relay // Switch to other relay for next minute currentClockRelay = !currentClockRelay; } 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; Serial.printf("TRIGGERING EVENT: %s at %s\n", 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(); Serial.printf("Playing melody: %s (UID: %s)\n", 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 Serial.println("Pre-loading tomorrow's events..."); // TODO: Implement if needed for smoother transitions }