MAJOR update. More like a Backup before things get Crazy

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
This commit is contained in:
2025-09-05 19:27:13 +03:00
parent c1fa1d5e57
commit 101f9e7135
20 changed files with 10746 additions and 9766 deletions

392
vesper/timekeeper.cpp Normal file
View File

@@ -0,0 +1,392 @@
#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
}