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
488 lines
13 KiB
C++
488 lines
13 KiB
C++
#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 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.
|
||
|
||
// 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;
|
||
// }
|