Files
project-vesper/vesper/functions.hpp
bonamin 101f9e7135 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
2025-09-05 19:27:13 +03:00

488 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
extern uint16_t bellDurations[16];
void loadRelayTimings();
void saveRelayTimings();
// - - - - - - - - - - - - - - - - - - - - - - - - - - -
// 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) {
// Initialize SD (if not already done)
if (!SD.begin(SD_CS)) {
LOG_ERROR("SD Card not initialized!");
return;
}
// Make sure directory exists
if (!SD.exists(dirPath)) {
SD.mkdir(dirPath);
}
// Build full path
String fullPath = String(dirPath);
if (!fullPath.endsWith("/")) fullPath += "/";
fullPath += filename;
File file = SD.open(fullPath.c_str(), FILE_WRITE);
if (!file) {
LOG_ERROR("Failed to open file: %s", fullPath.c_str());
return;
}
file.print(data);
file.close();
LOG_INFO("File %s saved successfully.\n", fullPath.c_str());
}
// Saves Relay Durations from RAM, into a file
void saveRelayTimings() {
StaticJsonDocument<512> doc; // Adjust size if needed
// Populate the JSON object with relay durations
for (uint8_t i = 0; i < 16; i++) {
String key = String("b") + (i + 1);
doc[key] = bellDurations[i];
}
char buffer[512];
size_t len = serializeJson(doc, buffer, sizeof(buffer));
if (len == 0) {
LOG_ERROR("Failed to serialize JSON.");
return;
}
const char * path = "/settings";
const char * filename = "relayTimings.json";
saveFileToSD(path, filename, buffer);
}
// Loads Relay Durations from file into RAM (called during boot)
void loadRelayTimings() {
if (!SD.begin(SD_CS)) {
LOG_ERROR("SD Card not initialized. Using default relay timings.");
return;
}
File file = SD.open("/settings/relayTimings.json", FILE_READ);
if (!file) {
LOG_ERROR("Settings file not found on SD. Using default relay timings.");
return;
}
// Parse the JSON file
StaticJsonDocument<512> doc; // Adjust size if needed
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error) {
LOG_ERROR("Failed to parse settings from SD. Using default relay timings.");
return;
}
// Populate relayDurations array
for (uint8_t i = 0; i < 16; i++) {
String key = String("b") + (i + 1);
if (doc.containsKey(key)) {
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
void syncTimeWithNTP() {
// Connect to Wi-Fi if not already connected
if (WiFi.status() != WL_CONNECTED) {
LOG_DEBUG("Connecting to Wi-Fi...");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
LOG_DEBUG(".");
}
LOG_DEBUG("\nWi-Fi connected!");
}
// Configure NTP
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
// Sync time from NTP server
LOG_DEBUG("Syncing time with NTP server...");
struct tm timeInfo;
if (!getLocalTime(&timeInfo)) {
LOG_DEBUG("Failed to obtain time from NTP server!");
return;
}
// Update RTC with synchronized time
rtc.adjust(DateTime(timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec));
// Log synchronized time
LOG_INFO("Time synced with NTP server.");
LOG_DEBUG("Synced time: %02d:%02d:%02d, %02d/%02d/%04d",
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec,
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...");
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
LOG_ERROR("(HTTP) GET failed, error: %s\n", http.errorToString(httpCode).c_str());
http.end();
return false;
}
if (!SD.begin(SD_CS)) {
LOG_ERROR("SD Card init failed!");
http.end();
return false;
}
// Ensure the directory ends with '/'
String dirPath = directory;
if (!dirPath.endsWith("/")) dirPath += "/";
// Create directory if it doesn't exist
SD.mkdir(dirPath.c_str());
String fullPath = dirPath + filename;
File file = SD.open(fullPath.c_str(), FILE_WRITE);
if (!file) {
LOG_ERROR("SD Failed to open file for writing: %s", fullPath.c_str());
http.end();
return false;
}
WiFiClient* stream = http.getStreamPtr();
uint8_t buffer[1024];
int bytesRead;
while (http.connected() && (bytesRead = stream->readBytes(buffer, sizeof(buffer))) > 0) {
file.write(buffer, bytesRead);
}
file.close();
http.end();
LOG_INFO("HTTP Download complete, file saved to: %s", fullPath.c_str());
return true;
}
// Returns the list of melodies (the filenames) currently inside the SD Card.
String listFilesAsJson(const char* dirPath) {
if (!SD.begin(SD_CS)) {
LOG_ERROR("SD init failed");
return "{}";
}
File dir = SD.open(dirPath);
if (!dir || !dir.isDirectory()) {
LOG_ERROR("Directory not found");
return "{}";
}
DynamicJsonDocument doc(1024); // Adjust size if needed
JsonArray fileList = doc.createNestedArray("files");
File file = dir.openNextFile();
while (file) {
if (!file.isDirectory()) {
fileList.add(file.name());
}
file = dir.openNextFile();
}
String json;
serializeJson(doc, json);
return json;
}
// Prints the Steps of a Melody from a file using its filename
void printMelodyFile(const String& filename) {
if (!SD.begin(SD_CS)) {
LOG_ERROR("SD Card not initialized.");
return;
}
File file = SD.open("/melodies/" + filename, FILE_READ);
if (!file) {
Serial.println("Failed to open Melody file for reading");
return;
}
Serial.printf("---- Contents of %s ----\n", filename.c_str());
uint16_t step;
int index = 0;
while (file.available() >= 2) {
uint8_t low = file.read();
uint8_t high = file.read();
step = (high << 8) | low;
Serial.printf("Step %5d: 0x%04X (%d)\n", index++, step, step);
}
file.close();
Serial.println("---- End of File ----");
}
// Prints the contents of a Text file using its filename
void printFileAsText(const String& path, const String& filename) {
if (!SD.begin(SD_CS)) {
LOG_ERROR("SD Card not initialized.");
return;
}
String fullPath = path;
if (!fullPath.endsWith("/")) fullPath += "/";
fullPath += filename;
File file = SD.open(fullPath, FILE_READ);
if (!file) {
Serial.println("Failed to open file for reading");
return;
}
Serial.printf("---- Contents of %s ----\n", filename.c_str());
while (file.available()) {
String line = file.readStringUntil('\n');
Serial.println(line);
}
file.close();
Serial.println("---- End of File ----");
}
// Downloads a new melody from HTTP
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
void checkFirmwareUpdate() {
if (!SD.begin(SD_CS)) {
Serial.println("SD init failed");
return;
}
File updateBin = SD.open("/firmware/update.bin");
if (!updateBin) {
Serial.println("No update.bin found");
return;
}
size_t updateSize = updateBin.size();
if (updateSize == 0) {
Serial.println("Empty update file");
updateBin.close();
return;
}
Serial.println("Starting firmware update...");
if (Update.begin(updateSize)) {
size_t written = Update.writeStream(updateBin);
if (written == updateSize) {
Serial.println("Update written successfully");
} else {
Serial.printf("Written only %d/%d bytes\n", written, updateSize);
}
if (Update.end()) {
Serial.println("Update finished!");
if (Update.isFinished()) {
Serial.println("Update complete. Rebooting...");
updateBin.close();
SD.remove("/firmware/update.bin"); // optional cleanup
ESP.restart();
} else {
Serial.println("Update not complete");
}
} else {
Serial.printf("Update error: %s\n", Update.errorString());
}
} else {
Serial.println("Not enough space to begin update");
}
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 senders 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.
// void startConfigPortal() {
// WiFi.mode(WIFI_AP);
// WiFi.softAP("Device_Config", "12345678");
// Serial.println("AP mode started. Connect to 'Device_Config'.");
// // Serve the configuration page
// server.on("/", HTTP_GET, []() {
// server.send(200, "text/html", generateConfigPageHTML());
// });
// // Handle form submission
// server.on("/save", HTTP_POST, []() {
// ssid = server.arg("ssid");
// password = server.arg("password");
// mqttHost.fromString(server.arg("mqttHost"));
// mqttUser = server.arg("mqttUser");
// mqttPassword = server.arg("mqttPassword");
// saveSettings(); // Save new settings to SPIFFS
// server.send(200, "text/plain", "Settings saved! Rebooting...");
// delay(1000);
// ESP.restart();
// });
// server.begin();
// }
// // Save settings to SPIFFS
// void saveSettings() {
// StaticJsonDocument<512> doc;
// doc["ssid"] = ssid;
// doc["password"] = password;
// doc["mqttHost"] = mqttHost.toString();
// doc["mqttUser"] = mqttUser;
// doc["mqttPassword"] = mqttPassword;
// File configFile = SPIFFS.open(CONFIG_FILE, "w");
// if (!configFile) {
// Serial.println("Failed to open config file for writing.");
// return;
// }
// serializeJson(doc, configFile);
// configFile.close();
// Serial.println("Settings saved to SPIFFS.");
// }
// // Load settings from SPIFFS
// void loadSettings() {
// if (!SPIFFS.exists(CONFIG_FILE)) {
// Serial.println("Config file not found. Using defaults.");
// return;
// }
// File configFile = SPIFFS.open(CONFIG_FILE, "r");
// if (!configFile) {
// Serial.println("Failed to open config file.");
// return;
// }
// StaticJsonDocument<512> doc;
// DeserializationError error = deserializeJson(doc, configFile);
// if (error) {
// Serial.println("Failed to parse config file.");
// return;
// }
// ssid = doc["ssid"].as<String>();
// password = doc["password"].as<String>();
// mqttHost.fromString(doc["mqttHost"].as<String>());
// mqttUser = doc["mqttUser"].as<String>();
// mqttPassword = doc["mqttPassword"].as<String>();
// configFile.close();
// Serial.println("Settings loaded from SPIFFS.");
// }
// // Generate HTML page for configuration
// String generateConfigPageHTML() {
// String page = R"rawliteral(
// <!DOCTYPE html>
// <html>
// <body>
// <h2>Device Configuration</h2>
// <form action="/save" method="POST">
// WiFi SSID: <input type="text" name="ssid" value=")rawliteral" +
// ssid + R"rawliteral("><br>
// WiFi Password: <input type="password" name="password" value=")rawliteral" +
// password + R"rawliteral("><br>
// MQTT Host: <input type="text" name="mqttHost" value=")rawliteral" +
// mqttHost.toString() + R"rawliteral("><br>
// MQTT Username: <input type="text" name="mqttUser" value=")rawliteral" +
// mqttUser + R"rawliteral("><br>
// MQTT Password: <input type="password" name="mqttPassword" value=")rawliteral" +
// mqttPassword + R"rawliteral("><br>
// <input type="submit" value="Save">
// </form>
// </body>
// </html>
// )rawliteral";
// return page;
// }