From 7d9bc42078881d4fdee705497da12712720c066d Mon Sep 17 00:00:00 2001 From: bonamin Date: Fri, 26 Dec 2025 09:33:24 +0200 Subject: [PATCH] Websocket Fix, Added Clock NTP Sync, Updated MQTT IP --- .../WebSocketServer/WebSocketServer.cpp | 88 ++++++++----- .../WebSocketServer/WebSocketServer.hpp | 9 ++ vesper/src/ConfigManager/ConfigManager.cpp | 118 +++++++++++++++--- vesper/src/ConfigManager/ConfigManager.hpp | 14 ++- vesper/src/FileManager/FileManager.cpp | 9 +- vesper/vesper.ino | 11 +- 6 files changed, 193 insertions(+), 56 deletions(-) diff --git a/vesper/src/Communication/WebSocketServer/WebSocketServer.cpp b/vesper/src/Communication/WebSocketServer/WebSocketServer.cpp index 7fa8420..c9ddb4a 100644 --- a/vesper/src/Communication/WebSocketServer/WebSocketServer.cpp +++ b/vesper/src/Communication/WebSocketServer/WebSocketServer.cpp @@ -105,53 +105,83 @@ void WebSocketServer::onConnect(AsyncWebSocketClient* client) { void WebSocketServer::onDisconnect(AsyncWebSocketClient* client) { LOG_INFO("WebSocket client #%u disconnected", client->id()); - + + // Clean up any fragment buffer for this client + _fragmentBuffers.erase(client->id()); + _clientManager.removeClient(client->id()); _clientManager.cleanupDisconnectedClients(); } void WebSocketServer::onData(AsyncWebSocketClient* client, void* arg, uint8_t* data, size_t len) { AwsFrameInfo* info = (AwsFrameInfo*)arg; - - // Only handle complete, single-frame text messages - if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) { - // Allocate buffer for payload - char* payload = (char*)malloc(len + 1); - if (!payload) { - LOG_ERROR("Failed to allocate memory for WebSocket payload"); - String errorResponse = ResponseBuilder::error("memory_error", "Out of memory"); - _clientManager.sendToClient(client->id(), errorResponse); - return; + uint32_t clientId = client->id(); + + // Use message_opcode to get the actual message type (not the frame opcode) + uint8_t messageOpcode = info->message_opcode; + + // Only handle TEXT and BINARY messages + if (messageOpcode != WS_TEXT && messageOpcode != WS_BINARY) { + LOG_DEBUG("WebSocket client #%u: ignoring non-text/binary message (opcode: %u)", clientId, messageOpcode); + return; + } + + // NOTE: As of ESPAsyncWebServer v3.9.3, the library automatically unmasks the data + // before calling this handler. The 'data' pointer contains plain, unmasked bytes. + + // Handle fragmented messages - reassemble them + if (info->index == 0) { + // First chunk - create or reset buffer + FragmentBuffer& buffer = _fragmentBuffers[clientId]; + buffer.data = ""; + + if (info->len > 0 && info->len < 65536) { + buffer.data.reserve(info->len); } - - memcpy(payload, data, len); - payload[len] = '\0'; - - LOG_DEBUG("WebSocket client #%u sent: %s", client->id(), payload); - + + buffer.opcode = messageOpcode; + buffer.lastUpdate = millis(); + + if (info->len > len) { + LOG_DEBUG("WebSocket client #%u: started fragmented message (%llu bytes total)", clientId, info->len); + } + } + + // Get or create buffer for this client + FragmentBuffer& buffer = _fragmentBuffers[clientId]; + + // Append this chunk to the buffer (data is already unmasked by the library) + buffer.data.concat((const char*)data, len); + buffer.lastUpdate = millis(); + + // Check if message is complete + if (info->final && buffer.data.length() >= info->len) { + String completeMessage = buffer.data; + uint8_t opcode = buffer.opcode; + _fragmentBuffers.erase(clientId); + + LOG_DEBUG("WebSocket client #%u sent (%u bytes): %s", + clientId, completeMessage.length(), completeMessage.c_str()); + // Parse JSON StaticJsonDocument<2048> doc; - DeserializationError error = deserializeJson(doc, payload); - + DeserializationError error = deserializeJson(doc, completeMessage); + if (error) { - LOG_ERROR("Failed to parse WebSocket JSON from client #%u: %s", client->id(), error.c_str()); + LOG_ERROR("Failed to parse WebSocket JSON from client #%u: %s", clientId, error.c_str()); String errorResponse = ResponseBuilder::error("parse_error", "Invalid JSON"); - _clientManager.sendToClient(client->id(), errorResponse); + _clientManager.sendToClient(clientId, errorResponse); } else { // Update client last seen time - _clientManager.updateClientLastSeen(client->id()); - + _clientManager.updateClientLastSeen(clientId); + // Call user callback if set if (_messageCallback) { - LOG_DEBUG("Routing message from client #%u to callback handler", client->id()); - _messageCallback(client->id(), doc); + LOG_DEBUG("Routing message from client #%u to callback handler", clientId); + _messageCallback(clientId, doc); } else { LOG_WARNING("WebSocket message received but no callback handler is set!"); } } - - free(payload); - } else { - LOG_WARNING("Received fragmented or non-text WebSocket message from client #%u - ignoring", client->id()); } } diff --git a/vesper/src/Communication/WebSocketServer/WebSocketServer.hpp b/vesper/src/Communication/WebSocketServer/WebSocketServer.hpp index 3af8085..1b40d33 100644 --- a/vesper/src/Communication/WebSocketServer/WebSocketServer.hpp +++ b/vesper/src/Communication/WebSocketServer/WebSocketServer.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "../../ClientManager/ClientManager.hpp" class WebSocketServer { @@ -77,6 +78,14 @@ private: ClientManager& _clientManager; MessageCallback _messageCallback; + // Fragment reassembly buffer (stores incomplete messages per client) + struct FragmentBuffer { + String data; + uint8_t opcode; + unsigned long lastUpdate; + }; + std::map _fragmentBuffers; + /** * @brief Static WebSocket event handler */ diff --git a/vesper/src/ConfigManager/ConfigManager.cpp b/vesper/src/ConfigManager/ConfigManager.cpp index e22dbd1..4d53ac1 100644 --- a/vesper/src/ConfigManager/ConfigManager.cpp +++ b/vesper/src/ConfigManager/ConfigManager.cpp @@ -79,7 +79,13 @@ bool ConfigManager::begin() { LOG_WARNING("ConfigManager - ⚠️ Creating default network config file"); saveNetworkConfig(); } - + + // Load time config, save defaults if not found + if (!loadTimeConfig()) { + LOG_WARNING("ConfigManager - ⚠️ Creating default time config file (GMT+2)"); + saveTimeConfig(); + } + // Load bell durations, save defaults if not found if (!loadBellDurations()) { LOG_WARNING("ConfigManager - ⚠️ Creating default bell durations file"); @@ -580,48 +586,50 @@ void ConfigManager::updateClockSilence(JsonVariant doc) { bool ConfigManager::loadClockConfig() { if (!ensureSDCard()) return false; - + File file = SD.open("/settings/clockConfig.json", FILE_READ); if (!file) { LOG_WARNING("ConfigManager - ⚠️ Clock config file not found - using defaults"); return false; } - + StaticJsonDocument<512> doc; DeserializationError error = deserializeJson(doc, file); file.close(); - + if (error) { LOG_ERROR("ConfigManager - ❌ Failed to parse clock config from SD: %s", error.c_str()); return false; } - + + if (doc.containsKey("enabled")) clockConfig.enabled = doc["enabled"].as(); if (doc.containsKey("c1output")) clockConfig.c1output = doc["c1output"].as(); if (doc.containsKey("c2output")) clockConfig.c2output = doc["c2output"].as(); if (doc.containsKey("pulseDuration")) clockConfig.pulseDuration = doc["pulseDuration"].as(); if (doc.containsKey("pauseDuration")) clockConfig.pauseDuration = doc["pauseDuration"].as(); - + LOG_DEBUG("ConfigManager - Clock config loaded"); return true; } bool ConfigManager::saveClockConfig() { if (!ensureSDCard()) return false; - + StaticJsonDocument<512> doc; + doc["enabled"] = clockConfig.enabled; doc["c1output"] = clockConfig.c1output; doc["c2output"] = clockConfig.c2output; doc["pulseDuration"] = clockConfig.pulseDuration; doc["pauseDuration"] = clockConfig.pauseDuration; - + char buffer[512]; size_t len = serializeJson(doc, buffer, sizeof(buffer)); - + if (len == 0 || len >= sizeof(buffer)) { LOG_ERROR("ConfigManager - ❌ Failed to serialize clock config JSON"); return false; } - + saveFileToSD("/settings", "clockConfig.json", buffer); LOG_DEBUG("ConfigManager - Clock config saved"); return true; @@ -727,8 +735,8 @@ std::vector ConfigManager::getUpdateServers() const { void ConfigManager::updateTimeConfig(long gmtOffsetSec, int daylightOffsetSec) { timeConfig.gmtOffsetSec = gmtOffsetSec; timeConfig.daylightOffsetSec = daylightOffsetSec; - saveToSD(); - LOG_DEBUG("ConfigManager - TimeConfig updated - GMT offset %ld sec, DST offset %d sec", + saveTimeConfig(); // Save time config specifically + LOG_DEBUG("ConfigManager - TimeConfig updated - GMT offset %ld sec, DST offset %d sec", gmtOffsetSec, daylightOffsetSec); } @@ -826,12 +834,12 @@ bool ConfigManager::loadNetworkConfig() { bool ConfigManager::saveNetworkConfig() { if (!ensureSDCard()) return false; - + StaticJsonDocument<512> doc; - + // Save hostname (user can customize) doc["hostname"] = networkConfig.hostname; - + // Save static IP configuration doc["useStaticIP"] = networkConfig.useStaticIP; doc["ip"] = networkConfig.ip.toString(); @@ -839,20 +847,93 @@ bool ConfigManager::saveNetworkConfig() { doc["subnet"] = networkConfig.subnet.toString(); doc["dns1"] = networkConfig.dns1.toString(); doc["dns2"] = networkConfig.dns2.toString(); - + char buffer[512]; size_t len = serializeJson(doc, buffer, sizeof(buffer)); - + if (len == 0 || len >= sizeof(buffer)) { LOG_ERROR("ConfigManager - ❌ Failed to serialize network config JSON"); return false; } - + saveFileToSD("/settings", "networkConfig.json", buffer); LOG_DEBUG("ConfigManager - Network config saved to SD"); return true; } +// ════════════════════════════════════════════════════════════════════════════ +// TIME CONFIGURATION PERSISTENCE +// ════════════════════════════════════════════════════════════════════════════ + +bool ConfigManager::loadTimeConfig() { + if (!ensureSDCard()) return false; + + File file = SD.open("/settings/timeConfig.json", FILE_READ); + if (!file) { + LOG_DEBUG("ConfigManager - Time config file not found - using defaults (GMT+2)"); + return false; + } + + StaticJsonDocument<256> doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if (error) { + LOG_ERROR("ConfigManager - ❌ Failed to parse time config from SD: %s", error.c_str()); + return false; + } + + // Load NTP server if present + if (doc.containsKey("ntpServer")) { + String ntpServer = doc["ntpServer"].as(); + if (!ntpServer.isEmpty()) { + timeConfig.ntpServer = ntpServer; + } + } + + // Load GMT offset + if (doc.containsKey("gmtOffsetSec")) { + timeConfig.gmtOffsetSec = doc["gmtOffsetSec"].as(); + } + + // Load daylight saving offset + if (doc.containsKey("daylightOffsetSec")) { + timeConfig.daylightOffsetSec = doc["daylightOffsetSec"].as(); + } + + LOG_DEBUG("ConfigManager - Time config loaded - NTP: %s, GMT offset: %ld, DST offset: %d", + timeConfig.ntpServer.c_str(), + timeConfig.gmtOffsetSec, + timeConfig.daylightOffsetSec); + + return true; +} + +bool ConfigManager::saveTimeConfig() { + if (!ensureSDCard()) return false; + + StaticJsonDocument<256> doc; + + // Save NTP server + doc["ntpServer"] = timeConfig.ntpServer; + + // Save timezone offsets + doc["gmtOffsetSec"] = timeConfig.gmtOffsetSec; + doc["daylightOffsetSec"] = timeConfig.daylightOffsetSec; + + char buffer[256]; + size_t len = serializeJson(doc, buffer, sizeof(buffer)); + + if (len == 0 || len >= sizeof(buffer)) { + LOG_ERROR("ConfigManager - ❌ Failed to serialize time config JSON"); + return false; + } + + saveFileToSD("/settings", "timeConfig.json", buffer); + LOG_DEBUG("ConfigManager - Time config saved to SD"); + return true; +} + // ═══════════════════════════════════════════════════════════════════════════════ // SETTINGS RESET IMPLEMENTATION // ═══════════════════════════════════════════════════════════════════════════════ @@ -878,6 +959,7 @@ bool ConfigManager::resetAllToDefaults() { const char* settingsFiles[] = { "/settings/deviceConfig.json", "/settings/networkConfig.json", + "/settings/timeConfig.json", "/settings/relayTimings.json", "/settings/bellOutputs.json", "/settings/clockConfig.json", diff --git a/vesper/src/ConfigManager/ConfigManager.hpp b/vesper/src/ConfigManager/ConfigManager.hpp index fe5b75c..7ed4044 100644 --- a/vesper/src/ConfigManager/ConfigManager.hpp +++ b/vesper/src/ConfigManager/ConfigManager.hpp @@ -79,7 +79,7 @@ public: * Username defaults to deviceUID for unique identification. */ struct MqttConfig { - IPAddress host = IPAddress(145, 223, 96, 251); // 📡 Local Mosquitto broker + IPAddress host = IPAddress(72,61,191,197); // 📡 Local Mosquitto broker int port = 1883; // 🔌 Standard MQTT port (non-SSL) String user; // 👤 Auto-set to deviceUID String password = "vesper"; // 🔑 Default password @@ -119,7 +119,7 @@ public: */ struct TimeConfig { String ntpServer = "pool.ntp.org"; // ⏰ Universal NTP - OK as is - long gmtOffsetSec = 0; // 🌍 Default UTC, app-configurable via SD + long gmtOffsetSec = 7200; // 🌍 Default GMT+2 (Greek Time), app-configurable via SD int daylightOffsetSec = 0; // ☀️ Default no DST, app-configurable via SD }; @@ -211,7 +211,7 @@ public: * All clock settings are loaded from SD card at startup. */ struct GeneralConfig { - uint8_t serialLogLevel = 0; + uint8_t serialLogLevel = 5; uint8_t sdLogLevel = 0; }; @@ -313,12 +313,16 @@ public: // Configuration update methods for app commands void updateTimeConfig(long gmtOffsetSec, int daylightOffsetSec); - void updateNetworkConfig(const String& hostname, bool useStaticIP, IPAddress ip, IPAddress gateway, + void updateNetworkConfig(const String& hostname, bool useStaticIP, IPAddress ip, IPAddress gateway, IPAddress subnet, IPAddress dns1, IPAddress dns2); - + // Network configuration persistence bool loadNetworkConfig(); bool saveNetworkConfig(); + + // Time configuration persistence + bool loadTimeConfig(); + bool saveTimeConfig(); // Bell and clock configuration methods (unchanged) bool loadBellDurations(); diff --git a/vesper/src/FileManager/FileManager.cpp b/vesper/src/FileManager/FileManager.cpp index 66b00c2..cf241b0 100644 --- a/vesper/src/FileManager/FileManager.cpp +++ b/vesper/src/FileManager/FileManager.cpp @@ -39,13 +39,18 @@ bool FileManager::ensureDirectoryExists(const String& dirPath) { if (!initializeSD()) { return false; } - + // Ensure the directory ends with '/' String normalizedPath = dirPath; if (!normalizedPath.endsWith("/")) { normalizedPath += "/"; } - + + // Check if directory already exists + if (SD.exists(normalizedPath.c_str())) { + return true; // Directory already exists, success + } + // Create directory if it doesn't exist return SD.mkdir(normalizedPath.c_str()); } diff --git a/vesper/vesper.ino b/vesper/vesper.ino index 7925d75..248e8cf 100644 --- a/vesper/vesper.ino +++ b/vesper/vesper.ino @@ -342,8 +342,11 @@ void setup() // Set up network callbacks networking.setNetworkCallbacks( - []() { + []() { communication.onNetworkConnected(); + // Sync time with NTP server when network becomes available + LOG_INFO("⏰ Syncing time with NTP server..."); + timekeeper.syncTimeWithNTP(); // Start AsyncWebServer when network becomes available if (networking.getState() != NetworkState::WIFI_PORTAL_MODE) { LOG_INFO("🚀 Starting AsyncWebServer on port 80..."); @@ -358,7 +361,11 @@ void setup() if (networking.isConnected()) { LOG_INFO("Network already connected - triggering MQTT connection"); communication.onNetworkConnected(); - + + // Sync time with NTP server if network is already connected + LOG_INFO("⏰ Syncing time with NTP server..."); + timekeeper.syncTimeWithNTP(); + // 🔥 CRITICAL: Start AsyncWebServer ONLY when network is ready // Do NOT start if WiFiManager portal is active (port 80 conflict!) LOG_INFO("🚀 Starting AsyncWebServer on port 80...");