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

213
vesper/class_player.hpp Normal file
View File

@@ -0,0 +1,213 @@
#pragma once
extern std::vector<uint16_t> melody_steps;
void sendToApp(String jsonMessage);
void PublishMqtt(const char * data);
bool addMelody(JsonVariant doc);
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
std::string uid = "x"; // The UID of the melody, from Firestore
std::string url = "-"; // The URL of the binary, from Firestore
uint16_t noteAssignments[16] = {0}; // The list of Note Assignments for the melody
uint16_t speed = 500; // Time to wait per beat. (In Miliseconds)
uint32_t segment_duration = 15000; // Duration of the playback per segment (per "loop")
uint32_t pause_duration = 0; // Duration of the pauses between segments (loops)
uint32_t total_duration = 0; // The Total Run Duration of the program. Including all loops
uint64_t segmentCmpltTime = 0; // The time of completion of the last Segment
uint64_t segmentStartTime = 0; // The time-point the current segment started Playing
uint64_t startTime = 0; // The time-point the Melody started Playing
uint64_t pauseTime = 0; // The time-point the melody paused
bool continuous_loop = false; // Indicates if the Loop should Run 1-Take or Continuous with Intervals
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
bool hardStop = false; // Flags a hardstop, immediately.
void play() {
isPlaying = true;
hardStop = false;
startTime = segmentStartTime = millis();
LOG_DEBUG("Plbck: PLAY");
}
void forceStop() {
hardStop = true;
isPlaying = false;
LOG_DEBUG("Plbck: FORCE STOP");
}
void stop() {
hardStop = false;
isPlaying = false;
LOG_DEBUG("Plbck: STOP");
StaticJsonDocument<128> doc;
doc["status"] = "NOTIFY";
doc["type"] = "playback";
doc["payload"] = "stop";
String output;
serializeJson(doc, output);
sendToApp(output);
}
void pause() {
isPaused = true;
LOG_DEBUG("Plbck: PAUSE");
}
void unpause() {
isPaused = false;
segmentStartTime = millis();
LOG_DEBUG("Plbck: RESUME");
}
// Handles Incoming Commands to PLAY or STOP
void command(JsonVariant data, AsyncWebSocketClient *client = nullptr){
setMelodyAttributes(data);
loadMelodyInRAM(melody_steps);
String action = data["action"];
LOG_DEBUG("Incoming Command: %s", action);
// Play or Stop Logic
if (action == "play") {
play();
} else if (action == "stop") {
forceStop();
} else {
return;
}
// Prepare JSON response
StaticJsonDocument<128> response;
response["status"] = "OK";
response["type"] = action; // "play" or "stop"
response["payload"] = nullptr; // Use null in JSON
// Serialize JSON to char buffer
char jsonOut[256];
serializeJson(response, jsonOut);
// Send via MQTT
PublishMqtt(jsonOut);
// Optionally send via WebSocket
if (client) {
client->text(jsonOut);
}
}
// 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("uid")) {
uid = doc["uid"].as<const char*>();
}
if (doc.containsKey("url")) {
url = doc["url"].as<const char*>();
}
if (doc.containsKey("speed")) {
speed = doc["speed"].as<uint16_t>();
}
if (doc.containsKey("note_assignments")) {
JsonArray noteArray = doc["note_assignments"];
size_t arraySize = min(noteArray.size(), (size_t)16);
for (size_t i = 0; i < arraySize; i++) {
noteAssignments[i] = noteArray[i];
}
}
if (doc.containsKey("segment_duration")) {
segment_duration = doc["segment_duration"].as<uint32_t>();
}
if (doc.containsKey("pause_duration")) {
pause_duration = doc["pause_duration"].as<uint32_t>();
}
if (doc.containsKey("total_duration")) {
total_duration = doc["total_duration"].as<uint32_t>();
}
if (doc.containsKey("continuous_loop")) {
continuous_loop = doc["continuous_loop"].as<bool>();
}
if (continuous_loop && total_duration == 0){
infinite_play = true;
}
if (!continuous_loop) {
total_duration = segment_duration;
}
// Print Just for Debugging Purposes
LOG_DEBUG("Set Melody Vars / Name: %s, UID: %s",
name.c_str(),
uid.c_str()
);
// Print Just for Debugging Purposes
LOG_DEBUG("URL: %s",
url.c_str()
);
// Print Just for Debugging Purposes
LOG_DEBUG("Speed: %d, Per Segment Duration: %lu, Pause Duration: %lu, Total Duration: %d, Continuous: %s, Infinite: %s",
speed,
segment_duration,
pause_duration,
total_duration,
continuous_loop ? "true" : "false",
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(uid.c_str());
File bin_file = SD.open(filePath.c_str(), FILE_READ);
if (!bin_file) {
LOG_ERROR("Failed to open file: %s", filePath.c_str());
LOG_ERROR("Check Servers for the File...");
StaticJsonDocument<128> doc;
doc["download_url"] = url;
doc["melodys_uid"] = uid;
if (!addMelody(doc)){
LOG_ERROR("Failed to Download File. Check Internet Connection");
return;
} else {
bin_file = SD.open(filePath.c_str(), FILE_READ);
}
}
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();
}
};