823 lines
30 KiB
C++
823 lines
30 KiB
C++
#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 <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::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<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);
|
|
|
|
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<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;
|
|
}
|
|
|
|
// 🔥 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;
|
|
} |