Websocket Fix, Added Clock NTP Sync, Updated MQTT IP
This commit is contained in:
@@ -106,52 +106,82 @@ void WebSocketServer::onConnect(AsyncWebSocketClient* client) {
|
|||||||
void WebSocketServer::onDisconnect(AsyncWebSocketClient* client) {
|
void WebSocketServer::onDisconnect(AsyncWebSocketClient* client) {
|
||||||
LOG_INFO("WebSocket client #%u disconnected", client->id());
|
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.removeClient(client->id());
|
||||||
_clientManager.cleanupDisconnectedClients();
|
_clientManager.cleanupDisconnectedClients();
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketServer::onData(AsyncWebSocketClient* client, void* arg, uint8_t* data, size_t len) {
|
void WebSocketServer::onData(AsyncWebSocketClient* client, void* arg, uint8_t* data, size_t len) {
|
||||||
AwsFrameInfo* info = (AwsFrameInfo*)arg;
|
AwsFrameInfo* info = (AwsFrameInfo*)arg;
|
||||||
|
uint32_t clientId = client->id();
|
||||||
|
|
||||||
// Only handle complete, single-frame text messages
|
// Use message_opcode to get the actual message type (not the frame opcode)
|
||||||
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
|
uint8_t messageOpcode = info->message_opcode;
|
||||||
// Allocate buffer for payload
|
|
||||||
char* payload = (char*)malloc(len + 1);
|
// Only handle TEXT and BINARY messages
|
||||||
if (!payload) {
|
if (messageOpcode != WS_TEXT && messageOpcode != WS_BINARY) {
|
||||||
LOG_ERROR("Failed to allocate memory for WebSocket payload");
|
LOG_DEBUG("WebSocket client #%u: ignoring non-text/binary message (opcode: %u)", clientId, messageOpcode);
|
||||||
String errorResponse = ResponseBuilder::error("memory_error", "Out of memory");
|
return;
|
||||||
_clientManager.sendToClient(client->id(), errorResponse);
|
}
|
||||||
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);
|
buffer.opcode = messageOpcode;
|
||||||
payload[len] = '\0';
|
buffer.lastUpdate = millis();
|
||||||
|
|
||||||
LOG_DEBUG("WebSocket client #%u sent: %s", client->id(), payload);
|
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
|
// Parse JSON
|
||||||
StaticJsonDocument<2048> doc;
|
StaticJsonDocument<2048> doc;
|
||||||
DeserializationError error = deserializeJson(doc, payload);
|
DeserializationError error = deserializeJson(doc, completeMessage);
|
||||||
|
|
||||||
if (error) {
|
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");
|
String errorResponse = ResponseBuilder::error("parse_error", "Invalid JSON");
|
||||||
_clientManager.sendToClient(client->id(), errorResponse);
|
_clientManager.sendToClient(clientId, errorResponse);
|
||||||
} else {
|
} else {
|
||||||
// Update client last seen time
|
// Update client last seen time
|
||||||
_clientManager.updateClientLastSeen(client->id());
|
_clientManager.updateClientLastSeen(clientId);
|
||||||
|
|
||||||
// Call user callback if set
|
// Call user callback if set
|
||||||
if (_messageCallback) {
|
if (_messageCallback) {
|
||||||
LOG_DEBUG("Routing message from client #%u to callback handler", client->id());
|
LOG_DEBUG("Routing message from client #%u to callback handler", clientId);
|
||||||
_messageCallback(client->id(), doc);
|
_messageCallback(clientId, doc);
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("WebSocket message received but no callback handler is set!");
|
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <ESPAsyncWebServer.h>
|
#include <ESPAsyncWebServer.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
|
#include <map>
|
||||||
#include "../../ClientManager/ClientManager.hpp"
|
#include "../../ClientManager/ClientManager.hpp"
|
||||||
|
|
||||||
class WebSocketServer {
|
class WebSocketServer {
|
||||||
@@ -77,6 +78,14 @@ private:
|
|||||||
ClientManager& _clientManager;
|
ClientManager& _clientManager;
|
||||||
MessageCallback _messageCallback;
|
MessageCallback _messageCallback;
|
||||||
|
|
||||||
|
// Fragment reassembly buffer (stores incomplete messages per client)
|
||||||
|
struct FragmentBuffer {
|
||||||
|
String data;
|
||||||
|
uint8_t opcode;
|
||||||
|
unsigned long lastUpdate;
|
||||||
|
};
|
||||||
|
std::map<uint32_t, FragmentBuffer> _fragmentBuffers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Static WebSocket event handler
|
* @brief Static WebSocket event handler
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ bool ConfigManager::begin() {
|
|||||||
saveNetworkConfig();
|
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
|
// Load bell durations, save defaults if not found
|
||||||
if (!loadBellDurations()) {
|
if (!loadBellDurations()) {
|
||||||
LOG_WARNING("ConfigManager - ⚠️ Creating default bell durations file");
|
LOG_WARNING("ConfigManager - ⚠️ Creating default bell durations file");
|
||||||
@@ -596,6 +602,7 @@ bool ConfigManager::loadClockConfig() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (doc.containsKey("enabled")) clockConfig.enabled = doc["enabled"].as<bool>();
|
||||||
if (doc.containsKey("c1output")) clockConfig.c1output = doc["c1output"].as<uint8_t>();
|
if (doc.containsKey("c1output")) clockConfig.c1output = doc["c1output"].as<uint8_t>();
|
||||||
if (doc.containsKey("c2output")) clockConfig.c2output = doc["c2output"].as<uint8_t>();
|
if (doc.containsKey("c2output")) clockConfig.c2output = doc["c2output"].as<uint8_t>();
|
||||||
if (doc.containsKey("pulseDuration")) clockConfig.pulseDuration = doc["pulseDuration"].as<uint16_t>();
|
if (doc.containsKey("pulseDuration")) clockConfig.pulseDuration = doc["pulseDuration"].as<uint16_t>();
|
||||||
@@ -609,6 +616,7 @@ bool ConfigManager::saveClockConfig() {
|
|||||||
if (!ensureSDCard()) return false;
|
if (!ensureSDCard()) return false;
|
||||||
|
|
||||||
StaticJsonDocument<512> doc;
|
StaticJsonDocument<512> doc;
|
||||||
|
doc["enabled"] = clockConfig.enabled;
|
||||||
doc["c1output"] = clockConfig.c1output;
|
doc["c1output"] = clockConfig.c1output;
|
||||||
doc["c2output"] = clockConfig.c2output;
|
doc["c2output"] = clockConfig.c2output;
|
||||||
doc["pulseDuration"] = clockConfig.pulseDuration;
|
doc["pulseDuration"] = clockConfig.pulseDuration;
|
||||||
@@ -727,7 +735,7 @@ std::vector<String> ConfigManager::getUpdateServers() const {
|
|||||||
void ConfigManager::updateTimeConfig(long gmtOffsetSec, int daylightOffsetSec) {
|
void ConfigManager::updateTimeConfig(long gmtOffsetSec, int daylightOffsetSec) {
|
||||||
timeConfig.gmtOffsetSec = gmtOffsetSec;
|
timeConfig.gmtOffsetSec = gmtOffsetSec;
|
||||||
timeConfig.daylightOffsetSec = daylightOffsetSec;
|
timeConfig.daylightOffsetSec = daylightOffsetSec;
|
||||||
saveToSD();
|
saveTimeConfig(); // Save time config specifically
|
||||||
LOG_DEBUG("ConfigManager - TimeConfig updated - GMT offset %ld sec, DST offset %d sec",
|
LOG_DEBUG("ConfigManager - TimeConfig updated - GMT offset %ld sec, DST offset %d sec",
|
||||||
gmtOffsetSec, daylightOffsetSec);
|
gmtOffsetSec, daylightOffsetSec);
|
||||||
}
|
}
|
||||||
@@ -853,6 +861,79 @@ bool ConfigManager::saveNetworkConfig() {
|
|||||||
return true;
|
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<String>();
|
||||||
|
if (!ntpServer.isEmpty()) {
|
||||||
|
timeConfig.ntpServer = ntpServer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load GMT offset
|
||||||
|
if (doc.containsKey("gmtOffsetSec")) {
|
||||||
|
timeConfig.gmtOffsetSec = doc["gmtOffsetSec"].as<long>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load daylight saving offset
|
||||||
|
if (doc.containsKey("daylightOffsetSec")) {
|
||||||
|
timeConfig.daylightOffsetSec = doc["daylightOffsetSec"].as<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// SETTINGS RESET IMPLEMENTATION
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -878,6 +959,7 @@ bool ConfigManager::resetAllToDefaults() {
|
|||||||
const char* settingsFiles[] = {
|
const char* settingsFiles[] = {
|
||||||
"/settings/deviceConfig.json",
|
"/settings/deviceConfig.json",
|
||||||
"/settings/networkConfig.json",
|
"/settings/networkConfig.json",
|
||||||
|
"/settings/timeConfig.json",
|
||||||
"/settings/relayTimings.json",
|
"/settings/relayTimings.json",
|
||||||
"/settings/bellOutputs.json",
|
"/settings/bellOutputs.json",
|
||||||
"/settings/clockConfig.json",
|
"/settings/clockConfig.json",
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ public:
|
|||||||
* Username defaults to deviceUID for unique identification.
|
* Username defaults to deviceUID for unique identification.
|
||||||
*/
|
*/
|
||||||
struct MqttConfig {
|
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)
|
int port = 1883; // 🔌 Standard MQTT port (non-SSL)
|
||||||
String user; // 👤 Auto-set to deviceUID
|
String user; // 👤 Auto-set to deviceUID
|
||||||
String password = "vesper"; // 🔑 Default password
|
String password = "vesper"; // 🔑 Default password
|
||||||
@@ -119,7 +119,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
struct TimeConfig {
|
struct TimeConfig {
|
||||||
String ntpServer = "pool.ntp.org"; // ⏰ Universal NTP - OK as is
|
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
|
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.
|
* All clock settings are loaded from SD card at startup.
|
||||||
*/
|
*/
|
||||||
struct GeneralConfig {
|
struct GeneralConfig {
|
||||||
uint8_t serialLogLevel = 0;
|
uint8_t serialLogLevel = 5;
|
||||||
uint8_t sdLogLevel = 0;
|
uint8_t sdLogLevel = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -320,6 +320,10 @@ public:
|
|||||||
bool loadNetworkConfig();
|
bool loadNetworkConfig();
|
||||||
bool saveNetworkConfig();
|
bool saveNetworkConfig();
|
||||||
|
|
||||||
|
// Time configuration persistence
|
||||||
|
bool loadTimeConfig();
|
||||||
|
bool saveTimeConfig();
|
||||||
|
|
||||||
// Bell and clock configuration methods (unchanged)
|
// Bell and clock configuration methods (unchanged)
|
||||||
bool loadBellDurations();
|
bool loadBellDurations();
|
||||||
bool saveBellDurations();
|
bool saveBellDurations();
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ bool FileManager::ensureDirectoryExists(const String& dirPath) {
|
|||||||
normalizedPath += "/";
|
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
|
// Create directory if it doesn't exist
|
||||||
return SD.mkdir(normalizedPath.c_str());
|
return SD.mkdir(normalizedPath.c_str());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,6 +344,9 @@ void setup()
|
|||||||
networking.setNetworkCallbacks(
|
networking.setNetworkCallbacks(
|
||||||
[]() {
|
[]() {
|
||||||
communication.onNetworkConnected();
|
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
|
// Start AsyncWebServer when network becomes available
|
||||||
if (networking.getState() != NetworkState::WIFI_PORTAL_MODE) {
|
if (networking.getState() != NetworkState::WIFI_PORTAL_MODE) {
|
||||||
LOG_INFO("🚀 Starting AsyncWebServer on port 80...");
|
LOG_INFO("🚀 Starting AsyncWebServer on port 80...");
|
||||||
@@ -359,6 +362,10 @@ void setup()
|
|||||||
LOG_INFO("Network already connected - triggering MQTT connection");
|
LOG_INFO("Network already connected - triggering MQTT connection");
|
||||||
communication.onNetworkConnected();
|
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
|
// 🔥 CRITICAL: Start AsyncWebServer ONLY when network is ready
|
||||||
// Do NOT start if WiFiManager portal is active (port 80 conflict!)
|
// Do NOT start if WiFiManager portal is active (port 80 conflict!)
|
||||||
LOG_INFO("🚀 Starting AsyncWebServer on port 80...");
|
LOG_INFO("🚀 Starting AsyncWebServer on port 80...");
|
||||||
|
|||||||
Reference in New Issue
Block a user