#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(); 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(); // password = doc["password"].as(); // mqttHost.fromString(doc["mqttHost"].as()); // mqttUser = doc["mqttUser"].as(); // mqttPassword = doc["mqttPassword"].as(); // configFile.close(); // Serial.println("Settings loaded from SPIFFS."); // } // // Generate HTML page for configuration // String generateConfigPageHTML() { // String page = R"rawliteral( // // // //

Device Configuration

//
// WiFi SSID:
// WiFi Password:
// MQTT Host:
// MQTT Username:
// MQTT Password:
// //
// // // )rawliteral"; // return page; // }