Added Websocket Support Added Universal Message Handling for both MQTT and WS Added Timekeeper Class, that handles Physical Clock and Scheduling Added Bell Assignment Settings, Note to Bell mapping
393 lines
12 KiB
C++
393 lines
12 KiB
C++
#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<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();
|
|
|
|
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<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);
|
|
|
|
Serial.printf("Added event '%s' at %s\n",
|
|
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);
|
|
}
|
|
|
|
// Static task functions
|
|
void Timekeeper::clockTask(void* parameter) {
|
|
Timekeeper* keeper = static_cast<Timekeeper*>(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<Timekeeper*>(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<Timekeeper*>(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<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>();
|
|
|
|
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
|
|
}
|