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:
335
vesper/classes.hpp
Normal file
335
vesper/classes.hpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user