Complete Rebuild, with Subsystems for each component. RTOS Tasks. (help by Claude)
This commit is contained in:
772
vesper/src/TimeKeeper/TimeKeeper.cpp
Normal file
772
vesper/src/TimeKeeper/TimeKeeper.cpp
Normal file
@@ -0,0 +1,772 @@
|
||||
#include "TimeKeeper.hpp"
|
||||
#include "../OutputManager/OutputManager.hpp"
|
||||
#include "../ConfigManager/ConfigManager.hpp"
|
||||
#include "../Networking/Networking.hpp"
|
||||
#include "SD.h"
|
||||
#include <time.h>
|
||||
|
||||
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::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_ERROR("Cannot sync time: Networking or ConfigManager not set");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_networking->isConnected()) {
|
||||
LOG_WARNING("Cannot sync time: No network connection");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Syncing time with NTP server...");
|
||||
|
||||
// Get config from ConfigManager
|
||||
auto& timeConfig = _configManager->getTimeConfig();
|
||||
|
||||
// Configure NTP with settings from config
|
||||
configTime(timeConfig.gmtOffsetSec, timeConfig.daylightOffsetSec, timeConfig.ntpServer.c_str());
|
||||
|
||||
// Wait for time sync with timeout
|
||||
struct tm timeInfo;
|
||||
int attempts = 0;
|
||||
while (!getLocalTime(&timeInfo) && attempts < 10) {
|
||||
LOG_DEBUG("Waiting for NTP sync... attempt %d", attempts + 1);
|
||||
delay(1000);
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (attempts >= 10) {
|
||||
LOG_ERROR("Failed to obtain time from NTP server after 10 attempts");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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("Time synced successfully: %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();
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// CONSOLIDATED TimeKeeper Task - SIMPLE approach like your Arduino code
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void Timekeeper::mainTimekeeperTask(void* parameter) {
|
||||
Timekeeper* keeper = static_cast<Timekeeper*>(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);
|
||||
|
||||
// Handle 12-hour rollover (if negative, add 12 hours)
|
||||
if (timeDifference < 0) {
|
||||
timeDifference += 12 * 60; // Add 12 hours to handle rollover
|
||||
}
|
||||
|
||||
// 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 = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_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<bool>()) {
|
||||
continue; // Skip disabled events
|
||||
}
|
||||
|
||||
String type = event["type"].as<String>();
|
||||
bool shouldAdd = false;
|
||||
|
||||
if (type == "single") {
|
||||
// Check if event date matches today
|
||||
String eventDateTime = event["datetime"].as<String>();
|
||||
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<int>();
|
||||
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<int>();
|
||||
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<String>();
|
||||
if (datetime.length() >= 19) {
|
||||
schedEvent.timeStr = datetime.substring(11, 19);
|
||||
}
|
||||
} else {
|
||||
// Weekly/Monthly events have separate time field
|
||||
schedEvent.timeStr = event["time"].as<String>();
|
||||
}
|
||||
|
||||
// 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<String>().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<String>().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>();
|
||||
String melodyName = melody["name"].as<String>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Get current time
|
||||
DateTime now = rtc.now();
|
||||
int currentHour = now.hour();
|
||||
int currentMinute = now.minute();
|
||||
int currentSecond = now.second();
|
||||
|
||||
// Only trigger alerts on exact seconds (0-2) to avoid multiple triggers
|
||||
if (currentSecond > 2) {
|
||||
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();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (isTimeInRange(currentTime, clockConfig.daytimeSilenceOnTime, clockConfig.daytimeSilenceOffTime)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check nighttime silence period
|
||||
if (clockConfig.nighttimeSilenceEnabled) {
|
||||
if (isTimeInRange(currentTime, clockConfig.nighttimeSilenceOnTime, clockConfig.nighttimeSilenceOffTime)) {
|
||||
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;
|
||||
}
|
||||
152
vesper/src/TimeKeeper/TimeKeeper.hpp
Normal file
152
vesper/src/TimeKeeper/TimeKeeper.hpp
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* TIMEKEEPER.HPP - NTP Synchronization and Clock Management
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* ⏰ THE TIME MASTER OF VESPER ⏰
|
||||
*
|
||||
* This class manages all time-related functionality including NTP synchronization,
|
||||
* timezone handling, and hardware clock signal generation. It ensures accurate
|
||||
* timekeeping across the entire system.
|
||||
*
|
||||
* 🏗️ TIME MANAGEMENT:
|
||||
* • NTP synchronization with automatic retry
|
||||
* • Timezone and daylight saving time support
|
||||
* • Hardware clock signal generation
|
||||
* • Network-dependent time sync
|
||||
* • Clean dependency injection architecture
|
||||
*
|
||||
* 📋 VERSION: 2.0 (Modular time management)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#ifndef TIMEKEEPER_HPP
|
||||
#define TIMEKEEPER_HPP
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <ArduinoJson.h>
|
||||
#include <RTClib.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "../Logging/Logging.hpp"
|
||||
|
||||
// Forward declarations
|
||||
class OutputManager;
|
||||
class ConfigManager;
|
||||
class Networking;
|
||||
|
||||
// Structure to hold scheduled events
|
||||
struct ScheduledEvent {
|
||||
String timeStr; // Time in "HH:MM:SS" format
|
||||
JsonObject eventData; // Complete event data from JSON
|
||||
bool triggered = false; // Flag to prevent multiple triggers
|
||||
};
|
||||
|
||||
class Timekeeper {
|
||||
private:
|
||||
// RTC object
|
||||
RTC_DS1307 rtc;
|
||||
|
||||
// Event storage
|
||||
std::vector<ScheduledEvent> todaysEvents;
|
||||
std::vector<ScheduledEvent> tomorrowsEvents;
|
||||
|
||||
// Clocktower Updating Pause
|
||||
bool clockUpdatesPaused = false;
|
||||
|
||||
// Alert management - new functionality
|
||||
int lastHour = -1; // Track last processed hour to avoid duplicate alerts
|
||||
int lastMinute = -1; // Track last processed minute for quarter/half alerts
|
||||
|
||||
// Backlight management - new functionality
|
||||
bool backlightState = false; // Track current backlight state
|
||||
|
||||
// Clean dependencies
|
||||
OutputManager* _outputManager = nullptr;
|
||||
ConfigManager* _configManager = nullptr;
|
||||
Networking* _networking = nullptr;
|
||||
|
||||
// Legacy function pointer (DEPRECATED - will be removed)
|
||||
void (*relayWriteFunc)(int relay, int state) = nullptr;
|
||||
|
||||
// Task handles - CONSOLIDATED!
|
||||
TaskHandle_t mainTaskHandle = NULL; // Single task handles everything
|
||||
|
||||
public:
|
||||
// Main initialization (no clock outputs initially)
|
||||
void begin();
|
||||
|
||||
// Modern clean interface
|
||||
void setOutputManager(OutputManager* outputManager);
|
||||
void setConfigManager(ConfigManager* configManager);
|
||||
void setNetworking(Networking* networking);
|
||||
|
||||
// Clock Updates Pause Functions
|
||||
void pauseClockUpdates() { clockUpdatesPaused = true; }
|
||||
void resumeClockUpdates() { clockUpdatesPaused = false; }
|
||||
bool areClockUpdatesPaused() const { return clockUpdatesPaused; }
|
||||
|
||||
// Legacy interface (DEPRECATED - will be removed)
|
||||
void setRelayWriteFunction(void (*func)(int, int));
|
||||
|
||||
// DEPRECATED: Clock outputs now configured via ConfigManager
|
||||
void setClockOutputs(int relay1, int relay2) __attribute__((deprecated("Use ConfigManager to set clock outputs")));
|
||||
|
||||
// Time management functions
|
||||
void setTime(unsigned long timestamp);
|
||||
|
||||
/**
|
||||
* @brief Set RTC time with local timestamp (timezone already applied)
|
||||
* @param localTimestamp Unix timestamp with timezone offset already applied
|
||||
*/
|
||||
void setTimeWithLocalTimestamp(unsigned long localTimestamp); // Set RTC time from Unix timestamp
|
||||
unsigned long getTime(); // Get current time as Unix timestamp
|
||||
void syncTimeWithNTP(); // Sync RTC time with NTP server
|
||||
|
||||
// Event management
|
||||
void loadTodaysEvents();
|
||||
void loadNextDayEvents();
|
||||
|
||||
// Static task functions (CONSOLIDATED)
|
||||
static void mainTimekeeperTask(void* parameter);
|
||||
|
||||
bool isHealthy();
|
||||
|
||||
private:
|
||||
// Helper functions
|
||||
bool isSameDate(String eventDateTime, int year, int month, int day);
|
||||
void addToTodaysSchedule(JsonObject event);
|
||||
void sortEventsByTime();
|
||||
String getCurrentTimeString();
|
||||
|
||||
// Core functionality
|
||||
void checkScheduledEvents();
|
||||
void triggerEvent(ScheduledEvent& event);
|
||||
|
||||
// New clock features - comprehensive alert and automation system
|
||||
void checkClockAlerts();
|
||||
void triggerHourlyAlert(int hour);
|
||||
void checkBacklightAutomation();
|
||||
bool isInSilencePeriod();
|
||||
bool isTimeInRange(const String& currentTime, const String& startTime, const String& endTime) const;
|
||||
void fireAlertBell(uint8_t bellNumber, int count = 1);
|
||||
|
||||
// Physical clock synchronization - SIMPLE approach based on your Arduino code
|
||||
void checkAndSyncPhysicalClock();
|
||||
void advancePhysicalClockOneMinute();
|
||||
void updatePhysicalClockTime();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK METHOD
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
public:
|
||||
/** @brief Check if TimeKeeper is in healthy state */
|
||||
bool isHealthy() const;
|
||||
};
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user