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

@@ -7,10 +7,8 @@ void saveRelayTimings();
// - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Sets Incoming Relay Durations to RAM and then call funtion to save them to file.
void updateRelayTimings(JsonDocument doc) {
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", ...
@@ -25,16 +23,33 @@ void updateRelayTimings(JsonDocument doc) {
LOG_INFO("Updated Relay Timings.")
}
// Save file "fileName" with data: "data"
void savefile(const char* fileName, const char* data) {
File file = SPIFFS.open(fileName, "w");
if (!file) {
LOG_ERROR("Failed to open file!");
// 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", fileName);
LOG_INFO("File %s saved successfully.\n", fullPath.c_str());
}
// Saves Relay Durations from RAM, into a file
@@ -54,41 +69,30 @@ void saveRelayTimings() {
return;
}
const char * fileName = "/settings/relayTimings.json";
savefile(fileName, buffer);
// // Open the file for writing
// File file = SPIFFS.open("/settings/relayTimings.json", "w");
// if (!file) {
// Serial.println("Failed to open file for writing");
// return;
// }
//
// // Serialize JSON to the file
// if (serializeJson(doc, file) == 0) {
// Serial.println("Failed to write JSON to file");
// } else {
// Serial.println("Relay timings saved successfully");
// }
//
// file.close();
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() {
// Open the file for reading
File file = SPIFFS.open("/settings/relayTimings.json", "r");
if (!file) {
LOG_ERROR("Settings file not found. Using default relay timings.");
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 file. Using default relay timings.");
file.close();
LOG_ERROR("Failed to parse settings from SD. Using default relay timings.");
return;
}
@@ -101,28 +105,343 @@ void loadRelayTimings() {
}
}
file.close();
}
//
void updateSchedule(JsonDocument doc) {
const char* fileName = doc["file"];
// Ensure fileName exists and is valid
if (!fileName || strlen(fileName) == 0) {
LOG_ERROR("Invalid JSON payload: Missing or invalid 'file' field");
return;
// 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!");
}
// Serialize the "data" object into a string
String dataString;
serializeJson(doc["data"], dataString); // Converts the "data" object to a JSON string
// Configure NTP
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
if (dataString.length() > 0) {
savefile(fileName, dataString.c_str());
LOG_INFO("File '%s' updated successfully.\n", fileName);
// 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
void addMelody(JsonVariant doc) {
LOG_INFO("Trying Saving...");
const char* url = doc["url"];
const char* filename = doc["filename"];
downloadFileToSD(url, "/melodies", filename);
}
// 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 {
LOG_DEBUG("Invalid JSON payload: Unable to serialize 'data'");
Serial.printf("Written only %d/%d bytes\n", written, updateSize);
}
timekeeper.refreshDailySchedule();
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();
}
// 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;
// }