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.
336 lines
11 KiB
C++
336 lines
11 KiB
C++
#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;
|
|
}
|
|
}
|
|
}
|
|
|
|
};
|