Added basic WebSocket Functionality

This commit is contained in:
2025-07-12 18:14:40 +03:00
parent 516eeab751
commit c1fa1d5e57
27 changed files with 138489 additions and 906 deletions

View File

@@ -13,7 +13,7 @@ public:
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 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
@@ -51,276 +51,83 @@ public:
}
// Handles Incoming Commands to PLAY or STOP
void command(char * command){
LOG_DEBUG("Incoming Command: %s",command);
if (command[0] == '1') {
void command(JsonVariant content){
String action = content["action"];
LOG_DEBUG("Incoming Command: %s", action);
if (action == "play") {
play();
PublishMqtt("OK - PLAY");
} else if (command[0] == '0') {
} else if (action == "stop") {
forceStop();
PublishMqtt("OK - STOP");
}
}
// Sets incoming Attributes for the Melody, into the class' variables.
void setMelodyAttributes(JsonDocument doc){
// Sets incoming Attributes for the Melody, into the class' variables.
void setMelodyAttributes(JsonVariant 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
LOG_DEBUG("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"
);
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
LOG_DEBUG("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 on SD into RAM
void loadMelodyInRAM(std::vector<uint16_t> &melody_steps) {
String filePath = "/melodies/" + String(name.c_str()) + ".bin";
File bin_file = SD.open(filePath.c_str(), FILE_READ);
if (!bin_file) {
LOG_ERROR("Failed to open file: %s", filePath.c_str());
return;
}
size_t fileSize = bin_file.size();
if (fileSize % 2 != 0) {
LOG_ERROR("Invalid file size: %u (not a multiple of 2)", fileSize);
bin_file.close();
return;
}
melody_steps.resize(fileSize / 2);
for (size_t i = 0; i < melody_steps.size(); i++) {
uint8_t high = bin_file.read();
uint8_t low = bin_file.read();
melody_steps[i] = (high << 8) | low;
}
LOG_INFO("Melody Load Successful");
bin_file.close();
}
// Loads the Selected melody from a .bin file, into RAM
void loadMelodyInRAM(std::vector<uint16_t> &melody_steps) {
LOG_INFO("Loading Melody.");
std::string filePath = "/" + name + ".bin";
File bin_file = SPIFFS.open(filePath.c_str(), "r");
if (!bin_file) {
LOG_ERROR("Failed to Open File");
return;
}
size_t fileSize = bin_file.size();
size_t steps = fileSize / 2;
melody_steps.resize(steps);
LOG_DEBUG("Opened File, size: %zu - Steps: %zu",fileSize,steps)
for (size_t i=0; i<steps; i++){
melody_steps[i] = bin_file.read() << 8 | bin_file.read();
LOG_DEBUG("Current Step: %03d // HEX Value: 0x%04X", i, melody_steps[i]);
}
LOG_DEBUG("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) {
LOG_ERROR("Failed to open file: %s\n", filePath.c_str());
return;
}
LOG_INFO("Opened daily schedule file");
StaticJsonDocument<8192> doc; // Adjust size based on expected JSON complexity
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error) {
LOG_ERROR("Failed to parse JSON: %s\n", error.c_str());
return;
}
if (LOG_LEVEL_ENABLED(LOG_LEVEL_DEBUG)){
String jsonString;
serializeJsonPretty(doc, jsonString);
LOG_DEBUG("Serialized JSON: /n%s",jsonString.c_str());
}
// Extract entries for the current day
std::string currentDay = getDay();
dailySchedule.clear(); // Clear previous day's schedule
LOG_DEBUG("Current Day: %s\n", currentDay.c_str());
if (doc.containsKey(currentDay.c_str())) {
LOG_DEBUG("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);
LOG_INFO("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);
LOG_INFO("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 {
LOG_WARNING("Invalid data format for the current day.");
}
} else {
LOG_INFO("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;
LOG_DEBUG("New day detected, returning true.");
loadDailySchedule();
return true;
} else {
// It's still midnight, but we've already triggered
LOG_DEBUG("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(){
LOG_INFO("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) {
LOG_DEBUG("Running daily schedule check");
for (auto it = dailySchedule.begin(); it != dailySchedule.end(); ) {
if (now.hour == it->hour && now.minute == it->minute) {
LOG_DEBUG("Entry Exists, Calling program.");
StaticJsonDocument<200> jsonDoc;
jsonDoc["name"] = it->mel.c_str();
jsonDoc["speed"] = it->speed;
jsonDoc["duration"] = it->duration;
jsonDoc["loop_dur"] = 0;
jsonDoc["internal"] = 0;
//LOG_INFO("Entry Found. 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 {
LOG_DEBUG("Entry's time doesn't match. Skipping.");
++it;
}
}
}
};