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:
40
vesper/MQTT_Functions.hpp
Normal file
40
vesper/MQTT_Functions.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
// MQTT Functions.
|
||||
// Both for Incoming and Outgoing MQTT Messages
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Publishes a message on the MQTT server. Message passed as an argument.
|
||||
void PublishMqtt(const char * data) {
|
||||
if (mqttClient.connected()){
|
||||
String topicData = String("vesper/") + DEV_ID + "/data";
|
||||
mqttClient.publish(topicData.c_str(), 0, true, data);
|
||||
} else {
|
||||
LOG_ERROR("MQTT Not Connected ! Message Failed.");
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribes to certain topics on the MQTT Server.
|
||||
void SuscribeMqtt() {
|
||||
|
||||
String topic = String("vesper/") + DEV_ID + "/control";
|
||||
uint16_t topic_id = mqttClient.subscribe(topic.c_str(), 2);
|
||||
LOG_INFO("Subscribing to Command topic, QoS 2, packetId: %d", topic_id);
|
||||
|
||||
}
|
||||
|
||||
// Handles incoming MQTT messages on subscribed topics.
|
||||
// Could move logic out of this into a dedicated function.
|
||||
void OnMqttReceived(char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
|
||||
|
||||
String inc_topic = String("vesper/") + DEV_ID + "/control";
|
||||
|
||||
// Don't know what this is. Check it out later.
|
||||
//String payloadContent = String(payload).substring(0, len);
|
||||
|
||||
if (String(topic) == inc_topic){
|
||||
JsonDocument command = payload2json(payload);
|
||||
handleCommand(command);
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
void setRelayDurations(JsonDocument& doc);
|
||||
void updateRelayTimings();
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Handles the incoming payload. Returns it into a "JsonDocument" format.
|
||||
JsonDocument handleJSON(char * payload) {
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, payload);
|
||||
if (error) {
|
||||
Serial.print("deserializeJson() failed: ");
|
||||
Serial.println(error.c_str());
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
|
||||
void replyOnWebSocket(AsyncWebSocketClient *client, String list) {
|
||||
client->text(list);
|
||||
}
|
||||
|
||||
// Handles the JSON Commands
|
||||
void handleCommand(JsonDocument json, AsyncWebSocketClient *client = nullptr){
|
||||
|
||||
String cmd = json["cmd"];
|
||||
JsonVariant contents = json["contents"];
|
||||
|
||||
if (cmd == "playback") {
|
||||
player.command(contents);
|
||||
} else if (cmd == "set_melody") {
|
||||
player.setMelodyAttributes(contents);
|
||||
player.loadMelodyInRAM(melody_steps);
|
||||
} else if (cmd == "list_melodies") {
|
||||
String list = listFilesAsJson("/melodies");
|
||||
PublishMqtt(list.c_str());
|
||||
if (client) {
|
||||
replyOnWebSocket(client, list); // Only reply via WS if client exists
|
||||
}
|
||||
} else if (cmd == "set_relay_timers") {
|
||||
updateRelayTimings(contents);
|
||||
} else if (cmd == "add_melody"){
|
||||
addMelody(contents);
|
||||
} else {
|
||||
LOG_WARNING("Unknown Command Received");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Subscribes to certain topics on the MQTT Server.
|
||||
void SuscribeMqtt() {
|
||||
|
||||
String command = String("vesper/") + DEV_ID + "/control";
|
||||
uint16_t command_id = mqttClient.subscribe(command.c_str(), 2);
|
||||
LOG_INFO("Subscribing to Command topic, QoS 2, packetId: %d", command_id);
|
||||
|
||||
}
|
||||
|
||||
// Handles incoming MQTT messages on subscribed topics.
|
||||
// Could move logic out of this into a dedicated function.
|
||||
void OnMqttReceived(char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
|
||||
|
||||
String command = String("vesper/") + DEV_ID + "/control";
|
||||
|
||||
// Don't know what this is. Check it out later.
|
||||
//String payloadContent = String(payload).substring(0, len);
|
||||
|
||||
if (String(topic) == command){
|
||||
JsonDocument json = handleJSON(payload);
|
||||
handleCommand(json);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handles incoming WebSocket messages on subscribed topics.
|
||||
// Could move logic out of this into a dedicated function.
|
||||
void onWebSocketReceived(AsyncWebSocketClient *client, void *arg, uint8_t *data, size_t len) {
|
||||
AwsFrameInfo *info = (AwsFrameInfo*)arg;
|
||||
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
|
||||
data[len] = '\0'; // Null-terminate the received data
|
||||
Serial.printf("Received message: %s\n", (char*)data);
|
||||
|
||||
JsonDocument json = handleJSON((char*)data);
|
||||
handleCommand(json, client);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void onWebSocketEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
|
||||
if (type == WS_EVT_DATA) {
|
||||
onWebSocketReceived(client, arg, data, len);
|
||||
}
|
||||
}
|
||||
|
||||
// Publishes a message on the MQTT server. Message passed as an argument.
|
||||
void PublishMqtt(const char * data) {
|
||||
String topicData = String("vesper/") + DEV_ID + "/data";
|
||||
mqttClient.publish(topicData.c_str(), 0, true, data);
|
||||
}
|
||||
@@ -12,7 +12,6 @@ void durationTimer(void *param) {
|
||||
|
||||
// Task Setup
|
||||
|
||||
|
||||
// Task Loop
|
||||
while (true) {
|
||||
|
||||
@@ -26,16 +25,16 @@ void durationTimer(void *param) {
|
||||
player.unpause();
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(1000)); // Check every 100ms
|
||||
vTaskDelay(pdMS_TO_TICKS(500)); // Check every 500ms
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's time to stop playback
|
||||
bool timeToStop(unsigned long now) {
|
||||
if (player.isPlaying) {
|
||||
uint64_t stopTime = player.startTime + player.duration;
|
||||
if (player.isPlaying && !player.infinite_play) {
|
||||
uint64_t stopTime = player.startTime + player.total_duration;
|
||||
if (now >= stopTime) {
|
||||
LOG_DEBUG("TIMER: Total Duration Reached");
|
||||
LOG_DEBUG("(TimerFunction) Total Run Duration Reached. Soft Stopping.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -44,11 +43,11 @@ bool timeToStop(unsigned long now) {
|
||||
|
||||
// Check if it's time to pause playback
|
||||
bool timeToPause(unsigned long now) {
|
||||
if (player.isPlaying && player.loop_duration > 0) {
|
||||
uint64_t pauseTimeLimit = player.loopStartTime + player.loop_duration;
|
||||
LOG_DEBUG("PTL: %lu // NOW: %d",pauseTimeLimit, now);
|
||||
if (now >= pauseTimeLimit && !player.isPaused) {
|
||||
LOG_DEBUG("TIMER: Segment Duration Reached");
|
||||
if (player.isPlaying && player.continuous_loop) {
|
||||
uint64_t timeToPause = player.segmentStartTime + player.segment_duration;
|
||||
LOG_DEBUG("PTL: %llu // NOW: %lu", timeToPause, now);
|
||||
if (now >= timeToPause && !player.isPaused) {
|
||||
LOG_DEBUG("(TimerFunction) Segment Duration Reached. Pausing.");
|
||||
player.pauseTime = now;
|
||||
return true;
|
||||
}
|
||||
@@ -59,9 +58,9 @@ bool timeToPause(unsigned long now) {
|
||||
// Check if it's time to resume playback
|
||||
bool timeToResume(unsigned long now) {
|
||||
if (player.isPaused) {
|
||||
uint64_t resumeTime = player.pauseTime + player.interval;
|
||||
if (now >= resumeTime) {
|
||||
LOG_DEBUG("TIMER: Pause Duration Reached");
|
||||
uint64_t timeToResume = player.segmentCmpltTime + player.pause_duration;
|
||||
if (now >= timeToResume) {
|
||||
LOG_DEBUG("(TimerFunction) Pause Duration Reached. Resuming");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
49
vesper/WebSocket_Functions.hpp
Normal file
49
vesper/WebSocket_Functions.hpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
void sendToApp(String jsonMessage) {
|
||||
if (activeClient && activeClient->status() == WS_CONNECTED) {
|
||||
activeClient->text(jsonMessage);
|
||||
} else {
|
||||
Serial.println("No active WebSocket client connected.");
|
||||
}
|
||||
}
|
||||
|
||||
void replyOnWebSocket(AsyncWebSocketClient *client, String list) {
|
||||
LOG_DEBUG("Sending WebSocket reply: %s", list.c_str());
|
||||
client->text(list);
|
||||
}
|
||||
|
||||
// Handles incoming WebSocket messages on subscribed topics.
|
||||
// Could move logic out of this into a dedicated function.
|
||||
void onWebSocketReceived(AsyncWebSocketClient *client, void *arg, uint8_t *data, size_t len) {
|
||||
AwsFrameInfo *info = (AwsFrameInfo*)arg;
|
||||
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
|
||||
data[len] = '\0'; // Null-terminate the received data
|
||||
Serial.printf("Received message: %s\n", (char*)data);
|
||||
|
||||
JsonDocument json = payload2json((char*)data);
|
||||
handleCommand(json, client);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void onWebSocketEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
|
||||
if (type == WS_EVT_DATA) {
|
||||
onWebSocketReceived(client, arg, data, len);
|
||||
Serial.println("WebSocket Message Received");
|
||||
}
|
||||
if (type == WS_EVT_CONNECT) {
|
||||
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
|
||||
activeClient = client; // Save the connected client
|
||||
}
|
||||
if (type == WS_EVT_DISCONNECT) {
|
||||
Serial.printf("WebSocket client #%u disconnected\n", client->id());
|
||||
if (client == activeClient) {
|
||||
activeClient = nullptr; // Clear it if it's the one we saved
|
||||
}
|
||||
}
|
||||
if (type == WS_EVT_ERROR) {
|
||||
Serial.printf("WebSocket client #%u error(%u): %s\n", client->id(), *((uint16_t*)arg), (char*)data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +1,27 @@
|
||||
// MELODY PLAYBACK WILL BE HANDLED HERE
|
||||
#include <vector>
|
||||
|
||||
extern Player player;
|
||||
|
||||
// Define a structure to track active solenoids
|
||||
struct ActiveRelay {
|
||||
uint8_t relayIndex; // Index of the relay
|
||||
uint8_t relayIndex; // Physical relay index (0-15)
|
||||
uint8_t bellIndex; // Bell index for duration lookup
|
||||
uint64_t activationTime; // Activation start time
|
||||
uint16_t duration; // Duration for which it should remain active
|
||||
};
|
||||
|
||||
// Array of durations for each relay (configure remotely)
|
||||
uint16_t relayDurations[16] = {90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90};
|
||||
// Duration per BELL (not per relay output)
|
||||
uint16_t bellDurations[16] = {90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90};
|
||||
|
||||
// Level 1: Bell to Physical Output mapping (bell index -> relay index)
|
||||
// bellOutputs[0] = which relay controls Bell #0, etc.
|
||||
uint16_t bellOutputs[16] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; // 0-based indexing
|
||||
|
||||
// Vector to track active solenoids
|
||||
std::vector<ActiveRelay> activeRelays;
|
||||
|
||||
|
||||
// Locks for Writing the Counters
|
||||
portMUX_TYPE mySpinlock = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
void loop_playback(std::vector<uint16_t> &melody_steps);
|
||||
void bellEngine(void *parameter);
|
||||
@@ -23,6 +29,7 @@ void relayControlTask(void *param);
|
||||
void itsHammerTime(uint16_t note);
|
||||
void turnOffRelays(uint64_t now);
|
||||
|
||||
// Main Bell Engine. Activates Relays on the exact timing required.
|
||||
void bellEngine(void *parameter) {
|
||||
// SETUP TASK
|
||||
for (;;) {
|
||||
@@ -36,7 +43,7 @@ void bellEngine(void *parameter) {
|
||||
}
|
||||
}
|
||||
|
||||
// Task to deactivate relays dynamically
|
||||
// Task to deactivate relays dynamically after set timers
|
||||
void relayControlTask(void *param) {
|
||||
while (true) {
|
||||
uint64_t now = millis();
|
||||
@@ -49,14 +56,15 @@ void relayControlTask(void *param) {
|
||||
++it; // Move to the next relay
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(10)); // Check every 10ms
|
||||
}
|
||||
}
|
||||
|
||||
// Function to wait for tempo, then loop to the next beat.
|
||||
void loop_playback(std::vector<uint16_t> &melody_steps) {
|
||||
|
||||
while(player.isPlaying && !player.isPaused){
|
||||
while(player.isPlaying && !player.isPaused){
|
||||
LOG_DEBUG("(BellEngine) Single Loop Starting.");
|
||||
|
||||
// iterate through the beats and call the bell mechanism on each beat
|
||||
for (uint16_t note : melody_steps) {
|
||||
@@ -66,21 +74,92 @@ void loop_playback(std::vector<uint16_t> &melody_steps) {
|
||||
vTaskDelay(pdMS_TO_TICKS(tempo));
|
||||
}
|
||||
|
||||
LOG_DEBUG("Single Loop Over.");
|
||||
//if (!player.isPlaying) break; // Stop playback only after completing the loop
|
||||
player.segmentCmpltTime = millis();
|
||||
LOG_DEBUG("(BellEngine) Single Full Loop Complete");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Function to activate relays for a specific note
|
||||
void itsHammerTime(uint16_t note) {
|
||||
uint64_t now = millis();
|
||||
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
if (note & (1 << i)) { // Check if this relay needs to activate
|
||||
relays.digitalWrite(i, LOW); // Activate the relay
|
||||
// First, determine which bells should ring based on the note pattern
|
||||
for (uint8_t noteIndex = 0; noteIndex < 16; noteIndex++) {
|
||||
if (note & (1 << noteIndex)) { // This note should be played
|
||||
|
||||
// Add to the activeRelays list
|
||||
activeRelays.push_back({i, now, relayDurations[i]});
|
||||
// Level 2: Map note to bell using noteAssignments
|
||||
uint8_t bellIndex = player.noteAssignments[noteIndex];
|
||||
|
||||
// Skip if no bell assigned to this note (0 means no assignment)
|
||||
if (bellIndex == 0) continue;
|
||||
|
||||
// Convert to 0-based indexing (noteAssignments uses 1-based)
|
||||
bellIndex = bellIndex - 1;
|
||||
|
||||
// Level 1: Map bell to physical relay output
|
||||
uint8_t relayIndex = bellOutputs[bellIndex];
|
||||
|
||||
// Activate the relay
|
||||
relays.digitalWrite(relayIndex, LOW);
|
||||
|
||||
// Add to the activeRelays list with bell-specific duration
|
||||
activeRelays.push_back({
|
||||
relayIndex,
|
||||
bellIndex,
|
||||
now,
|
||||
bellDurations[bellIndex]
|
||||
});
|
||||
|
||||
// Write ring to counter (count per bell, not per relay)
|
||||
portENTER_CRITICAL(&mySpinlock);
|
||||
strikeCounters[bellIndex]++; // Count strikes per bell
|
||||
bellLoad[bellIndex]++; // Load per bell
|
||||
coolingActive = true;
|
||||
portEXIT_CRITICAL(&mySpinlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helper function to update bell-to-output mapping via JSON
|
||||
void updateBellOutputs(JsonVariant doc) {
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
String key = String("b") + (i + 1);
|
||||
if (doc.containsKey(key)) {
|
||||
bellOutputs[i] = doc[key].as<uint16_t>()-1; // -1 to convert to 0 based indexing
|
||||
LOG_DEBUG("Bell %d Output set to Relay #%d", i + 1, bellOutputs[i]+1);
|
||||
} else {
|
||||
LOG_DEBUG("Bell %d not found in JSON payload. Keeping previous Output: Relay #%d", i + 1, bellOutputs[i]+1);
|
||||
}
|
||||
}
|
||||
LOG_INFO("Updated Relay Outputs.")
|
||||
StaticJsonDocument<128> response;
|
||||
response["status"] = "OK";
|
||||
response["type"] = "set_relay_outputs";
|
||||
char jsonOut[128]; // Create Char Buffer
|
||||
serializeJson(response, jsonOut); // Serialize to Buffer
|
||||
replyOnWebSocket(activeClient, jsonOut); // Reply on WebSocket
|
||||
}
|
||||
|
||||
// Sets Incoming Relay Durations to RAM and then call funtion to save them to file.
|
||||
void updateRelayTimings(JsonVariant doc) {
|
||||
// Iterate through the relays in the JSON payload
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
String key = String("b") + (i + 1); // Generate "b1", "b2", ...
|
||||
if (doc.containsKey(key)) {
|
||||
bellDurations[i] = doc[key].as<uint16_t>();
|
||||
LOG_DEBUG("Relay %d duration set to %d ms", i + 1, bellDurations[i]);
|
||||
} else {
|
||||
LOG_DEBUG("Relay %d not found in JSON payload. Keeping previous duration: %d ms", i + 1, bellDurations[i]);
|
||||
}
|
||||
}
|
||||
saveRelayTimings();
|
||||
LOG_INFO("Updated Relay Timings.")
|
||||
StaticJsonDocument<128> response;
|
||||
response["status"] = "OK";
|
||||
response["type"] = "set_relay_timings";
|
||||
char jsonOut[128]; // Create Char Buffer
|
||||
serializeJson(response, jsonOut); // Serialize to Buffer
|
||||
replyOnWebSocket(activeClient, jsonOut); // Reply on WebSocket
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@ void setRelayDurations(JsonDocument& doc);
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Handles the incoming payload. Returns it into a "JsonDocument" format.
|
||||
JsonDocument handleJSON(char * payload) {
|
||||
JsonDocument payload2json(char * payload) {
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, payload);
|
||||
if (error) {
|
||||
@@ -62,7 +62,7 @@ void OnMqttReceived(char * topic, char * payload, AsyncMqttClientMessageProperti
|
||||
}
|
||||
|
||||
else if (String(topic) == topicSetMelody) {
|
||||
setMelodyAttributes(handleJSON(payload));
|
||||
setMelodyAttributes(payload2json(payload));
|
||||
loadMelodyInRAM(melody_steps);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ void OnMqttReceived(char * topic, char * payload, AsyncMqttClientMessageProperti
|
||||
}
|
||||
|
||||
else if (String(topic) == topicRelayTimers) {
|
||||
setRelayDurations(handleJSON(payload));
|
||||
setRelayDurations(payload2json(payload));
|
||||
}
|
||||
|
||||
else {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#line 1 "C:\\Users\\espi_\\Documents\\Arduino\\4. Bell Systems\\1. Main Projects\\Project - Vesper\\PlaybackControls.hpp"
|
||||
#pragma once
|
||||
|
||||
extern melody_attributes melody;
|
||||
@@ -12,6 +11,8 @@ void durationTimer(void *param);
|
||||
void durationTimer(void *param) {
|
||||
|
||||
// Task Setup
|
||||
Serial.print("Main millis: ");
|
||||
Serial.println(millis());
|
||||
|
||||
|
||||
// Task Loop
|
||||
@@ -27,7 +28,7 @@ void durationTimer(void *param) {
|
||||
melody.unpause();
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(1000)); // Check every 100ms
|
||||
vTaskDelay(pdMS_TO_TICKS(100)); // Check every 100ms
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +48,9 @@ bool timeToStop(unsigned long now) {
|
||||
bool timeToPause(unsigned long now) {
|
||||
if (melody.isPlaying && melody.loop_duration > 0) {
|
||||
uint64_t pauseTimeLimit = melody.loopStartTime + melody.loop_duration;
|
||||
Serial.printf("PTL: %lu // NOW: ",pauseTimeLimit);
|
||||
Serial.print("PTL: ");
|
||||
Serial.print(pauseTimeLimit);
|
||||
Serial.print(" // NOW: ");
|
||||
Serial.println(now);
|
||||
if (now >= pauseTimeLimit && !melody.isPaused) {
|
||||
Serial.println("TIMER: Segment Duration Reached");
|
||||
|
||||
213
vesper/class_player.hpp
Normal file
213
vesper/class_player.hpp
Normal 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();
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,133 +0,0 @@
|
||||
#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();
|
||||
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");
|
||||
}
|
||||
|
||||
void pause() {
|
||||
isPaused = true;
|
||||
LOG_DEBUG("Plbck: PAUSE");
|
||||
}
|
||||
|
||||
void unpause() {
|
||||
isPaused = false;
|
||||
loopStartTime = millis();
|
||||
LOG_DEBUG("Plbck: RESUME");
|
||||
}
|
||||
|
||||
// Handles Incoming Commands to PLAY or STOP
|
||||
void command(JsonVariant content){
|
||||
String action = content["action"];
|
||||
LOG_DEBUG("Incoming Command: %s", action);
|
||||
if (action == "play") {
|
||||
play();
|
||||
PublishMqtt("OK - PLAY");
|
||||
} else if (action == "stop") {
|
||||
forceStop();
|
||||
PublishMqtt("OK - STOP");
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,33 +1,92 @@
|
||||
ADD MELODY:
|
||||
####################################
|
||||
## DOWNLOAD MELODY ON DEVICE:
|
||||
####################################
|
||||
|
||||
{
|
||||
"cmd": "add_melody",
|
||||
"cmd": "download_melody",
|
||||
"contents": {
|
||||
"url": "URL",
|
||||
"filename": "NAME.EXT"
|
||||
"download_url": "http://url.com/SDFJDLS9043FM3RM1/binaries/melody.bin",
|
||||
"melodys_uid": "SDFJDLS9043FM3RM1"
|
||||
}
|
||||
}
|
||||
|
||||
melodys_uid >> The Reference UID of the Melody in the database
|
||||
|
||||
SET RELAY TIMERS:
|
||||
|
||||
{
|
||||
"cmd": "set_relay_timers",
|
||||
"contents": {"b1":100, "b2":200}
|
||||
"status": "OK",
|
||||
"type": "Download",
|
||||
"payload": "null"
|
||||
}
|
||||
|
||||
|
||||
START PLAYBACK:
|
||||
|
||||
|
||||
|
||||
####################################
|
||||
## SET RELAY TIMERS:
|
||||
####################################
|
||||
|
||||
{
|
||||
"cmd": "set_relay_durations",
|
||||
"contents": {"b1":100, "b2":200}
|
||||
}
|
||||
|
||||
no reply system implemented yet
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
####################################
|
||||
## SET RELAY OUTPUTS:
|
||||
####################################
|
||||
|
||||
{
|
||||
"cmd": "set_relay_outputs",
|
||||
"contents": {"b1":1, "b2":2}
|
||||
}
|
||||
|
||||
no reply system implemented yet
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
####################################
|
||||
## START PLAYBACK:
|
||||
####################################
|
||||
|
||||
{
|
||||
"cmd": "playback",
|
||||
"contents": {
|
||||
"action": "play"
|
||||
"action": "play",
|
||||
"name": "esperinos",
|
||||
"uid": "01DegzV9FA8tYbQpkIHR",
|
||||
"url": "https://firebasestorage.googleapis.com/v0/b/bs-vesper.firebasestorage.app/o/melodies%2F01DegzV9FA8tYbQpkIHR%2Fbinary.bin?alt=media&token=f63bfc48-de0e-44cb-a2cc-4880c675558b",
|
||||
"speed": 200,
|
||||
"loop_duration": 5000,
|
||||
"pause_duration": 30000,
|
||||
"total_duration": 200,
|
||||
"continuous_loop": false
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"status": "OK",
|
||||
"type": "play",
|
||||
"payload": "null"
|
||||
}
|
||||
|
||||
STOP PLAYBACK:
|
||||
|
||||
|
||||
|
||||
|
||||
####################################
|
||||
## STOP PLAYBACK:
|
||||
####################################
|
||||
|
||||
{
|
||||
"cmd": "playback",
|
||||
@@ -36,27 +95,108 @@ STOP PLAYBACK:
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"status": "OK",
|
||||
"type": "stop",
|
||||
"payload": "null"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
####################################
|
||||
## LIST MELODIES
|
||||
####################################
|
||||
|
||||
SET MELODY:
|
||||
|
||||
{
|
||||
"cmd": "set_melody",
|
||||
"cmd": "list_melodies"
|
||||
}
|
||||
|
||||
{
|
||||
"status": "OK",
|
||||
"type": "list_melodies",
|
||||
"payload": ["jdslkfj09823jvcm", "cj2309jcvscv32", "csdvn02dsffv48nvm"]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
####################################
|
||||
## SYNC TIME
|
||||
####################################
|
||||
|
||||
{
|
||||
"cmd": "sync_time",
|
||||
"contents": {
|
||||
"name": "esperinos",
|
||||
"id": "10",
|
||||
"duration": 5000,
|
||||
"infinite": true,
|
||||
"interval": 1000,
|
||||
"speed": 200,
|
||||
"loop_dur": 2000
|
||||
"timestamp": 16546416
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
LIST MELODIES
|
||||
{
|
||||
"status": "OK",
|
||||
"type": "time_set_response",
|
||||
"payload": "Time updated successfully"
|
||||
}
|
||||
|
||||
{
|
||||
"cmd": "list_melodies",
|
||||
"status": "ERROR",
|
||||
"type": "time_set_response",
|
||||
"payload": "Missing timestamp parameter"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
####################################
|
||||
## REPORT_TIME
|
||||
####################################
|
||||
|
||||
{
|
||||
"cmd": "report_time"
|
||||
}
|
||||
|
||||
{
|
||||
"status": "OK",
|
||||
"type": "time_response",
|
||||
"timestamp": 2438543431, (UNIX TIME)
|
||||
"datetime": "2025-05-18T16:54:12"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
####################################
|
||||
## PING
|
||||
####################################
|
||||
|
||||
{
|
||||
"cmd": "ping"
|
||||
}
|
||||
|
||||
{
|
||||
"status": "OK",
|
||||
"type": "pong",
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
####################################
|
||||
## REPORT STATUS
|
||||
####################################
|
||||
|
||||
{
|
||||
"cmd": "report_status"
|
||||
}
|
||||
|
||||
{
|
||||
"status": "OK",
|
||||
"type": "current_status",
|
||||
"is_playing": true,
|
||||
"time_elapsed": 78850
|
||||
}
|
||||
|
||||
120
vesper/commands_handling.hpp
Normal file
120
vesper/commands_handling.hpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#pragma once
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
// Command Handling of incoming JSON commands.
|
||||
// Both MQTT and Websocket
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
void replyOnWebSocket(AsyncWebSocketClient *client, String list);
|
||||
void updateRelayTimings(JsonVariant doc);
|
||||
void updateBellOutputs(JsonVariant doc);
|
||||
|
||||
// Handles the incoming payload. Returns it into a "JsonDocument" format.
|
||||
JsonDocument payload2json(char * payload) {
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, payload);
|
||||
if (error) {
|
||||
Serial.print("deserializeJson() failed: ");
|
||||
Serial.println(error.c_str());
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
// Handles the JSON Commands
|
||||
void handleCommand(JsonDocument command, AsyncWebSocketClient *client = nullptr){
|
||||
|
||||
String cmd = command["cmd"];
|
||||
JsonVariant contents = command["contents"];
|
||||
|
||||
if (cmd == "playback") {
|
||||
player.command(contents, client);
|
||||
} else if (cmd == "list_melodies") {
|
||||
String list = listFilesAsJson("/melodies");
|
||||
PublishMqtt(list.c_str());
|
||||
if (client) {
|
||||
replyOnWebSocket(client, list); // Only reply via WS if client exists
|
||||
Serial.println("Replying on WebSocket");
|
||||
}
|
||||
} else if (cmd == "set_relay_timers") {
|
||||
updateRelayTimings(contents);
|
||||
} else if (cmd == "set_relay_outputs") {
|
||||
updateBellOutputs(contents);
|
||||
} else if (cmd == "download_melody") {
|
||||
addMelody(contents);
|
||||
// Prepare JSON response
|
||||
StaticJsonDocument<128> response;
|
||||
response["status"] = "OK";
|
||||
response["type"] = "Download";
|
||||
response["payload"] = nullptr; // Use null in JSON
|
||||
char jsonOut[256]; // Create Char Buffer
|
||||
serializeJson(response, jsonOut); // Serialize to Buffer
|
||||
replyOnWebSocket(client, jsonOut); // Reply on WebSocket
|
||||
} /* else if (cmd == "sync_time") {
|
||||
StaticJsonDocument<256> response;
|
||||
if (manualTimeSync(contents)){
|
||||
response["status"] = "OK";
|
||||
response["type"] = "time_set_response";
|
||||
response["payload"] = "Time updated successfully";
|
||||
}
|
||||
response["status"] = "ERROR";
|
||||
response["type"] = "time_set_response";
|
||||
response["payload"] = "Missing timestamp parameter";
|
||||
char jsonOut[256];
|
||||
serializeJson(response, jsonOut);
|
||||
replyOnWebSocket(client, jsonOut);
|
||||
} else if (cmd == "report_time") {
|
||||
StaticJsonDocument<256> response;
|
||||
DateTime now = rtc.now();
|
||||
response["status"] = "OK";
|
||||
response["type"] = "time_response";
|
||||
response["timestamp"] = now.unixtime(); // Unix timestamp (seconds since 1970)
|
||||
// Also include human-readable format for debugging
|
||||
response["datetime"] = String(now.year()) + "-" +
|
||||
String(now.month()) + "-" +
|
||||
String(now.day()) + "T" +
|
||||
String(now.hour()) + ":" +
|
||||
String(now.minute()) + ":" +
|
||||
String(now.second());
|
||||
|
||||
char jsonOut[256];
|
||||
serializeJson(response, jsonOut);
|
||||
replyOnWebSocket(client, jsonOut);
|
||||
} else if (cmd == "set_time") {
|
||||
StaticJsonDocument<256> response;
|
||||
if (contents.containsKey("timestamp")) {
|
||||
uint32_t timestamp = contents["timestamp"];
|
||||
DateTime newTime = DateTime(timestamp);
|
||||
rtc.adjust(newTime);
|
||||
response["status"] = "OK";
|
||||
response["type"] = "time_set";
|
||||
LOG_DEBUG("Time updated from app.");
|
||||
} else {
|
||||
response["status"] = "ERROR";
|
||||
response["type"] = "time_set";
|
||||
response["message"] = "Missing or Wrong timestamp parameter";
|
||||
LOG_ERROR("Set time command missing timestamp parameter");
|
||||
}
|
||||
char jsonOut[256];
|
||||
serializeJson(response, jsonOut);
|
||||
replyOnWebSocket(client, jsonOut);
|
||||
} */ else if (cmd == "ping") {
|
||||
StaticJsonDocument<128> response;
|
||||
response["status"] = "OK";
|
||||
response["type"] = "pong";
|
||||
char jsonOut[128]; // Create Char Buffer
|
||||
serializeJson(response, jsonOut); // Serialize to Buffer
|
||||
replyOnWebSocket(client, jsonOut); // Reply on WebSocket
|
||||
return;
|
||||
} else if (cmd == "report_status") {
|
||||
StaticJsonDocument<256> response;
|
||||
response["status"] = "OK";
|
||||
response["type"] = "current_status";
|
||||
response["is_playing"] = player.isPlaying;
|
||||
response["time_elapsed"] = millis() - player.startTime;
|
||||
char jsonOut[256]; // Create Char Buffer
|
||||
serializeJson(response, jsonOut); // Serialize to Buffer
|
||||
replyOnWebSocket(client, jsonOut); // Reply on WebSocket
|
||||
} else {
|
||||
LOG_WARNING("Unknown Command Received");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#define DEV_ID "PC201504180001"
|
||||
#define DEV_ID "PV202508190001"
|
||||
|
||||
// Network Config
|
||||
const char* ssid = "SmartNet"; //Not used with WiFi Manager
|
||||
|
||||
35
vesper/dataLogging.hpp
Normal file
35
vesper/dataLogging.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
extern Player player;
|
||||
|
||||
void dataLogging (void * param) {
|
||||
|
||||
// Task Setup
|
||||
uint16_t bellMaxLoad[16] = {60};
|
||||
|
||||
// Task Loop
|
||||
for (;;) {
|
||||
// Only run if player is playing OR we're still cooling
|
||||
if (player.isPlaying || coolingActive) {
|
||||
coolingActive = false; // will be re-enabled if any load > 0
|
||||
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
if (bellLoad[i] > 0) {
|
||||
bellLoad[i]--;
|
||||
coolingActive = true; // still has heat left
|
||||
}
|
||||
|
||||
// Optional: check for overload
|
||||
if (bellLoad[i] > bellMaxLoad[i]) {
|
||||
Serial.printf("⚠️ Bell %d OVERLOAD! load=%d max=%d\n",
|
||||
i, bellLoad[i], bellMaxLoad[i]);
|
||||
player.forceStop();
|
||||
// You could also block new strikes here if you want
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(1000)); // run every 1s
|
||||
}
|
||||
|
||||
}
|
||||
47
vesper/features.nfo
Normal file
47
vesper/features.nfo
Normal file
@@ -0,0 +1,47 @@
|
||||
Features:
|
||||
|
||||
- WiFi Manager Support (captive portal with hotspot)
|
||||
- MQTT Support both for Subscribing and Publishing
|
||||
- WebSocket Support both for Sending and Receiving data
|
||||
- JSON support to handle messaging (both MQTT and WS)
|
||||
- SD Card Handling and File Ops
|
||||
- Melody Class with functions to Play/Pause/Stop etc.
|
||||
- Main BellEngine that runs autonomously
|
||||
- Custom Relay Timings (saved on-board)
|
||||
- Onboard RTC support
|
||||
- Timekeeper class, with functions to track time and call schedules (deprecated. will be removed)
|
||||
- OTA Update Functionality with Versioning
|
||||
- Global logger with Mode Selection (None, Error, Warning, Info, Debug)
|
||||
- NTP Time Sync
|
||||
- UDP Listener for Auto Device Discovery
|
||||
- Ability to report (when asked) the list of Melodies on the SD
|
||||
- Ability to report the list of melodies on both MQTT and WebSocket
|
||||
- Counters and Statistics:
|
||||
- Counter for each bell (counts total times the bell ringed)
|
||||
- Counter per bell, beats/minute for reliability and thermal protection. Warranty Void scenario.
|
||||
|
||||
|
||||
ToDo Features:
|
||||
|
||||
- Add Tower Clock Control Support
|
||||
- Add reset to Factory Defaults button
|
||||
- Add manual Sync-Time (for No-Connectivity Setups)
|
||||
|
||||
- Add Bluetooth support
|
||||
- Add WiFi Direct AP Support
|
||||
- Add PCB Temperature Sensor Support
|
||||
- Move PCB DEV ID out of the Firmware and hardcode it on Device (spiffs or something)
|
||||
|
||||
- Counters and Statistics:
|
||||
- Counter per playback, to figure out which melody is the most played.
|
||||
This can be implemented on the App itself. Doesn't need to be on the Device.
|
||||
|
||||
- Add the ability to assign bells to a "Master Melody" effectively creating multiple combinations of the same Archetype
|
||||
|
||||
- Add CLEAR/FORMAT SD Card function.
|
||||
|
||||
|
||||
ToDo Fixes:
|
||||
|
||||
- When STOP Command is sent, don't print debug for Play Settings.
|
||||
- Start counting the Loop Pause, when the last loop ENDS. Not when the Loop Time ended.
|
||||
@@ -1,27 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
extern uint16_t relayDurations[16];
|
||||
extern uint16_t bellDurations[16];
|
||||
|
||||
void loadRelayTimings();
|
||||
void saveRelayTimings();
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Sets Incoming Relay Durations to RAM and then call funtion to save them to file.
|
||||
void updateRelayTimings(JsonVariant doc) {
|
||||
// Iterate through the relays in the JSON payload
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
String key = String("b") + (i + 1); // Generate "b1", "b2", ...
|
||||
if (doc.containsKey(key)) {
|
||||
relayDurations[i] = doc[key].as<uint16_t>();
|
||||
LOG_DEBUG("Relay %d duration s1et to %d ms\n", i + 1, relayDurations[i]);
|
||||
} else {
|
||||
LOG_DEBUG("Relay %d not found in JSON payload. Keeping previous duration: %d ms\n", i + 1, relayDurations[i]);
|
||||
}
|
||||
}
|
||||
saveRelayTimings();
|
||||
LOG_INFO("Updated Relay Timings.")
|
||||
}
|
||||
|
||||
|
||||
// Save file "filename" with data: "data" to the "dirPath" directory of the SD card
|
||||
void saveFileToSD(const char* dirPath, const char* filename, const char* data) {
|
||||
@@ -59,7 +45,7 @@ void saveRelayTimings() {
|
||||
// Populate the JSON object with relay durations
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
String key = String("b") + (i + 1);
|
||||
doc[key] = relayDurations[i];
|
||||
doc[key] = bellDurations[i];
|
||||
}
|
||||
|
||||
char buffer[512];
|
||||
@@ -100,14 +86,14 @@ void loadRelayTimings() {
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
String key = String("b") + (i + 1);
|
||||
if (doc.containsKey(key)) {
|
||||
relayDurations[i] = doc[key].as<uint16_t>();
|
||||
LOG_DEBUG("Loaded relay %d duration: %d ms\n", i + 1, relayDurations[i]);
|
||||
bellDurations[i] = doc[key].as<uint16_t>();
|
||||
LOG_DEBUG("Loaded relay %d duration: %d ms", i + 1, bellDurations[i]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Function to sync time with NTP server and update RTC
|
||||
/* Function to sync time with NTP server and update RTC
|
||||
void syncTimeWithNTP() {
|
||||
// Connect to Wi-Fi if not already connected
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
@@ -142,6 +128,8 @@ void syncTimeWithNTP() {
|
||||
timeInfo.tm_mday, timeInfo.tm_mon + 1, timeInfo.tm_year + 1900);
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
// Call this function with the Firebase URL and desired local filename
|
||||
bool downloadFileToSD(const String& url, const String& directory, const String& filename) {
|
||||
LOG_INFO("HTTP Starting download...");
|
||||
@@ -192,7 +180,6 @@ bool downloadFileToSD(const String& url, const String& directory, const String&
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Returns the list of melodies (the filenames) currently inside the SD Card.
|
||||
String listFilesAsJson(const char* dirPath) {
|
||||
if (!SD.begin(SD_CS)) {
|
||||
@@ -280,14 +267,14 @@ void printFileAsText(const String& path, const String& filename) {
|
||||
}
|
||||
|
||||
// Downloads a new melody from HTTP
|
||||
void addMelody(JsonVariant doc) {
|
||||
|
||||
LOG_INFO("Trying Saving...");
|
||||
const char* url = doc["url"];
|
||||
const char* filename = doc["filename"];
|
||||
downloadFileToSD(url, "/melodies", filename);
|
||||
|
||||
|
||||
bool addMelody(JsonVariant doc) {
|
||||
LOG_INFO("Trying Download of Melody...");
|
||||
const char* url = doc["download_url"];
|
||||
const char* filename = doc["melodys_uid"];
|
||||
if (downloadFileToSD(url, "/melodies", filename)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Checks the onboard SD Card for new firmware
|
||||
@@ -340,6 +327,59 @@ void checkFirmwareUpdate() {
|
||||
updateBin.close();
|
||||
}
|
||||
|
||||
// call this in setup() after WiFi is up
|
||||
void setupUdpDiscovery() {
|
||||
if (udp.listen(DISCOVERY_PORT)) {
|
||||
Serial.printf("UDP discovery listening on %u\n", DISCOVERY_PORT);
|
||||
|
||||
udp.onPacket([](AsyncUDPPacket packet) {
|
||||
// Parse request
|
||||
String msg = String((const char*)packet.data(), packet.length());
|
||||
Serial.printf("UDP from %s:%u -> %s\n",
|
||||
packet.remoteIP().toString().c_str(),
|
||||
packet.remotePort(),
|
||||
msg.c_str());
|
||||
|
||||
// Minimal: accept plain text or JSON
|
||||
bool shouldReply = false;
|
||||
|
||||
if (msg.indexOf("discover") >= 0) {
|
||||
shouldReply = true;
|
||||
} else {
|
||||
// Try JSON
|
||||
StaticJsonDocument<128> req;
|
||||
DeserializationError err = deserializeJson(req, msg);
|
||||
if (!err) {
|
||||
shouldReply = (req["op"] == "discover" && req["svc"] == "vesper");
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldReply) return;
|
||||
|
||||
// Build reply JSON
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["op"] = "discover_reply";
|
||||
doc["svc"] = "vesper";
|
||||
doc["ver"] = 1;
|
||||
|
||||
doc["name"] = "Proj. Vesper v0.5"; // your device name
|
||||
doc["id"] = DEV_ID; // stable unique ID if you have one
|
||||
doc["ip"] = WiFi.localIP().toString();
|
||||
doc["ws"] = String("ws://") + WiFi.localIP().toString() + "/ws";
|
||||
doc["port"] = 80; // your WS server port
|
||||
doc["fw"] = "1.2.3"; // firmware version
|
||||
|
||||
String out;
|
||||
serializeJson(doc, out);
|
||||
|
||||
// Reply directly to the sender’s IP/port
|
||||
udp.writeTo((const uint8_t*)out.c_str(), out.length(),
|
||||
packet.remoteIP(), packet.remotePort());
|
||||
});
|
||||
} else {
|
||||
Serial.println("Failed to start UDP discovery.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// UNSUSED FUNCTIONS.
|
||||
|
||||
392
vesper/timekeeper.cpp
Normal file
392
vesper/timekeeper.cpp
Normal file
@@ -0,0 +1,392 @@
|
||||
#include "timekeeper.hpp"
|
||||
#include "SD.h"
|
||||
|
||||
void Timekeeper::begin() {
|
||||
// Clock outputs start as unassigned (-1)
|
||||
clockRelay1 = -1;
|
||||
clockRelay2 = -1;
|
||||
clockEnabled = false;
|
||||
|
||||
Serial.println("Timekeeper initialized - clock outputs not assigned");
|
||||
|
||||
// Initialize RTC
|
||||
if (!rtc.begin()) {
|
||||
Serial.println("Couldn't find RTC");
|
||||
// Continue anyway, but log the error
|
||||
} else {
|
||||
Serial.println("RTC initialized successfully");
|
||||
|
||||
// Check if RTC lost power
|
||||
if (!rtc.isrunning()) {
|
||||
Serial.println("RTC is NOT running! Setting time...");
|
||||
// Set to compile time as fallback
|
||||
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
|
||||
}
|
||||
}
|
||||
|
||||
// Load today's events
|
||||
loadTodaysEvents();
|
||||
|
||||
// Create tasks with appropriate priorities
|
||||
// Priority 3 = Highest (clock), 2 = Medium (scheduler), 1 = Lowest (rtc)
|
||||
xTaskCreate(clockTask, "ClockTask", 2048, this, 3, &clockTaskHandle);
|
||||
xTaskCreate(schedulerTask, "SchedulerTask", 4096, this, 2, &schedulerTaskHandle);
|
||||
xTaskCreate(rtcTask, "RTCTask", 4096, this, 1, &rtcTaskHandle);
|
||||
|
||||
Serial.println("Timekeeper initialized with tasks");
|
||||
}
|
||||
|
||||
void Timekeeper::setRelayWriteFunction(void (*func)(int, int)) {
|
||||
relayWriteFunc = func;
|
||||
}
|
||||
|
||||
void Timekeeper::setClockOutputs(int relay1, int relay2) {
|
||||
// Validate relay numbers (assuming 0-5 are valid)
|
||||
if (relay1 < 0 || relay1 > 5 || relay2 < 0 || relay2 > 5) {
|
||||
Serial.println("Invalid relay numbers! Must be 0-5");
|
||||
return;
|
||||
}
|
||||
|
||||
if (relay1 == relay2) {
|
||||
Serial.println("Clock relays cannot be the same!");
|
||||
return;
|
||||
}
|
||||
|
||||
clockRelay1 = relay1;
|
||||
clockRelay2 = relay2;
|
||||
clockEnabled = true; // Enable clock functionality
|
||||
|
||||
Serial.printf("Clock outputs set to: Relay %d and Relay %d - Clock ENABLED\n", relay1, relay2);
|
||||
}
|
||||
|
||||
void Timekeeper::setTime(unsigned long timestamp) {
|
||||
if (!rtc.begin()) {
|
||||
Serial.println("RTC not available - cannot set time");
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert Unix timestamp to DateTime object
|
||||
DateTime newTime(timestamp);
|
||||
|
||||
// Set the RTC
|
||||
rtc.adjust(newTime);
|
||||
|
||||
Serial.printf("RTC time set to: %04d-%02d-%02d %02d:%02d:%02d (timestamp: %lu)\n",
|
||||
newTime.year(), newTime.month(), newTime.day(),
|
||||
newTime.hour(), newTime.minute(), newTime.second(),
|
||||
timestamp);
|
||||
|
||||
// Reload today's events since the date might have changed
|
||||
loadTodaysEvents();
|
||||
}
|
||||
|
||||
unsigned long Timekeeper::getTime() {
|
||||
if (!rtc.isrunning()) {
|
||||
Serial.println("RTC not running - cannot get time");
|
||||
return 0;
|
||||
}
|
||||
|
||||
DateTime now = rtc.now();
|
||||
unsigned long timestamp = now.unixtime();
|
||||
|
||||
Serial.printf("Current RTC time: %04d-%02d-%02d %02d:%02d:%02d (timestamp: %lu)\n",
|
||||
now.year(), now.month(), now.day(),
|
||||
now.hour(), now.minute(), now.second(),
|
||||
timestamp);
|
||||
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
void Timekeeper::loadTodaysEvents() {
|
||||
// Clear existing events
|
||||
todaysEvents.clear();
|
||||
|
||||
// Get current date/time from RTC
|
||||
DateTime now = rtc.now();
|
||||
if (!rtc.isrunning()) {
|
||||
Serial.println("RTC not running - cannot load events");
|
||||
return;
|
||||
}
|
||||
|
||||
int currentYear = now.year();
|
||||
int currentMonth = now.month();
|
||||
int currentDay = now.day();
|
||||
int currentDayOfWeek = now.dayOfTheWeek(); // 0=Sunday, 1=Monday, etc.
|
||||
|
||||
Serial.printf("Loading events for: %04d-%02d-%02d (day %d)\n",
|
||||
currentYear, currentMonth, currentDay, currentDayOfWeek);
|
||||
|
||||
// Open and parse events file
|
||||
File file = SD.open("/events/events.json");
|
||||
if (!file) {
|
||||
Serial.println("Failed to open events.json");
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument doc(8192);
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
if (error) {
|
||||
Serial.print("JSON parsing failed: ");
|
||||
Serial.println(error.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
JsonArray events = doc["events"];
|
||||
int eventsLoaded = 0;
|
||||
|
||||
for (JsonObject event : events) {
|
||||
if (!event["enabled"].as<bool>()) {
|
||||
continue; // Skip disabled events
|
||||
}
|
||||
|
||||
String type = event["type"].as<String>();
|
||||
bool shouldAdd = false;
|
||||
|
||||
if (type == "single") {
|
||||
// Check if event date matches today
|
||||
String eventDateTime = event["datetime"].as<String>();
|
||||
if (isSameDate(eventDateTime, currentYear, currentMonth, currentDay)) {
|
||||
shouldAdd = true;
|
||||
}
|
||||
}
|
||||
else if (type == "weekly") {
|
||||
// Check if today's day of week is in the event's days
|
||||
JsonArray daysOfWeek = event["days_of_week"];
|
||||
for (JsonVariant dayVar : daysOfWeek) {
|
||||
int day = dayVar.as<int>();
|
||||
if (day == currentDayOfWeek) {
|
||||
shouldAdd = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == "monthly") {
|
||||
// Check if today's date is in the event's days of month
|
||||
JsonArray daysOfMonth = event["days_of_month"];
|
||||
for (JsonVariant dayVar : daysOfMonth) {
|
||||
int day = dayVar.as<int>();
|
||||
if (day == currentDay) {
|
||||
shouldAdd = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldAdd) {
|
||||
addToTodaysSchedule(event);
|
||||
eventsLoaded++;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort events by time
|
||||
sortEventsByTime();
|
||||
|
||||
Serial.printf("Loaded %d events for today\n", eventsLoaded);
|
||||
}
|
||||
|
||||
bool Timekeeper::isSameDate(String eventDateTime, int year, int month, int day) {
|
||||
// Parse "2025-12-25T09:00:00" format
|
||||
if (eventDateTime.length() < 10) return false;
|
||||
|
||||
int eventYear = eventDateTime.substring(0, 4).toInt();
|
||||
int eventMonth = eventDateTime.substring(5, 7).toInt();
|
||||
int eventDay = eventDateTime.substring(8, 10).toInt();
|
||||
|
||||
return (eventYear == year && eventMonth == month && eventDay == day);
|
||||
}
|
||||
|
||||
void Timekeeper::addToTodaysSchedule(JsonObject event) {
|
||||
ScheduledEvent schedEvent;
|
||||
|
||||
// Extract time based on event type
|
||||
if (event["type"] == "single") {
|
||||
// Extract time from datetime: "2025-12-25T09:00:00" -> "09:00:00"
|
||||
String datetime = event["datetime"].as<String>();
|
||||
if (datetime.length() >= 19) {
|
||||
schedEvent.timeStr = datetime.substring(11, 19);
|
||||
}
|
||||
} else {
|
||||
// Weekly/Monthly events have separate time field
|
||||
schedEvent.timeStr = event["time"].as<String>();
|
||||
}
|
||||
|
||||
// Store the entire event data (create a copy)
|
||||
schedEvent.eventData = event;
|
||||
|
||||
todaysEvents.push_back(schedEvent);
|
||||
|
||||
Serial.printf("Added event '%s' at %s\n",
|
||||
event["name"].as<String>().c_str(),
|
||||
schedEvent.timeStr.c_str());
|
||||
}
|
||||
|
||||
void Timekeeper::sortEventsByTime() {
|
||||
std::sort(todaysEvents.begin(), todaysEvents.end(),
|
||||
[](const ScheduledEvent& a, const ScheduledEvent& b) {
|
||||
return a.timeStr < b.timeStr;
|
||||
});
|
||||
}
|
||||
|
||||
String Timekeeper::getCurrentTimeString() {
|
||||
DateTime now = rtc.now();
|
||||
if (!rtc.isrunning()) {
|
||||
return "00:00:00";
|
||||
}
|
||||
|
||||
char timeStr[9];
|
||||
sprintf(timeStr, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
|
||||
return String(timeStr);
|
||||
}
|
||||
|
||||
// Static task functions
|
||||
void Timekeeper::clockTask(void* parameter) {
|
||||
Timekeeper* keeper = static_cast<Timekeeper*>(parameter);
|
||||
|
||||
Serial.println("Clock task started");
|
||||
|
||||
while(1) {
|
||||
unsigned long now = millis();
|
||||
|
||||
// Check if a minute has passed (60000ms = 60 seconds)
|
||||
if (now - keeper->lastClockPulse >= 60000) {
|
||||
keeper->updateClock();
|
||||
keeper->lastClockPulse = now;
|
||||
}
|
||||
|
||||
// Check every 100ms for precision
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void Timekeeper::schedulerTask(void* parameter) {
|
||||
Timekeeper* keeper = static_cast<Timekeeper*>(parameter);
|
||||
|
||||
Serial.println("Scheduler task started");
|
||||
|
||||
while(1) {
|
||||
keeper->checkScheduledEvents();
|
||||
|
||||
// Check every second
|
||||
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void Timekeeper::rtcTask(void* parameter) {
|
||||
Timekeeper* keeper = static_cast<Timekeeper*>(parameter);
|
||||
|
||||
Serial.println("RTC task started");
|
||||
|
||||
while(1) {
|
||||
DateTime now = keeper->rtc.now();
|
||||
if (keeper->rtc.isrunning()) {
|
||||
|
||||
// Check if it's midnight - reload events for new day
|
||||
if (now.hour() == 0 && now.minute() == 0 && now.second() < 10) {
|
||||
Serial.println("Midnight detected - reloading events");
|
||||
keeper->loadTodaysEvents();
|
||||
keeper->loadNextDayEvents();
|
||||
}
|
||||
|
||||
// Check hourly for maintenance tasks
|
||||
if (now.minute() == 0 && now.second() < 10) {
|
||||
Serial.printf("Hourly check at %02d:00\n", now.hour());
|
||||
// Add any hourly maintenance here:
|
||||
// - Log status
|
||||
// - Check for schedule updates
|
||||
}
|
||||
} else {
|
||||
Serial.println("Warning: RTC not running!");
|
||||
}
|
||||
|
||||
// Check every 10 seconds
|
||||
vTaskDelay(10000 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void Timekeeper::updateClock() {
|
||||
// Check if clock is enabled and outputs are assigned
|
||||
if (!clockEnabled || clockRelay1 == -1 || clockRelay2 == -1) {
|
||||
return; // Silently skip if clock not configured
|
||||
}
|
||||
|
||||
DateTime now = rtc.now();
|
||||
if (!rtc.isrunning()) {
|
||||
Serial.println("RTC not running - cannot update clock");
|
||||
return;
|
||||
}
|
||||
|
||||
if (relayWriteFunc == nullptr) {
|
||||
Serial.println("Relay write function not set - cannot update clock");
|
||||
return;
|
||||
}
|
||||
|
||||
// Alternate between the two relays
|
||||
int activeRelay = currentClockRelay ? clockRelay2 : clockRelay1;
|
||||
|
||||
Serial.printf("Clock pulse: Relay %d at %02d:%02d:%02d\n",
|
||||
activeRelay, now.hour(), now.minute(), now.second());
|
||||
|
||||
// Pulse for 5 seconds
|
||||
relayWriteFunc(activeRelay, LOW); // Assuming LOW activates relay
|
||||
vTaskDelay(5000 / portTICK_PERIOD_MS);
|
||||
relayWriteFunc(activeRelay, HIGH); // HIGH deactivates relay
|
||||
|
||||
// Switch to other relay for next minute
|
||||
currentClockRelay = !currentClockRelay;
|
||||
}
|
||||
|
||||
void Timekeeper::checkScheduledEvents() {
|
||||
String currentTime = getCurrentTimeString();
|
||||
|
||||
// Only check the seconds part for exact matching
|
||||
String currentTimeMinute = currentTime.substring(0, 5); // "HH:MM"
|
||||
|
||||
for (auto& event : todaysEvents) {
|
||||
String eventTimeMinute = event.timeStr.substring(0, 5); // "HH:MM"
|
||||
|
||||
if (eventTimeMinute == currentTimeMinute) {
|
||||
// Check if we haven't already triggered this event
|
||||
if (!event.triggered) {
|
||||
triggerEvent(event);
|
||||
event.triggered = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset trigger flag when we're past the minute
|
||||
if (eventTimeMinute < currentTimeMinute) {
|
||||
event.triggered = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Timekeeper::triggerEvent(ScheduledEvent& event) {
|
||||
JsonObject eventData = event.eventData;
|
||||
|
||||
Serial.printf("TRIGGERING EVENT: %s at %s\n",
|
||||
eventData["name"].as<String>().c_str(),
|
||||
event.timeStr.c_str());
|
||||
|
||||
// Here you would trigger your melody playback
|
||||
// You might want to call a function from your main program
|
||||
// or send a message to another task
|
||||
|
||||
// Example of what you might do:
|
||||
JsonObject melody = eventData["melody"];
|
||||
String melodyUID = melody["uid"].as<String>();
|
||||
String melodyName = melody["name"].as<String>();
|
||||
|
||||
Serial.printf("Playing melody: %s (UID: %s)\n",
|
||||
melodyName.c_str(), melodyUID.c_str());
|
||||
|
||||
// TODO: Add your melody trigger code here
|
||||
// playMelody(melody);
|
||||
}
|
||||
|
||||
void Timekeeper::loadNextDayEvents() {
|
||||
// This function would load tomorrow's events for smooth midnight transition
|
||||
// Implementation similar to loadTodaysEvents() but for tomorrow's date
|
||||
Serial.println("Pre-loading tomorrow's events...");
|
||||
// TODO: Implement if needed for smoother transitions
|
||||
}
|
||||
78
vesper/timekeeper.hpp
Normal file
78
vesper/timekeeper.hpp
Normal file
@@ -0,0 +1,78 @@
|
||||
#ifndef TIMEKEEPER_HPP
|
||||
#define TIMEKEEPER_HPP
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <ArduinoJson.h>
|
||||
#include <RTClib.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
// Structure to hold scheduled events
|
||||
struct ScheduledEvent {
|
||||
String timeStr; // Time in "HH:MM:SS" format
|
||||
JsonObject eventData; // Complete event data from JSON
|
||||
bool triggered = false; // Flag to prevent multiple triggers
|
||||
};
|
||||
|
||||
class Timekeeper {
|
||||
private:
|
||||
// RTC object
|
||||
RTC_DS1307 rtc;
|
||||
|
||||
// Event storage
|
||||
std::vector<ScheduledEvent> todaysEvents;
|
||||
std::vector<ScheduledEvent> tomorrowsEvents;
|
||||
|
||||
// Clock management (using PCF8574 relays)
|
||||
int clockRelay1 = -1, clockRelay2 = -1; // -1 means not assigned
|
||||
unsigned long lastClockPulse = 0;
|
||||
bool currentClockRelay = false; // false = relay1, true = relay2
|
||||
bool clockEnabled = false; // Only enable when outputs are set
|
||||
|
||||
// Function pointer for relay control
|
||||
void (*relayWriteFunc)(int relay, int state) = nullptr;
|
||||
|
||||
// Task handles
|
||||
TaskHandle_t rtcTaskHandle = NULL;
|
||||
TaskHandle_t clockTaskHandle = NULL;
|
||||
TaskHandle_t schedulerTaskHandle = NULL;
|
||||
|
||||
public:
|
||||
// Main initialization (no clock outputs initially)
|
||||
void begin();
|
||||
|
||||
// Set the relay control function
|
||||
void setRelayWriteFunction(void (*func)(int, int));
|
||||
|
||||
// Set/change clock relay outputs after initialization
|
||||
void setClockOutputs(int relay1, int relay2);
|
||||
|
||||
// Time management functions
|
||||
void setTime(unsigned long timestamp); // Set RTC time from Unix timestamp
|
||||
unsigned long getTime(); // Get current time as Unix timestamp
|
||||
|
||||
// Event management
|
||||
void loadTodaysEvents();
|
||||
void loadNextDayEvents();
|
||||
|
||||
// Static task functions (required by FreeRTOS)
|
||||
static void rtcTask(void* parameter);
|
||||
static void clockTask(void* parameter);
|
||||
static void schedulerTask(void* parameter);
|
||||
|
||||
private:
|
||||
// Helper functions
|
||||
bool isSameDate(String eventDateTime, int year, int month, int day);
|
||||
void addToTodaysSchedule(JsonObject event);
|
||||
void sortEventsByTime();
|
||||
String getCurrentTimeString();
|
||||
|
||||
// Core functionality
|
||||
void updateClock();
|
||||
void checkScheduledEvents();
|
||||
void triggerEvent(ScheduledEvent& event);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,43 +1,4 @@
|
||||
/*
|
||||
|
||||
Done Features:
|
||||
|
||||
- Initiate General Structure
|
||||
- Add WiFi Support
|
||||
- Add MQTT Support both for Subscribing and Publishing
|
||||
- Add JSON support to handle MQTT messaging
|
||||
- Add File Handling
|
||||
- Add Melody Class with functions to Play/Pause/Stop etc.
|
||||
- Add Main BellEngine
|
||||
- Add custom Relay Timings (saved on-board)
|
||||
- Add RTC support
|
||||
- Add Timekeeper class, with functions to track time and call schedules
|
||||
- Add OTA Update Functionality
|
||||
- Add global logger with Mode Selection (None, Error, Warning, Info, Debug)
|
||||
- Add Captive Portal / WiFi HotSpot
|
||||
- Add NTP Time Sync
|
||||
|
||||
ToDo Features:
|
||||
|
||||
- Add reset to Factory Defaults button
|
||||
- Add manual Sync-Time (for No-Connectivity Setups)
|
||||
- Add the ability to report the list of melodies
|
||||
- Add the ability to report a month's ScheduleEntry
|
||||
- Add the ability to report the free space in SPIFFS.
|
||||
- Add Bluetooth support
|
||||
- Add WiFi Direct AP Support
|
||||
- Add PCB Temperature Sensor
|
||||
- Counters and Statistics:
|
||||
- Counter for each bell (counts total times the bell ringed)
|
||||
- Counter per bell, beats/minute for reliability and thermal protection. Warranty Void scenario.
|
||||
- Counter per playback, to figure out which melody is the most played.
|
||||
- Counter of items per Scheduler
|
||||
|
||||
|
||||
*/
|
||||
|
||||
#include "logging.hpp"
|
||||
|
||||
#include <SD.h>
|
||||
#include <FS.h>
|
||||
#include <ETH.h>
|
||||
@@ -51,37 +12,54 @@ ToDo Features:
|
||||
#include <string>
|
||||
#include <Wire.h>
|
||||
#include <Adafruit_PCF8574.h>
|
||||
#include "RTClib.h"
|
||||
#include <WebServer.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <WiFiManager.h>
|
||||
#include <AsyncUDP.h>
|
||||
#include <RTClib.h>
|
||||
|
||||
// Custom Classes
|
||||
|
||||
#include "timekeeper.hpp"
|
||||
|
||||
// Hardware Constructors:
|
||||
Adafruit_PCF8574 relays;
|
||||
RTC_DS1307 rtc;
|
||||
// Wrapper function to connect timekeeper to your relays
|
||||
void relayWrite(int relayIndex, int state) {
|
||||
relays.digitalWrite(relayIndex, state);
|
||||
}
|
||||
|
||||
// SD Card Chip Select:
|
||||
#define SD_CS 5
|
||||
|
||||
// Include Classes
|
||||
#include "classes.hpp"
|
||||
#include "class_player.hpp"
|
||||
|
||||
// Class Constructors
|
||||
Timekeeper timekeeper;
|
||||
AsyncMqttClient mqttClient;
|
||||
Player player;
|
||||
std::vector<uint16_t> melody_steps; // holds the steps of the melody. Should move into bell Engine.
|
||||
AsyncWebServer server(80);
|
||||
|
||||
AsyncWebSocket ws("/ws");
|
||||
AsyncWebSocketClient* activeClient = nullptr;
|
||||
AsyncUDP udp;
|
||||
constexpr uint16_t DISCOVERY_PORT = 32101;
|
||||
uint32_t strikeCounters[16] = {0};
|
||||
uint16_t bellLoad[16] = {0};
|
||||
bool coolingActive = false;
|
||||
|
||||
|
||||
#include "config.h"
|
||||
#include "ota.hpp"
|
||||
#include "functions.hpp"
|
||||
#include "MQTT_Message_Handling.hpp"
|
||||
#include "MQTT_WiFi_Utilities.hpp"
|
||||
#include "commands_handling.hpp"
|
||||
#include "MQTT_Functions.hpp"
|
||||
#include "MQTT_Connection_Handling.hpp"
|
||||
#include "WebSocket_Functions.hpp"
|
||||
#include "PlaybackControls.hpp"
|
||||
#include "bellEngine.hpp"
|
||||
#include "dataLogging.hpp"
|
||||
|
||||
TaskHandle_t bellEngineHandle = NULL;
|
||||
TimerHandle_t schedulerTimer;
|
||||
@@ -111,11 +89,12 @@ void setup()
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// Initialize RTC
|
||||
if (!rtc.begin()) {
|
||||
LOG_ERROR("Couldn't find RTC");
|
||||
while(true) delay(10);
|
||||
}
|
||||
// Initialize timekeeper with NO clock outputs
|
||||
timekeeper.begin(); // No parameters needed
|
||||
// Connect the timekeeper to your relay controller
|
||||
timekeeper.setRelayWriteFunction(relayWrite);
|
||||
timekeeper.setClockOutputs(5, 4);
|
||||
|
||||
|
||||
// Initialize Networking and MQTT
|
||||
Network.onEvent(NetworkEvent);
|
||||
@@ -135,7 +114,7 @@ void setup()
|
||||
delay(100);
|
||||
|
||||
checkForUpdates(); // checks for updates online
|
||||
syncTimeWithNTP(); // syncs time from NTP Server
|
||||
setupUdpDiscovery();
|
||||
|
||||
delay(100);
|
||||
|
||||
@@ -149,6 +128,7 @@ void setup()
|
||||
xTaskCreatePinnedToCore(bellEngine,"bellEngine", 8192, NULL, 1, &bellEngineHandle, 1);
|
||||
xTaskCreatePinnedToCore(durationTimer, "durationTimer", 8192, NULL, 2, NULL, 1);
|
||||
xTaskCreatePinnedToCore(relayControlTask, "Relay Control", 2048, NULL, 2, NULL, 1);
|
||||
//xTaskCreatePinnedToCore(dataLogging, "dataLogging", 2048, NULL, 2, NULL, 1);
|
||||
|
||||
loadRelayTimings();
|
||||
}
|
||||
@@ -156,5 +136,5 @@ void setup()
|
||||
|
||||
void loop()
|
||||
{
|
||||
|
||||
//Serial.printf("b1:%d - b2:%d - Bell 1 Load: %d \n",strikeCounters[0], strikeCounters[1], bellLoad[0]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user