Added basic Scheduling Functionality

A JSON message can now be received on:
'vesper/DEV_ID/control/addSchedule"
Each message, must hold a "file" and "data".
The file is the month's name in 3 letter mode (eg jan, feb, mar)
The data is an entry for each day of the month.
Each day can be an array containing multiple items.
This commit is contained in:
2025-01-26 14:02:15 +02:00
parent 84534025f4
commit 7dd6f81264
254 changed files with 148509 additions and 946 deletions

335
vesper/classes.hpp Normal file
View File

@@ -0,0 +1,335 @@
#pragma once
extern std::vector<uint16_t> melody_steps;
void PublishMqtt(const char * data);
class Player {
public:
uint16_t id; // The (internal) ID of the selected melody. Not specificly used anywhere atm. Might be used later.
std::string name = "melody1"; // Name of the Melody saved. Will be used to read the file: /name.bin
uint16_t speed = 500; // Time to wait per beat. (In Miliseconds)
uint32_t duration = 15000; // Total Duration that program will run (In Miliseconds)
uint32_t loop_duration = 0; // Duration of the playback per segment
uint32_t interval = 0; // Indicates the Duration of the Interval between finished segments, IF "inf" is true
bool infinite_play = false; // Infinite Loop Indicator (If True the melody will loop forever or until stoped, with pauses of "interval" in between loops)
bool isPlaying = false;; // Indicates if the Melody is actually Playing right now.
bool isPaused = false; // If playing, indicates if the Melody is Paused
uint64_t startTime = 0; // The time-point the Melody started Playing
uint64_t loopStartTime = 0; // The time-point the current segment started Playing
bool hardStop = false; // Flags a hardstop, immediately.
uint64_t pauseTime = 0; // The time-point the melody paused
void play() {
isPlaying = true;
hardStop = false;
startTime = loopStartTime = millis();
Serial.println("Plbck: PLAY");
}
void forceStop() {
hardStop = true;
isPlaying = false;
Serial.println("Plbck: FORCE STOP");
}
void stop() {
hardStop = false;
isPlaying = false;
Serial.println("Plbck: STOP");
}
void pause() {
isPaused = true;
Serial.println("Plbck: PAUSE");
}
void unpause() {
isPaused = false;
loopStartTime = millis();
Serial.println("Plbck: RESUME");
}
// Handles Incoming Commands to PLAY or STOP
void command(char * command){
Serial.print("INCOMING COMMAND: ");
Serial.println(command);
if (command[0] == '1') {
play();
PublishMqtt("OK - PLAY");
} else if (command[0] == '0') {
forceStop();
PublishMqtt("OK - STOP");
}
}
// Sets incoming Attributes for the Melody, into the class' variables.
void setMelodyAttributes(JsonDocument doc){
if (doc.containsKey("name")) {
name = doc["name"].as<const char*>();
}
if (doc.containsKey("id")) {
id = doc["id"].as<uint16_t>();
}
if (doc.containsKey("duration")) {
duration = doc["duration"].as<uint32_t>();
}
if (doc.containsKey("infinite")) {
infinite_play = doc["infinite"].as<bool>();
}
if (doc.containsKey("interval")) {
interval = doc["interval"].as<uint32_t>();
}
if (doc.containsKey("speed")) {
speed = doc["speed"].as<uint16_t>();
}
if (doc.containsKey("loop_dur")) {
loop_duration = doc["loop_dur"].as<uint32_t>();
}
// Print Just for Debugging Purposes
Serial.printf("Name: %s, ID: %d, Total Duration: %lu, Loop Duration: %lu, Interval: %d, Speed: %d, Inf: %s\n",
name.c_str(),
id,
duration,
loop_duration,
interval,
speed,
infinite_play ? "true" : "false"
);
}
// Loads the Selected melody from a .bin file, into RAM
void loadMelodyInRAM(std::vector<uint16_t> &melody_steps) {
std::string filePath = "/" + name + ".bin";
Serial.println("New Melody Selected !!!");
Serial.println("Reading data from file...");
File bin_file = SPIFFS.open(filePath.c_str(), "r");
if (!bin_file) {
Serial.println("Failed to Open File");
return;
}
size_t fileSize = bin_file.size();
size_t steps = fileSize / 2;
melody_steps.resize(steps);
Serial.print("Opened File ! Size: ");
Serial.print(fileSize);
Serial.print(" Steps: ");
Serial.println(steps);
for (size_t i=0; i<steps; i++){
melody_steps[i] = bin_file.read() << 8 | bin_file.read();
}
for (size_t i=0; i<steps; i++){
Serial.print("Current Step: ");
Serial.printf("%03d // ", i);
Serial.print(" HEX Value: ");
Serial.printf("0x%04X\n", melody_steps[i]);
}
Serial.println("Closing File");
bin_file.close();
// closing the file
}
};
class TimeKeeper {
private:
// holds the daily scheduled melodies
struct ScheduleEntry {
std::string mel;
uint8_t hour;
uint8_t minute;
uint16_t speed;
uint16_t duration;
};
struct TimeNow {
uint8_t hour;
uint8_t minute;
uint8_t second;
};
std::vector<ScheduleEntry> dailySchedule;
TimeNow now;
// update to current time
void updateTime(){
now.hour = getHour();
now.minute = getMinute();
now.second = getSecond();
}
// Read the current Month's JSON file and add the daily entries to the dailySchedule
void loadDailySchedule() {
std::string filePath = "/" + getMonth() + ".json";
File file = SPIFFS.open(filePath.c_str(), "r");
if (!file) {
Serial.printf("Failed to open file: %s\n", filePath.c_str());
return;
}
Serial.println("Opened daily schedule file");
StaticJsonDocument<8192> doc; // Adjust size based on expected JSON complexity
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error) {
Serial.printf("Failed to parse JSON: %s\n", error.c_str());
return;
}
Serial.println("- - - SERIALIZE JSON - - -");
serializeJsonPretty(doc, Serial);
Serial.println("- - - - END JSON - - - -");
// Extract entries for the current day
std::string currentDay = getDay();
dailySchedule.clear(); // Clear previous day's schedule
Serial.printf("Current Day: %s\n", currentDay.c_str());
if (doc.containsKey(currentDay.c_str())) {
Serial.println("doc contains key!");
// Check if the current day contains a single object or an array
JsonVariant dayVariant = doc[currentDay.c_str()];
if (dayVariant.is<JsonArray>()) {
// Handle case where the day contains an array of entries
JsonArray dayEntries = dayVariant.as<JsonArray>();
for (JsonObject entry : dayEntries) {
ScheduleEntry schedule;
schedule.mel = entry["name"].as<std::string>();
schedule.hour = entry["hour"];
schedule.minute = entry["minute"];
schedule.speed = entry["speed"];
schedule.duration = entry["duration"];
dailySchedule.push_back(schedule);
Serial.printf("Added Entry - Melody: %s, Hour: %d, Minute: %d, Speed: %d, Duration: %d\n",
schedule.mel.c_str(), schedule.hour, schedule.minute, schedule.speed, schedule.duration);
}
} else if (dayVariant.is<JsonObject>()) {
// Handle case where the day contains a single object
JsonObject entry = dayVariant.as<JsonObject>();
ScheduleEntry schedule;
schedule.mel = entry["name"].as<std::string>();
schedule.hour = entry["hour"];
schedule.minute = entry["minute"];
schedule.speed = entry["speed"];
schedule.duration = entry["duration"];
dailySchedule.push_back(schedule);
Serial.printf("Added Single Entry - Melody: %s, Hour: %d, Minute: %d, Speed: %d, Duration: %d\n",
schedule.mel.c_str(), schedule.hour, schedule.minute, schedule.speed, schedule.duration);
} else {
Serial.println("Invalid data format for the current day.");
}
} else {
Serial.println("No schedule found for today.");
}
}
// Calls "loadDailySchedule" and returns true ONCE per new day (00:00)
bool newDayCheck() {
// Static variable to store whether we've already detected the new day
static bool alreadyTriggered = false;
// Check if it's midnight
if (now.hour == 0 && now.minute == 0) {
if (!alreadyTriggered) {
// First time at midnight, trigger the event
alreadyTriggered = true;
Serial.println("New day detected, returning true.");
loadDailySchedule();
return true;
} else {
// It's still midnight, but we've already triggered
Serial.println("Already triggered for today, returning false.");
return false;
}
} else {
// Reset the trigger after midnight has passed
alreadyTriggered = false;
return false;
}
}
public:
// get date and time data from the RTC module
uint16_t getYear() { return rtc.now().year(); }
std::string getMonth() { return monthName(rtc.now().month()); }
std::string getDay() { return std::to_string(rtc.now().day()); }
uint8_t getHour() { return rtc.now().hour(); }
uint8_t getMinute() { return rtc.now().minute(); }
uint8_t getSecond() { return rtc.now().second(); }
// turn months from decimal to char*
std::string monthName(uint8_t monthNumber) {
const char* monthNames[] = {
"jan", "feb", "mar", "apr", "may", "jun",
"jul", "aug", "sep", "oct", "nov", "dec"
};
return monthNumber >= 1 && monthNumber <= 12 ? monthNames[monthNumber - 1] : "unknown";
}
// Loads and updates the daily schedule
void refreshDailySchedule() { loadDailySchedule(); }
// Returns the daily schedule in "ScheduleEntry" format
const std::vector<ScheduleEntry>& getDailySchedule() const { return dailySchedule; }
// Prints the time, NOW.
void printTimeNow(){
Serial.printf("Current Time: %s %s, %u - %02u:%02u:%02u\n",
getMonth().c_str(), // Month as a string
getDay().c_str(), // Day as a string
getYear(), // Year as an integer
getHour(), // Hour (24-hour format)
getMinute(), // Minute
getSecond()); // Second
}
void tick(){
updateTime();
newDayCheck();
}
void checkAndRunSchedule(Player & player) {
for (auto it = dailySchedule.begin(); it != dailySchedule.end(); ) {
Serial.println("Running daily schedule check");
if (now.hour == it->hour && now.minute == it->minute) {
Serial.printf("Entry Exists, returning True!");
StaticJsonDocument<200> jsonDoc;
jsonDoc["name"] = it->mel.c_str();
jsonDoc["speed"] = it->speed;
jsonDoc["duration"] = it->duration;
jsonDoc["loop_dur"] = 0;
jsonDoc["internal"] = 0;
Serial.printf("name: %s speed: %d duration: %d",it->mel.c_str(), it->speed, it->duration);
if (!player.isPlaying){
player.setMelodyAttributes(jsonDoc);
player.loadMelodyInRAM(melody_steps);
player.play();
it = dailySchedule.erase(it);
}
}
else {
Serial.printf("No Entry, returning False!");
++it;
}
}
}
};