From 956786321a4370bc6267590fb0e238215251bb5a Mon Sep 17 00:00:00 2001 From: bonamin Date: Mon, 13 Oct 2025 17:34:54 +0300 Subject: [PATCH] Fixed MQTT and WS Routing - w/o SSL --- .gitignore | 9 +- vesper/src/BellEngine/BellEngine.cpp | 72 +- vesper/src/BellEngine/BellEngine.hpp | 15 +- .../CommandHandler.cpp} | 724 +++++------------- .../CommandHandler/CommandHandler.hpp | 141 ++++ vesper/src/Communication/Communication.hpp | 232 ------ .../CommunicationRouter.cpp | 300 ++++++++ .../CommunicationRouter.hpp | 118 +++ .../MQTTAsyncClient/MQTTAsyncClient.cpp | 240 ++++++ .../MQTTAsyncClient/MQTTAsyncClient.hpp | 116 +++ .../{ => ResponseBuilder}/ResponseBuilder.cpp | 2 +- .../{ => ResponseBuilder}/ResponseBuilder.hpp | 2 +- .../WebSocketServer/WebSocketServer.cpp | 157 ++++ .../WebSocketServer/WebSocketServer.hpp | 103 +++ vesper/src/ConfigManager/ConfigManager.cpp | 370 +++++---- vesper/src/ConfigManager/ConfigManager.hpp | 68 +- vesper/src/HealthMonitor/HealthMonitor.cpp | 2 +- vesper/src/HealthMonitor/HealthMonitor.hpp | 6 +- vesper/src/MqttSSL/MqttSSL.cpp | 59 -- vesper/src/MqttSSL/MqttSSL.hpp | 48 -- vesper/src/Networking/Networking.cpp | 73 +- vesper/src/Networking/Networking.hpp | 7 +- vesper/src/OTAManager/OTAManager.cpp | 203 ++++- vesper/src/OTAManager/OTAManager.hpp | 28 +- vesper/src/Player/Player.cpp | 6 +- vesper/src/Player/Player.hpp | 8 +- vesper/src/Telemetry/Telemetry.cpp | 14 +- vesper/src/TimeKeeper/TimeKeeper.cpp | 6 +- vesper/vesper.ino | 124 +-- 29 files changed, 2043 insertions(+), 1210 deletions(-) rename vesper/src/Communication/{Communication.cpp => CommandHandler/CommandHandler.cpp} (63%) create mode 100644 vesper/src/Communication/CommandHandler/CommandHandler.hpp delete mode 100644 vesper/src/Communication/Communication.hpp create mode 100644 vesper/src/Communication/CommunicationRouter/CommunicationRouter.cpp create mode 100644 vesper/src/Communication/CommunicationRouter/CommunicationRouter.hpp create mode 100644 vesper/src/Communication/MQTTAsyncClient/MQTTAsyncClient.cpp create mode 100644 vesper/src/Communication/MQTTAsyncClient/MQTTAsyncClient.hpp rename vesper/src/Communication/{ => ResponseBuilder}/ResponseBuilder.cpp (99%) rename vesper/src/Communication/{ => ResponseBuilder}/ResponseBuilder.hpp (98%) create mode 100644 vesper/src/Communication/WebSocketServer/WebSocketServer.cpp create mode 100644 vesper/src/Communication/WebSocketServer/WebSocketServer.hpp delete mode 100644 vesper/src/MqttSSL/MqttSSL.cpp delete mode 100644 vesper/src/MqttSSL/MqttSSL.hpp diff --git a/.gitignore b/.gitignore index b9faf64..2f583cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ +.vscode/ +docs/ +html/ vesper/build/ -vesper/.vscode/ -vesper/docs/ vesper/sd_settings/ vesper/CLAUDE.md - +vesper/flutter/ +vesper/docs_manual/ +Doxyfile diff --git a/vesper/src/BellEngine/BellEngine.cpp b/vesper/src/BellEngine/BellEngine.cpp index fa36a09..aeba09f 100644 --- a/vesper/src/BellEngine/BellEngine.cpp +++ b/vesper/src/BellEngine/BellEngine.cpp @@ -26,7 +26,7 @@ #include "../ConfigManager/ConfigManager.hpp" // Configuration and settings #include "../Telemetry/Telemetry.hpp" // System monitoring and analytics #include "../OutputManager/OutputManager.hpp" // Hardware abstraction layer -#include "../Communication/Communication.hpp" // Communication system for notifications +#include "../Communication/CommunicationRouter/CommunicationRouter.hpp" // Communication system for notifications // ═════════════════════════════════════════════════════════════════════════════════ // CONSTRUCTOR & DESTRUCTOR IMPLEMENTATION @@ -94,7 +94,7 @@ void BellEngine::begin() { /** * @brief Set Communication manager reference for bell notifications */ -void BellEngine::setCommunicationManager(Communication* commManager) { +void BellEngine::setCommunicationManager(CommunicationRouter* commManager) { _communicationManager = commManager; LOG_DEBUG("BellEngine: Communication manager %s", commManager ? "connected" : "disconnected"); @@ -181,6 +181,9 @@ void BellEngine::engineLoop() { playbackLoop(); + // Check telemetry for overloads and send notifications if needed + checkAndNotifyOverloads(); + // Pause handling AFTER complete loop - never interrupt mid-melody! while (_player.isPaused && _player.isPlaying && !_player.hardStop) { LOG_DEBUG("⏸️ Pausing between melody loops"); @@ -241,15 +244,15 @@ void BellEngine::activateNote(uint16_t note) { // Iterate through each bit position (note index) for (uint8_t noteIndex = 0; noteIndex < 16; noteIndex++) { - if (note & (1 << noteIndex)) { - // Get bell mapping - uint8_t bellIndex = _player.noteAssignments[noteIndex]; + if (note & (1 << noteIndex)) { + // Get bell mapping (noteAssignments stored as 1-indexed) + uint8_t bellConfig = _player.noteAssignments[noteIndex]; - // Skip if no bell assigned - if (bellIndex == 0) continue; + // Skip if no bell assigned + if (bellConfig == 0) continue; - // Convert to 0-based indexing - bellIndex = bellIndex - 1; + // Convert 1-indexed config to 0-indexed bellIndex + uint8_t bellIndex = bellConfig - 1; // Additional safety check to prevent underflow crashes if (bellIndex >= 255) { @@ -373,12 +376,6 @@ bool BellEngine::isHealthy() const { return false; } - // Check if we're not in emergency stop state - if (_emergencyStop.load()) { - LOG_DEBUG("BellEngine: Unhealthy - Emergency stop active"); - return false; - } - // Check if OutputManager is properly connected and healthy if (!_outputManager.isInitialized()) { LOG_DEBUG("BellEngine: Unhealthy - OutputManager not initialized"); @@ -387,3 +384,48 @@ bool BellEngine::isHealthy() const { return true; } + +void BellEngine::checkAndNotifyOverloads() { + if (!_communicationManager) { + return; // No communication manager available + } + + // Collect overloaded bells from telemetry + std::vector criticalBells; + std::vector criticalLoads; + std::vector warningBells; + std::vector warningLoads; + + bool hasOverload = false; + + for (uint8_t i = 0; i < 16; i++) { + uint16_t load = _telemetry.getBellLoad(i); + if (load == 0) continue; // Skip inactive bells + + if (_telemetry.isOverloaded(i)) { + criticalBells.push_back(i); + criticalLoads.push_back(load); + hasOverload = true; + } else { + // Check thresholds - get max load for this bell (assume 60 default) + uint16_t criticalThreshold = 54; // 90% of 60 + uint16_t warningThreshold = 36; // 60% of 60 + + if (load > criticalThreshold) { + criticalBells.push_back(i); + criticalLoads.push_back(load); + } else if (load > warningThreshold) { + warningBells.push_back(i); + warningLoads.push_back(load); + } + } + } + + // Send notifications if needed + if (!criticalBells.empty()) { + String severity = hasOverload ? "critical" : "warning"; + _communicationManager->sendBellOverloadNotification(criticalBells, criticalLoads, severity); + } else if (!warningBells.empty()) { + _communicationManager->sendBellOverloadNotification(warningBells, warningLoads, "warning"); + } +} diff --git a/vesper/src/BellEngine/BellEngine.hpp b/vesper/src/BellEngine/BellEngine.hpp index d2baf88..3b4654f 100644 --- a/vesper/src/BellEngine/BellEngine.hpp +++ b/vesper/src/BellEngine/BellEngine.hpp @@ -60,7 +60,7 @@ class Player; // Melody playback controller class ConfigManager; // Configuration and settings management class Telemetry; // System monitoring and analytics class OutputManager; // Hardware abstraction layer -class Communication; // Communication system for notifications +class CommunicationRouter; // Communication system for notifications // ═════════════════════════════════════════════════════════════════════════════════ // ARCHITECTURE MIGRATION NOTE @@ -221,7 +221,7 @@ public: * @brief Set Communication manager reference for bell notifications * @param commManager Pointer to communication manager */ - void setCommunicationManager(Communication* commManager); + void setCommunicationManager(CommunicationRouter* commManager); // ═══════════════════════════════════════════════════════════════════════════════ // HEALTH CHECK METHOD @@ -239,7 +239,7 @@ private: ConfigManager& _configManager; // Configuration manager for bell settings Telemetry& _telemetry; // System monitoring and strike tracking OutputManager& _outputManager; // 🔥 Hardware abstraction layer for relay control - Communication* _communicationManager; // Communication system for bell notifications + CommunicationRouter* _communicationManager; // Communication system for bell notifications // ═══════════════════════════════════════════════════════════════════════════════ // ENGINE STATE (Atomic for thread safety) @@ -348,6 +348,15 @@ private: */ void emergencyShutdown(); + /** + * @brief Check telemetry and send overload notifications + * + * Monitors telemetry for bell overloads and sends notifications + * through Communication manager when thresholds are exceeded. + * Called after each playback loop. + */ + void checkAndNotifyOverloads(); + /** * @brief Notify WebSocket clients of fired bells * @param bellIndices Vector of bell indices that were fired (1-indexed) diff --git a/vesper/src/Communication/Communication.cpp b/vesper/src/Communication/CommandHandler/CommandHandler.cpp similarity index 63% rename from vesper/src/Communication/Communication.cpp rename to vesper/src/Communication/CommandHandler/CommandHandler.cpp index c0f79fe..38807b8 100644 --- a/vesper/src/Communication/Communication.cpp +++ b/vesper/src/Communication/CommandHandler/CommandHandler.cpp @@ -1,421 +1,55 @@ -#include "Communication.hpp" -#include "../ConfigManager/ConfigManager.hpp" -#include "../OTAManager/OTAManager.hpp" -#include "../Networking/Networking.hpp" -#include "../Logging/Logging.hpp" -#include "../Player/Player.hpp" -#include "../FileManager/FileManager.hpp" -#include "../TimeKeeper/TimeKeeper.hpp" -#include "../FirmwareValidator/FirmwareValidator.hpp" -#include "../MqttSSL/MqttSSL.hpp" -#include +/* + * COMMANDHANDLER.CPP - Command Processing Implementation + */ -Communication* Communication::_instance = nullptr; -StaticJsonDocument<2048> Communication::_parseDocument; +#include "CommandHandler.hpp" +#include "../../ConfigManager/ConfigManager.hpp" +#include "../../OTAManager/OTAManager.hpp" +#include "../../Player/Player.hpp" +#include "../../FileManager/FileManager.hpp" +#include "../../TimeKeeper/TimeKeeper.hpp" +#include "../../FirmwareValidator/FirmwareValidator.hpp" +#include "../../ClientManager/ClientManager.hpp" +#include "../../Logging/Logging.hpp" +#include "../ResponseBuilder/ResponseBuilder.hpp" -static void connectToMqttWrapper(TimerHandle_t xTimer) { - if (Communication::_instance) { - Communication::_instance->connectToMqtt(); - } -} - -Communication::Communication(ConfigManager& configManager, - OTAManager& otaManager, - Networking& networking, - AsyncMqttClient& mqttClient, - AsyncWebServer& server, - AsyncWebSocket& webSocket, - AsyncUDP& udp) +CommandHandler::CommandHandler(ConfigManager& configManager, OTAManager& otaManager) : _configManager(configManager) , _otaManager(otaManager) - , _networking(networking) - , _mqttClient(mqttClient) - , _server(server) - , _webSocket(webSocket) - , _udp(udp) , _player(nullptr) , _fileManager(nullptr) , _timeKeeper(nullptr) , _firmwareValidator(nullptr) - , _mqttReconnectTimer(nullptr) { + , _clientManager(nullptr) + , _responseCallback(nullptr) {} - _instance = this; +CommandHandler::~CommandHandler() {} + +void CommandHandler::setPlayerReference(Player* player) { + _player = player; } -Communication::~Communication() { - if (_mqttReconnectTimer != nullptr) { - xTimerDelete(_mqttReconnectTimer, portMAX_DELAY); - _mqttReconnectTimer = nullptr; - } - _instance = nullptr; +void CommandHandler::setFileManagerReference(FileManager* fm) { + _fileManager = fm; } -void Communication::begin() { - LOG_INFO("Initializing Communication Manager v2.1"); - initMqtt(); - initWebSocket(); - LOG_INFO("Communication Manager initialized with multi-client support"); +void CommandHandler::setTimeKeeperReference(Timekeeper* tk) { + _timeKeeper = tk; } -void Communication::initMqtt() { - _mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE, - (void*)0, connectToMqttWrapper); - - _mqttClient.onConnect(onMqttConnect); - _mqttClient.onDisconnect(onMqttDisconnect); - _mqttClient.onSubscribe(onMqttSubscribe); - _mqttClient.onUnsubscribe(onMqttUnsubscribe); - _mqttClient.onMessage(onMqttMessage); - _mqttClient.onPublish(onMqttPublish); - - auto& mqttConfig = _configManager.getMqttConfig(); - - // Log SSL status - AsyncMqttClient SSL is compile-time configured - MqttSSL::logSSLStatus(_mqttClient, mqttConfig.port); - - // DEBUG: Log connection details - LOG_INFO("MQTT Config: host=%s, port=%d, user=%s, pass=%s", - mqttConfig.host.c_str(), mqttConfig.port, - mqttConfig.user.c_str(), mqttConfig.password.c_str()); - - _mqttClient.setServer(mqttConfig.host.c_str(), mqttConfig.port); - _mqttClient.setCredentials(mqttConfig.user.c_str(), mqttConfig.password.c_str()); +void CommandHandler::setFirmwareValidatorReference(FirmwareValidator* fv) { + _firmwareValidator = fv; } -void Communication::initWebSocket() { - _webSocket.onEvent(onWebSocketEvent); - _server.addHandler(&_webSocket); +void CommandHandler::setClientManagerReference(ClientManager* cm) { + _clientManager = cm; } -void Communication::connectToMqtt() { - if (_networking.isConnected()) { - LOG_INFO("Connecting to MQTT..."); - _mqttClient.connect(); - } else { - LOG_WARNING("Cannot connect to MQTT: No network connection"); - } +void CommandHandler::setResponseCallback(ResponseCallback callback) { + _responseCallback = callback; } -void Communication::subscribeMqtt() { - char topic[64]; - snprintf(topic, sizeof(topic), "vesper/%s/control", _configManager.getDeviceUID().c_str()); - uint16_t topicId = _mqttClient.subscribe(topic, 2); - LOG_INFO("Subscribing to Command topic, QoS 2, packetId: %d", topicId); -} - -void Communication::sendResponse(const String& response, const MessageContext& context) { - if (context.source == MessageSource::MQTT) { - publishToMqtt(response); - } else if (context.source == MessageSource::WEBSOCKET) { - _clientManager.sendToClient(context.clientId, response); - } -} - -void Communication::sendSuccessResponse(const String& type, const String& payload, const MessageContext& context) { - String response = ResponseBuilder::success(type, payload); - sendResponse(response, context); -} - -void Communication::sendErrorResponse(const String& type, const String& message, const MessageContext& context) { - String response = ResponseBuilder::error(type, message); - sendResponse(response, context); -} - -void Communication::broadcastStatus(const String& statusMessage) { - publishToMqtt(statusMessage); - broadcastToAllWebSocketClients(statusMessage); -} - -void Communication::broadcastStatus(const JsonDocument& statusJson) { - String statusMessage; - serializeJson(statusJson, statusMessage); - broadcastStatus(statusMessage); -} - -void Communication::sendBellOverloadNotification(const std::vector& bellNumbers, - const std::vector& bellLoads, - const String& severity) { - StaticJsonDocument<512> overloadMsg; - overloadMsg["status"] = "INFO"; - overloadMsg["type"] = "bell_overload"; - - JsonArray bellsArray = overloadMsg["payload"]["bells"].to(); - JsonArray loadsArray = overloadMsg["payload"]["loads"].to(); - - for (size_t i = 0; i < bellNumbers.size() && i < bellLoads.size(); i++) { - bellsArray.add(bellNumbers[i] + 1); - loadsArray.add(bellLoads[i]); - } - - overloadMsg["payload"]["severity"] = severity; - broadcastStatus(overloadMsg); - - LOG_WARNING("Bell overload notification sent: %d bells, severity: %s", - bellNumbers.size(), severity.c_str()); -} - -void Communication::broadcastToMasterClients(const String& message) { - _clientManager.sendToMasterClients(message); -} - -void Communication::broadcastToSecondaryClients(const String& message) { - _clientManager.sendToSecondaryClients(message); -} - -void Communication::broadcastToAllWebSocketClients(const String& message) { - _clientManager.broadcastToAll(message); -} - -void Communication::broadcastToAllWebSocketClients(const JsonDocument& message) { - String messageStr; - serializeJson(message, messageStr); - _clientManager.broadcastToAll(messageStr); - LOG_DEBUG("Broadcasted JSON to WebSocket clients: %s", messageStr.c_str()); -} - -void Communication::publishToMqtt(const String& data) { - if (_mqttClient.connected()) { - char topicData[64]; - snprintf(topicData, sizeof(topicData), "vesper/%s/data", _configManager.getDeviceUID().c_str()); - _mqttClient.publish(topicData, 0, true, data.c_str()); - LOG_DEBUG("Published to MQTT: %s", data.c_str()); - } else { - LOG_ERROR("MQTT Not Connected! Message Failed: %s", data.c_str()); - } -} - -void Communication::onNetworkConnected() { - LOG_DEBUG("Network connected - attempting MQTT connection"); - connectToMqtt(); -} - -void Communication::onNetworkDisconnected() { - LOG_DEBUG("Network disconnected - stopping MQTT timer"); - xTimerStop(_mqttReconnectTimer, 0); -} - -void Communication::setupUdpDiscovery() { - uint16_t discoveryPort = _configManager.getNetworkConfig().discoveryPort; - if (_udp.listen(discoveryPort)) { - LOG_INFO("UDP discovery listening on port %u", discoveryPort); - - _udp.onPacket([this](AsyncUDPPacket packet) { - String msg = String((const char*)packet.data(), packet.length()); - LOG_DEBUG("UDP from %s:%u -> %s", - packet.remoteIP().toString().c_str(), - packet.remotePort(), - msg.c_str()); - - bool shouldReply = false; - - if (msg.indexOf("discover") >= 0) { - shouldReply = true; - } else { - StaticJsonDocument<128> req; - DeserializationError err = deserializeJson(req, msg); - if (!err) { - shouldReply = (req["op"] == "discover" && req["svc"] == "vesper"); - } - } - - if (!shouldReply) return; - - StaticJsonDocument<256> doc; - doc["op"] = "discover_reply"; - doc["svc"] = "vesper"; - doc["ver"] = 1; - - doc["name"] = "Proj. Vesper v0.5"; - doc["id"] = _configManager.getDeviceUID(); - doc["ip"] = _networking.getLocalIP(); - char wsUrl[64]; - snprintf(wsUrl, sizeof(wsUrl), "ws://%s/ws", _networking.getLocalIP().c_str()); - doc["ws"] = wsUrl; - doc["port"] = 80; - doc["fw"] = "1.2.3"; - doc["clients"] = _clientManager.getClientCount(); - - String out; - serializeJson(doc, out); - - _udp.writeTo((const uint8_t*)out.c_str(), out.length(), - packet.remoteIP(), packet.remotePort()); - }); - } else { - LOG_ERROR("Failed to start UDP discovery."); - } -} - -void Communication::onMqttConnect(bool sessionPresent) { - LOG_INFO("Connected to MQTT"); - if (_instance) { - _instance->subscribeMqtt(); - } -} - -void Communication::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) { - String reasonStr; - switch (reason) { - case AsyncMqttClientDisconnectReason::TCP_DISCONNECTED: - reasonStr = "TCP_DISCONNECTED"; - break; - case AsyncMqttClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: - reasonStr = "UNACCEPTABLE_PROTOCOL_VERSION"; - break; - case AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED: - reasonStr = "IDENTIFIER_REJECTED"; - break; - case AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE: - reasonStr = "SERVER_UNAVAILABLE"; - break; - case AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS: - reasonStr = "MALFORMED_CREDENTIALS"; - break; - case AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED: - reasonStr = "NOT_AUTHORIZED"; - break; - case AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT: - reasonStr = "TLS_BAD_FINGERPRINT"; - break; - default: - reasonStr = "UNKNOWN(" + String((int)reason) + ")"; - break; - } - - LOG_WARNING("Disconnected from MQTT: %s", reasonStr.c_str()); - - if (_instance && _instance->_networking.isConnected()) { - xTimerStart(_instance->_mqttReconnectTimer, 0); - } -} - -void Communication::onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, - size_t len, size_t index, size_t total) { - if (!_instance) return; - - char expectedTopic[64]; - snprintf(expectedTopic, sizeof(expectedTopic), "vesper/%s/control", _instance->_configManager.getDeviceUID().c_str()); - - if (strcmp(topic, expectedTopic) == 0) { - JsonDocument command = _instance->parsePayload(payload); - MessageContext context(MessageSource::MQTT); - _instance->handleCommand(command, context); - } -} - -void Communication::onMqttSubscribe(uint16_t packetId, uint8_t qos) { - LOG_INFO("Subscribe acknowledged. PacketID: %d / QoS: %d", packetId, qos); -} - -void Communication::onMqttUnsubscribe(uint16_t packetId) { - LOG_INFO("Unsubscribe Acknowledged. PacketID: %d", packetId); -} - -void Communication::onMqttPublish(uint16_t packetId) { - LOG_DEBUG("Publish Acknowledged. PacketID: %d", packetId); -} - -void Communication::onWebSocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, - AwsEventType type, void* arg, uint8_t* data, size_t len) { - if (!_instance) return; - - switch (type) { - case WS_EVT_CONNECT: - _instance->onWebSocketConnect(client); - break; - - case WS_EVT_DISCONNECT: - _instance->onWebSocketDisconnect(client); - break; - - case WS_EVT_DATA: - _instance->onWebSocketReceived(client, arg, data, len); - break; - - case WS_EVT_ERROR: - LOG_ERROR("WebSocket client #%u error(%u): %s", client->id(), *((uint16_t*)arg), (char*)data); - break; - - default: - break; - } -} - -void Communication::onWebSocketConnect(AsyncWebSocketClient* client) { - LOG_INFO("WebSocket client #%u connected from %s", client->id(), client->remoteIP().toString().c_str()); - - _clientManager.addClient(client, ClientManager::DeviceType::UNKNOWN); - - String welcomeMsg = ResponseBuilder::success("connection", "Connected to Vesper"); - _clientManager.sendToClient(client->id(), welcomeMsg); -} - -void Communication::onWebSocketDisconnect(AsyncWebSocketClient* client) { - LOG_INFO("WebSocket client #%u disconnected", client->id()); - _clientManager.removeClient(client->id()); - _clientManager.cleanupDisconnectedClients(); -} - -void Communication::onWebSocketReceived(AsyncWebSocketClient* client, void* arg, uint8_t* data, size_t len) { - AwsFrameInfo* info = (AwsFrameInfo*)arg; - - if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) { - char* payload = (char*)malloc(len + 1); - if (!payload) { - LOG_ERROR("Failed to allocate memory for WebSocket payload"); - return; - } - - memcpy(payload, data, len); - payload[len] = '\0'; - - LOG_DEBUG("Received WebSocket message from client #%u (length %d): %s", - client->id(), len, payload); - - JsonDocument command = parsePayload(payload); - MessageContext context(MessageSource::WEBSOCKET, client->id()); - - _clientManager.updateClientLastSeen(client->id()); - - handleCommand(command, context); - - free(payload); - } else { - LOG_WARNING("Received fragmented or non-text WebSocket message - ignoring"); - } -} - -JsonDocument Communication::parsePayload(char* payload) { - _parseDocument.clear(); - - size_t payloadLen = strlen(payload); - - LOG_DEBUG("Parsing payload (length %d): %s", payloadLen, payload); - - if (payloadLen == 0) { - LOG_ERROR("Empty payload received"); - return _parseDocument; - } - - String cleanJson = String(payload); - cleanJson.replace("\r\n", ""); - cleanJson.replace("\n", ""); - cleanJson.replace("\r", ""); - cleanJson.trim(); - - LOG_DEBUG("Cleaned JSON: %s", cleanJson.c_str()); - - DeserializationError error = deserializeJson(_parseDocument, cleanJson); - if (error) { - LOG_ERROR("JSON deserialization failed: %s", error.c_str()); - } else { - LOG_DEBUG("JSON parsed successfully"); - } - - return _parseDocument; -} - -void Communication::handleCommand(JsonDocument& command, const MessageContext& context) { +void CommandHandler::processCommand(JsonDocument& command, const MessageContext& context) { String cmd = command["cmd"]; JsonVariant contents = command["contents"]; @@ -444,12 +78,28 @@ void Communication::handleCommand(JsonDocument& command, const MessageContext& c } } -void Communication::handlePingCommand(const MessageContext& context) { +void CommandHandler::sendResponse(const String& response, const MessageContext& context) { + if (_responseCallback) { + _responseCallback(response, context); + } +} + +void CommandHandler::sendSuccessResponse(const String& type, const String& payload, const MessageContext& context) { + String response = ResponseBuilder::success(type, payload); + sendResponse(response, context); +} + +void CommandHandler::sendErrorResponse(const String& type, const String& message, const MessageContext& context) { + String response = ResponseBuilder::error(type, message); + sendResponse(response, context); +} + +void CommandHandler::handlePingCommand(const MessageContext& context) { String response = ResponseBuilder::pong(); sendResponse(response, context); } -void Communication::handleStatusCommand(const MessageContext& context) { +void CommandHandler::handleStatusCommand(const MessageContext& context) { PlayerStatus playerStatus = _player ? _player->getStatus() : PlayerStatus::STOPPED; uint32_t timeElapsedMs = 0; uint64_t projectedRunTime = 0; @@ -468,12 +118,19 @@ void Communication::handleStatusCommand(const MessageContext& context) { sendResponse(response, context); } -void Communication::handleIdentifyCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleIdentifyCommand(JsonVariant contents, const MessageContext& context) { if (context.source != MessageSource::WEBSOCKET) { sendErrorResponse("identify", "Identify command only available via WebSocket", context); return; } + // 🛡️ SAFETY CHECK: Ensure ClientManager reference is set + if (!_clientManager) { + LOG_ERROR("ClientManager reference not set in CommandHandler!"); + sendErrorResponse("identify", "Internal error: ClientManager not available", context); + return; + } + if (!contents.containsKey("device_type")) { sendErrorResponse("identify", "Missing device_type parameter", context); return; @@ -489,7 +146,7 @@ void Communication::handleIdentifyCommand(JsonVariant contents, const MessageCon } if (deviceType != ClientManager::DeviceType::UNKNOWN) { - _clientManager.updateClientType(context.clientId, deviceType); + _clientManager->updateClientType(context.clientId, deviceType); sendSuccessResponse("identify", "Device identified as " + deviceTypeStr, context); LOG_INFO("Client #%u identified as %s device", context.clientId, deviceTypeStr.c_str()); } else { @@ -497,7 +154,7 @@ void Communication::handleIdentifyCommand(JsonVariant contents, const MessageCon } } -void Communication::handlePlaybackCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handlePlaybackCommand(JsonVariant contents, const MessageContext& context) { if (_player) { bool success = _player->command(contents); @@ -512,7 +169,7 @@ void Communication::handlePlaybackCommand(JsonVariant contents, const MessageCon } } -void Communication::handleFileManagerCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleFileManagerCommand(JsonVariant contents, const MessageContext& context) { if (!contents.containsKey("action")) { sendErrorResponse("file_manager", "Missing action parameter", context); return; @@ -533,7 +190,7 @@ void Communication::handleFileManagerCommand(JsonVariant contents, const Message } } -void Communication::handleRelaySetupCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleRelaySetupCommand(JsonVariant contents, const MessageContext& context) { if (!contents.containsKey("action")) { sendErrorResponse("relay_setup", "Missing action parameter", context); return; @@ -552,7 +209,7 @@ void Communication::handleRelaySetupCommand(JsonVariant contents, const MessageC } } -void Communication::handleClockSetupCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleClockSetupCommand(JsonVariant contents, const MessageContext& context) { if (!contents.containsKey("action")) { sendErrorResponse("clock_setup", "Missing action parameter", context); return; @@ -585,7 +242,7 @@ void Communication::handleClockSetupCommand(JsonVariant contents, const MessageC } } -void Communication::handleSystemInfoCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleSystemInfoCommand(JsonVariant contents, const MessageContext& context) { if (!contents.containsKey("action")) { sendErrorResponse("system_info", "Missing action parameter", context); return; @@ -606,13 +263,15 @@ void Communication::handleSystemInfoCommand(JsonVariant contents, const MessageC handleRollbackFirmwareCommand(context); } else if (action == "get_firmware_status") { handleGetFirmwareStatusCommand(context); + } else if (action == "get_full_settings") { + handleGetFullSettingsCommand(context); } else { LOG_WARNING("Unknown system info action: %s", action.c_str()); sendErrorResponse("system_info", "Unknown action: " + action, context); } } -void Communication::handleListMelodiesCommand(const MessageContext& context) { +void CommandHandler::handleListMelodiesCommand(const MessageContext& context) { if (!_fileManager) { sendErrorResponse("list_melodies", "FileManager not available", context); return; @@ -639,7 +298,7 @@ void Communication::handleListMelodiesCommand(const MessageContext& context) { sendResponse(responseStr, context); } -void Communication::handleDownloadMelodyCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleDownloadMelodyCommand(JsonVariant contents, const MessageContext& context) { if (!_fileManager) { sendErrorResponse("download_melody", "FileManager not available", context); return; @@ -653,7 +312,7 @@ void Communication::handleDownloadMelodyCommand(JsonVariant contents, const Mess sendResponse(response, context); } -void Communication::handleDeleteMelodyCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleDeleteMelodyCommand(JsonVariant contents, const MessageContext& context) { if (!_fileManager) { sendErrorResponse("delete_melody", "FileManager not available", context); return; @@ -676,7 +335,7 @@ void Communication::handleDeleteMelodyCommand(JsonVariant contents, const Messag } } -void Communication::handleSetRelayTimersCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleSetRelayTimersCommand(JsonVariant contents, const MessageContext& context) { try { _configManager.updateBellDurations(contents); // Save configuration to ensure persistence @@ -694,7 +353,7 @@ void Communication::handleSetRelayTimersCommand(JsonVariant contents, const Mess } } -void Communication::handleSetRelayOutputsCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleSetRelayOutputsCommand(JsonVariant contents, const MessageContext& context) { try { _configManager.updateBellOutputs(contents); // Note: Bell outputs are typically not persisted to SD card as they're more of a mapping configuration @@ -706,7 +365,7 @@ void Communication::handleSetRelayOutputsCommand(JsonVariant contents, const Mes } } -void Communication::handleSetClockOutputsCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleSetClockOutputsCommand(JsonVariant contents, const MessageContext& context) { try { _configManager.updateClockOutputs(contents); // Save configuration to ensure persistence @@ -724,7 +383,7 @@ void Communication::handleSetClockOutputsCommand(JsonVariant contents, const Mes } } -void Communication::handleSetClockTimingsCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleSetClockTimingsCommand(JsonVariant contents, const MessageContext& context) { try { _configManager.updateClockOutputs(contents); // Save configuration to ensure persistence @@ -742,7 +401,7 @@ void Communication::handleSetClockTimingsCommand(JsonVariant contents, const Mes } } -void Communication::handleSetClockAlertsCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleSetClockAlertsCommand(JsonVariant contents, const MessageContext& context) { try { _configManager.updateClockAlerts(contents); // Save configuration to ensure persistence @@ -760,7 +419,7 @@ void Communication::handleSetClockAlertsCommand(JsonVariant contents, const Mess } } -void Communication::handleSetClockBacklightCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleSetClockBacklightCommand(JsonVariant contents, const MessageContext& context) { try { _configManager.updateClockBacklight(contents); // Save configuration to ensure persistence @@ -778,7 +437,7 @@ void Communication::handleSetClockBacklightCommand(JsonVariant contents, const M } } -void Communication::handleSetClockSilenceCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleSetClockSilenceCommand(JsonVariant contents, const MessageContext& context) { try { _configManager.updateClockSilence(contents); // Save configuration to ensure persistence @@ -796,7 +455,7 @@ void Communication::handleSetClockSilenceCommand(JsonVariant contents, const Mes } } -void Communication::handleSetRtcTimeCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleSetRtcTimeCommand(JsonVariant contents, const MessageContext& context) { if (!_timeKeeper) { sendErrorResponse("set_rtc_time", "TimeKeeper not available", context); return; @@ -866,7 +525,7 @@ void Communication::handleSetRtcTimeCommand(JsonVariant contents, const MessageC } } -void Communication::handleSetPhysicalClockTimeCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleSetPhysicalClockTimeCommand(JsonVariant contents, const MessageContext& context) { if (!contents.containsKey("hour") || !contents.containsKey("minute")) { sendErrorResponse("set_physical_clock_time", "Missing hour or minute parameter", context); return; @@ -886,8 +545,12 @@ void Communication::handleSetPhysicalClockTimeCommand(JsonVariant contents, cons return; } + // Convert 24-hour to 12-hour format for analog clock + int clockHour = hour % 12; + if (clockHour == 0) clockHour = 12; // Midnight/Noon = 12, not 0 + // Set the physical clock time using ConfigManager - _configManager.setPhysicalClockHour(hour); + _configManager.setPhysicalClockHour(clockHour); _configManager.setPhysicalClockMinute(minute); _configManager.setLastSyncTime(millis() / 1000); @@ -896,15 +559,15 @@ void Communication::handleSetPhysicalClockTimeCommand(JsonVariant contents, cons if (saved) { sendSuccessResponse("set_physical_clock_time", "Physical clock time updated and saved successfully", context); - LOG_INFO("Physical clock time set to %02d:%02d and saved to SD", hour, minute); + LOG_INFO("Physical clock time set to %02d:%02d (12h: %02d:%02d) and saved to SD", + hour, minute, clockHour, minute); } else { sendErrorResponse("set_physical_clock_time", "Physical clock time updated but failed to save to SD card", context); LOG_ERROR("Physical clock time set to %02d:%02d but failed to save to SD", hour, minute); } } - -void Communication::handlePauseClockUpdatesCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handlePauseClockUpdatesCommand(JsonVariant contents, const MessageContext& context) { if (!_timeKeeper) { return; } @@ -922,7 +585,7 @@ void Communication::handlePauseClockUpdatesCommand(JsonVariant contents, const M } } -void Communication::handleSetClockEnabledCommand(JsonVariant contents, const MessageContext& context) { +void CommandHandler::handleSetClockEnabledCommand(JsonVariant contents, const MessageContext& context) { if (!contents.containsKey("enabled")) { sendErrorResponse("set_clock_enabled", "Missing enabled parameter", context); return; @@ -948,29 +611,7 @@ void Communication::handleSetClockEnabledCommand(JsonVariant contents, const Mes } } - - - -String Communication::getPayloadContent(char* data, size_t len) { - String content = ""; - for (size_t i = 0; i < len; i++) { - content.concat(data[i]); - } - return content; -} - -int Communication::extractBellNumber(const String& key) { - if (key.length() >= 2) { - char firstChar = key.charAt(0); - if (firstChar == 'b' || firstChar == 'c') { - String numberPart = key.substring(1); - return numberPart.toInt(); - } - } - return 0; -} - -void Communication::handleGetDeviceTimeCommand(const MessageContext& context) { +void CommandHandler::handleGetDeviceTimeCommand(const MessageContext& context) { StaticJsonDocument<256> response; response["status"] = "SUCCESS"; response["type"] = "device_time"; @@ -1003,7 +644,7 @@ void Communication::handleGetDeviceTimeCommand(const MessageContext& context) { LOG_DEBUG("Device time requested"); } -void Communication::handleGetClockTimeCommand(const MessageContext& context) { +void CommandHandler::handleGetClockTimeCommand(const MessageContext& context) { StaticJsonDocument<256> response; response["status"] = "SUCCESS"; response["type"] = "clock_time"; @@ -1025,80 +666,11 @@ void Communication::handleGetClockTimeCommand(const MessageContext& context) { _configManager.getLastSyncTime()); } -// ════════════════════════════════════════════════════════════════════════════ -// HEALTH CHECK IMPLEMENTATION -// ════════════════════════════════════════════════════════════════════════════ - -bool Communication::isHealthy() const { - // Check if required references are set - if (!_player) { - LOG_DEBUG("Communication: Unhealthy - Player reference not set"); - return false; - } - - if (!_fileManager) { - LOG_DEBUG("Communication: Unhealthy - FileManager reference not set"); - return false; - } - - if (!_timeKeeper) { - LOG_DEBUG("Communication: Unhealthy - TimeKeeper reference not set"); - return false; - } - - // Check if WebSocket server is active - if (!hasActiveWebSocketClients() && !isMqttConnected()) { - LOG_DEBUG("Communication: Unhealthy - No active connections (WebSocket or MQTT)"); - return false; - } - - // Check if MQTT reconnection timer exists and is functioning - if (_mqttReconnectTimer == nullptr) { - LOG_DEBUG("Communication: Unhealthy - MQTT reconnection timer not created"); - return false; - } - - // Check if networking dependency is healthy - if (!_networking.isConnected()) { - LOG_DEBUG("Communication: Unhealthy - No network connection"); - return false; - } - - return true; -} - -// ════════════════════════════════════════════════════════════════════════════ -// SYSTEM COMMAND IMPLEMENTATION -// ════════════════════════════════════════════════════════════════════════════ - -void Communication::handleSystemCommand(JsonVariant contents, const MessageContext& context) { - if (!contents.containsKey("action")) { - sendErrorResponse("system", "Missing action parameter", context); - return; - } - - String action = contents["action"]; - LOG_DEBUG("Processing system action: %s", action.c_str()); - - if (action == "commit_firmware") { - handleCommitFirmwareCommand(context); - } else if (action == "rollback_firmware") { - handleRollbackFirmwareCommand(context); - } else if (action == "get_firmware_status") { - handleGetFirmwareStatusCommand(context); - } else if (action == "set_network_config") { - handleSetNetworkConfigCommand(contents, context); - } else { - LOG_WARNING("Unknown system action: %s", action.c_str()); - sendErrorResponse("system", "Unknown action: " + action, context); - } -} - // ════════════════════════════════════════════════════════════════════════════ // FIRMWARE MANAGEMENT IMPLEMENTATION // ════════════════════════════════════════════════════════════════════════════ -void Communication::handleCommitFirmwareCommand(const MessageContext& context) { +void CommandHandler::handleCommitFirmwareCommand(const MessageContext& context) { if (!_firmwareValidator) { sendErrorResponse("commit_firmware", "FirmwareValidator not available", context); return; @@ -1123,7 +695,7 @@ void Communication::handleCommitFirmwareCommand(const MessageContext& context) { } } -void Communication::handleRollbackFirmwareCommand(const MessageContext& context) { +void CommandHandler::handleRollbackFirmwareCommand(const MessageContext& context) { if (!_firmwareValidator) { sendErrorResponse("rollback_firmware", "FirmwareValidator not available", context); return; @@ -1144,7 +716,7 @@ void Communication::handleRollbackFirmwareCommand(const MessageContext& context) } } -void Communication::handleGetFirmwareStatusCommand(const MessageContext& context) { +void CommandHandler::handleGetFirmwareStatusCommand(const MessageContext& context) { if (!_firmwareValidator) { sendErrorResponse("get_firmware_status", "FirmwareValidator not available", context); return; @@ -1217,18 +789,44 @@ void Communication::handleGetFirmwareStatusCommand(const MessageContext& context LOG_DEBUG("Firmware status requested: %s", stateStr.c_str()); } -// ════════════════════════════════════════════════════════════════════════════ -// NETWORK CONFIGURATION IMPLEMENTATION -// ════════════════════════════════════════════════════════════════════════════ +void CommandHandler::handleGetFullSettingsCommand(const MessageContext& context) { + LOG_DEBUG("Full settings requested"); + + // Get all settings as JSON string from ConfigManager + String settingsJson = _configManager.getAllSettingsAsJson(); + + // Parse it to embed in our response structure + StaticJsonDocument<512> response; + response["status"] = "SUCCESS"; + response["type"] = "full_settings"; + + // Parse the settings JSON and add as payload + DynamicJsonDocument settingsDoc(4096); + DeserializationError error = deserializeJson(settingsDoc, settingsJson); + + if (error) { + LOG_ERROR("Failed to parse settings JSON: %s", error.c_str()); + sendErrorResponse("get_full_settings", "Failed to serialize settings", context); + return; + } + + response["payload"] = settingsDoc.as(); + + String responseStr; + serializeJson(response, responseStr); + sendResponse(responseStr, context); + + LOG_DEBUG("Full settings sent (%d bytes)", responseStr.length()); +} -void Communication::handleSetNetworkConfigCommand(JsonVariant contents, const MessageContext& context) { + + +void CommandHandler::handleSetNetworkConfigCommand(JsonVariant contents, const MessageContext& context) { // Validate that we have at least one parameter to update bool hasHostname = contents.containsKey("hostname"); bool hasStaticIPConfig = contents.containsKey("useStaticIP"); - bool hasAPPass = contents.containsKey("apPass"); - bool hasDiscoveryPort = contents.containsKey("discoveryPort"); - if (!hasHostname && !hasStaticIPConfig && !hasAPPass && !hasDiscoveryPort) { + if (!hasHostname && !hasStaticIPConfig) { sendErrorResponse("set_network_config", "No network parameters provided", context); return; } @@ -1248,8 +846,6 @@ void Communication::handleSetNetworkConfigCommand(JsonVariant contents, const Me IPAddress dns1 = currentConfig.dns1; IPAddress dns2 = currentConfig.dns2; String hostname = currentConfig.hostname; - String apPass = currentConfig.apPass; - uint16_t discoveryPort = currentConfig.discoveryPort; // Update hostname if provided if (hasHostname) { @@ -1310,16 +906,10 @@ void Communication::handleSetNetworkConfigCommand(JsonVariant contents, const Me // If anything changed, update and save configuration if (configChanged) { - // Update network config using existing method - _configManager.updateNetworkConfig(useStaticIP, ip, gateway, subnet, dns1, dns2); + // Update network config (saves to SD internally) + _configManager.updateNetworkConfig(hostname, useStaticIP, ip, gateway, subnet, dns1, dns2); - // Manually update fields not handled by updateNetworkConfig - // Note: This is a workaround since NetworkConfig doesn't have setters for all fields - auto& writableConfig = const_cast(_configManager.getNetworkConfig()); - writableConfig.hostname = hostname; - - // Save to SD card - bool saved = _configManager.saveNetworkConfig(); + bool saved = true; // saveNetworkConfig() already called in updateNetworkConfig() if (saved) { String responseMsg = "Network configuration updated successfully"; @@ -1344,3 +934,63 @@ void Communication::handleSetNetworkConfigCommand(JsonVariant contents, const Me LOG_ERROR("Unknown exception in handleSetNetworkConfigCommand"); } } + +// ════════════════════════════════════════════════════════════════════════════ +// RESET DEFAULTS COMMAND +// ════════════════════════════════════════════════════════════════════════════ + +void CommandHandler::handleResetDefaultsCommand(const MessageContext& context) { + + LOG_WARNING("⚠️ Factory reset requested. Proceeding..."); + + try { + // Reset all configurations to defaults + bool resetComplete = _configManager.resetAllToDefaults(); + + if (resetComplete) { + sendSuccessResponse("reset_defaults", "Reset to Defaults completed. Device will Restart to apply changes.", context); + LOG_WARNING("✅ Factory reset completed and all configurations saved to SD card"); + } else { + sendErrorResponse("reset_defaults", "Reset to Defaults applied but failed to save some configurations to SD card", context); + LOG_ERROR("❌ Reset to Defaults applied but failed to save some configurations to SD card"); + } + } catch (...) { + sendErrorResponse("reset_defaults", "Failed to perform Reset to Defaults", context); + LOG_ERROR("❌ Exception occurred during Resetting to Defaults"); + } + +} + + + +// ════════════════════════════════════════════════════════════════════════════ +// SYSTEM COMMAND IMPLEMENTATION +// ════════════════════════════════════════════════════════════════════════════ + +void CommandHandler::handleSystemCommand(JsonVariant contents, const MessageContext& context) { + if (!contents.containsKey("action")) { + sendErrorResponse("system", "Missing action parameter", context); + return; + } + + String action = contents["action"]; + LOG_DEBUG("Processing system action: %s", action.c_str()); + + if (action == "status") { + handleStatusCommand(context); + } else if (action == "reset_defaults") { + handleResetDefaultsCommand(context); + } else if (action == "commit_firmware") { + handleCommitFirmwareCommand(context); + } else if (action == "rollback_firmware") { + handleRollbackFirmwareCommand(context); + } else if (action == "get_firmware_status") { + handleGetFirmwareStatusCommand(context); + } else if (action == "set_network_config") { + handleSetNetworkConfigCommand(contents, context); + } else { + LOG_WARNING("Unknown system action: %s", action.c_str()); + sendErrorResponse("system", "Unknown action: " + action, context); + } +} + diff --git a/vesper/src/Communication/CommandHandler/CommandHandler.hpp b/vesper/src/Communication/CommandHandler/CommandHandler.hpp new file mode 100644 index 0000000..2a627f9 --- /dev/null +++ b/vesper/src/Communication/CommandHandler/CommandHandler.hpp @@ -0,0 +1,141 @@ +/* + * ═══════════════════════════════════════════════════════════════════════════════════ + * COMMANDHANDLER.HPP - Unified Command Processing + * ═══════════════════════════════════════════════════════════════════════════════════ + * + * ⚙️ COMMAND ROUTER AND PROCESSOR ⚙️ + * + * Processes all incoming commands from both MQTT and WebSocket: + * • System commands (ping, status, identify) + * • Playback commands + * • File manager commands + * • Relay setup commands + * • Clock setup commands + * • Firmware management + * • Network configuration + * + * 📋 VERSION: 1.0 + * 📅 DATE: 2025-10-01 + * 👨‍💻 AUTHOR: Advanced Bell Systems + * ═══════════════════════════════════════════════════════════════════════════════════ + */ + +#pragma once + +#include +#include + +// Forward declarations +class ConfigManager; +class OTAManager; +class Player; +class FileManager; +class Timekeeper; +class FirmwareValidator; +class ClientManager; + +class CommandHandler { +public: + // Message source identification + enum class MessageSource { + MQTT, + WEBSOCKET + }; + + struct MessageContext { + MessageSource source; + uint32_t clientId; // Only for WebSocket + + MessageContext(MessageSource src, uint32_t id = 0) + : source(src), clientId(id) {} + }; + + // Response callback type + using ResponseCallback = std::function; + + explicit CommandHandler(ConfigManager& configManager, + OTAManager& otaManager); + ~CommandHandler(); + + /** + * @brief Set component references + */ + void setPlayerReference(Player* player); + void setFileManagerReference(FileManager* fm); + void setTimeKeeperReference(Timekeeper* tk); + void setFirmwareValidatorReference(FirmwareValidator* fv); + void setClientManagerReference(ClientManager* cm); + + /** + * @brief Set response callback for sending responses back + */ + void setResponseCallback(ResponseCallback callback); + + /** + * @brief Process incoming command + * @param command JSON command document + * @param context Message context (source and client ID) + */ + void processCommand(JsonDocument& command, const MessageContext& context); + +private: + // Dependencies + ConfigManager& _configManager; + OTAManager& _otaManager; + Player* _player; + FileManager* _fileManager; + Timekeeper* _timeKeeper; + FirmwareValidator* _firmwareValidator; + ClientManager* _clientManager; + ResponseCallback _responseCallback; + + // Response helpers + void sendResponse(const String& response, const MessageContext& context); + void sendSuccessResponse(const String& type, const String& payload, const MessageContext& context); + void sendErrorResponse(const String& type, const String& message, const MessageContext& context); + + // Command handlers + void handlePingCommand(const MessageContext& context); + void handleStatusCommand(const MessageContext& context); + void handleIdentifyCommand(JsonVariant contents, const MessageContext& context); + void handlePlaybackCommand(JsonVariant contents, const MessageContext& context); + void handleFileManagerCommand(JsonVariant contents, const MessageContext& context); + void handleRelaySetupCommand(JsonVariant contents, const MessageContext& context); + void handleClockSetupCommand(JsonVariant contents, const MessageContext& context); + void handleSystemInfoCommand(JsonVariant contents, const MessageContext& context); + void handleSystemCommand(JsonVariant contents, const MessageContext& context); + + // File Manager sub-commands + void handleListMelodiesCommand(const MessageContext& context); + void handleDownloadMelodyCommand(JsonVariant contents, const MessageContext& context); + void handleDeleteMelodyCommand(JsonVariant contents, const MessageContext& context); + + // Relay Setup sub-commands + void handleSetRelayTimersCommand(JsonVariant contents, const MessageContext& context); + void handleSetRelayOutputsCommand(JsonVariant contents, const MessageContext& context); + + // Clock Setup sub-commands + void handleSetClockOutputsCommand(JsonVariant contents, const MessageContext& context); + void handleSetClockTimingsCommand(JsonVariant contents, const MessageContext& context); + void handleSetClockAlertsCommand(JsonVariant contents, const MessageContext& context); + void handleSetClockBacklightCommand(JsonVariant contents, const MessageContext& context); + void handleSetClockSilenceCommand(JsonVariant contents, const MessageContext& context); + void handleSetRtcTimeCommand(JsonVariant contents, const MessageContext& context); + void handleSetPhysicalClockTimeCommand(JsonVariant contents, const MessageContext& context); + void handlePauseClockUpdatesCommand(JsonVariant contents, const MessageContext& context); + void handleSetClockEnabledCommand(JsonVariant contents, const MessageContext& context); + + // System Info sub-commands + void handleGetDeviceTimeCommand(const MessageContext& context); + void handleGetClockTimeCommand(const MessageContext& context); + void handleCommitFirmwareCommand(const MessageContext& context); + void handleRollbackFirmwareCommand(const MessageContext& context); + void handleGetFirmwareStatusCommand(const MessageContext& context); + void handleGetFullSettingsCommand(const MessageContext& context); + + // Network configuration + void handleSetNetworkConfigCommand(JsonVariant contents, const MessageContext& context); + + // System Config + void handleResetDefaultsCommand(const MessageContext& context); +}; diff --git a/vesper/src/Communication/Communication.hpp b/vesper/src/Communication/Communication.hpp deleted file mode 100644 index 3375096..0000000 --- a/vesper/src/Communication/Communication.hpp +++ /dev/null @@ -1,232 +0,0 @@ -/* - * ═══════════════════════════════════════════════════════════════════════════════════ - * COMMUNICATION.HPP - Multi-Protocol Communication Manager v3.0 - * ═══════════════════════════════════════════════════════════════════════════════════ - * - * 📡 THE COMMUNICATION HUB OF VESPER 📡 - * - * This class manages all external communication protocols including MQTT, - * WebSocket, and UDP discovery. It provides a unified interface for - * grouped command handling and status reporting across multiple protocols. - * - * 🏗️ ARCHITECTURE: - * • Multi-protocol support with unified grouped command processing - * • Multi-client WebSocket support with device type identification - * • Automatic connection management and reconnection - * • Unified response system for consistent messaging - * • Thread-safe operation with proper resource management - * • Batch command support for efficient configuration - * - * 📡 SUPPORTED PROTOCOLS: - * • MQTT: Primary control interface with auto-reconnection - * • WebSocket: Real-time multi-client web interface communication - * • UDP Discovery: Auto-discovery service for network scanning - * - * 📱 CLIENT MANAGEMENT: - * • Support for multiple WebSocket clients (master/secondary devices) - * • Client type identification and targeted messaging - * • Automatic cleanup of disconnected clients - * • Broadcast capabilities for status updates - * - * 🔄 MESSAGE ROUTING: - * • Commands accepted from both MQTT and WebSocket - * • Responses sent only to originating protocol/client - * • Status broadcasts sent to all WebSocket clients + MQTT - * • Grouped command processing for all protocols - * - * 📋 VERSION: 3.0 (Grouped commands + batch processing) - * 📅 DATE: 2025 - * 👨‍💻 AUTHOR: Advanced Bell Systems - * ═══════════════════════════════════════════════════════════════════════════════════ - */ - -#pragma once - -#include -#include -#include -#include -#include -#include "ResponseBuilder.hpp" -#include "../ClientManager/ClientManager.hpp" - -class ConfigManager; -class OTAManager; -class Player; -class FileManager; -class Timekeeper; -class Networking; -class FirmwareValidator; - -class Communication { -public: - // Message source identification for response routing - enum class MessageSource { - MQTT, - WEBSOCKET - }; - - struct MessageContext { - MessageSource source; - uint32_t clientId; // Only used for WebSocket messages - - MessageContext(MessageSource src, uint32_t id = 0) - : source(src), clientId(id) {} - }; - - explicit Communication(ConfigManager& configManager, - OTAManager& otaManager, - Networking& networking, - AsyncMqttClient& mqttClient, - AsyncWebServer& server, - AsyncWebSocket& webSocket, - AsyncUDP& udp); - - ~Communication(); - - void begin(); - void setPlayerReference(Player* player) { _player = player; } - void setFileManagerReference(FileManager* fm) { _fileManager = fm; } - void setTimeKeeperReference(Timekeeper* tk) { _timeKeeper = tk; } - void setFirmwareValidatorReference(FirmwareValidator* fv) { _firmwareValidator = fv; } - void setupUdpDiscovery(); - - // Public methods for timer callbacks - void connectToMqtt(); - void subscribeMqtt(); - - // Status methods - bool isMqttConnected() const { return _mqttClient.connected(); } - bool hasActiveWebSocketClients() const { return _clientManager.hasClients(); } - size_t getWebSocketClientCount() const { return _clientManager.getClientCount(); } - - // Response methods - unified response system - void sendResponse(const String& response, const MessageContext& context); - void sendSuccessResponse(const String& type, const String& payload, const MessageContext& context); - void sendErrorResponse(const String& type, const String& message, const MessageContext& context); - - // Broadcast methods - for status updates that go to everyone - void broadcastStatus(const String& statusMessage); - void broadcastStatus(const JsonDocument& statusJson); - void broadcastToMasterClients(const String& message); - void broadcastToSecondaryClients(const String& message); - void broadcastToAllWebSocketClients(const String& message); - void broadcastToAllWebSocketClients(const JsonDocument& message); - void publishToMqtt(const String& data); - - // ═══════════════════════════════════════════════════════════════════════════════ - // HEALTH CHECK METHOD - // ═══════════════════════════════════════════════════════════════════════════════ - - /** @brief Check if Communication is in healthy state */ - bool isHealthy() const; - - // Bell overload notification - void sendBellOverloadNotification(const std::vector& bellNumbers, - const std::vector& bellLoads, - const String& severity); - - // Network connection callbacks (called by Networking) - void onNetworkConnected(); - void onNetworkDisconnected(); - - // Static instance for callbacks - static Communication* _instance; - -private: - // Dependencies - ConfigManager& _configManager; - OTAManager& _otaManager; - Networking& _networking; - AsyncMqttClient& _mqttClient; - AsyncWebServer& _server; - AsyncWebSocket& _webSocket; - AsyncUDP& _udp; - Player* _player; - FileManager* _fileManager; - Timekeeper* _timeKeeper; - FirmwareValidator* _firmwareValidator; - - // Client manager - ClientManager _clientManager; - - // State - TimerHandle_t _mqttReconnectTimer; - - // Reusable JSON documents - static StaticJsonDocument<2048> _parseDocument; - - // MQTT methods - void initMqtt(); - static void onMqttConnect(bool sessionPresent); - static void onMqttDisconnect(AsyncMqttClientDisconnectReason reason); - static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, - size_t len, size_t index, size_t total); - static void onMqttSubscribe(uint16_t packetId, uint8_t qos); - static void onMqttUnsubscribe(uint16_t packetId); - static void onMqttPublish(uint16_t packetId); - - // WebSocket methods - void initWebSocket(); - static void onWebSocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, - AwsEventType type, void* arg, uint8_t* data, size_t len); - void onWebSocketConnect(AsyncWebSocketClient* client); - void onWebSocketDisconnect(AsyncWebSocketClient* client); - void onWebSocketReceived(AsyncWebSocketClient* client, void* arg, uint8_t* data, size_t len); - void handleClientIdentification(AsyncWebSocketClient* client, JsonDocument& command); - - // Command processing - unified for both MQTT and WebSocket with grouped commands - JsonDocument parsePayload(char* payload); - void handleCommand(JsonDocument& command, const MessageContext& context); - - // ═════════════════════════════════════════════════════════════════════════════════ - // GROUPED COMMAND HANDLERS - // ═════════════════════════════════════════════════════════════════════════════════ - - // System commands - void handleSystemCommand(JsonVariant contents, const MessageContext& context); - void handleSystemInfoCommand(JsonVariant contents, const MessageContext& context); - void handlePlaybackCommand(JsonVariant contents, const MessageContext& context); - void handleFileManagerCommand(JsonVariant contents, const MessageContext& context); - void handleRelaySetupCommand(JsonVariant contents, const MessageContext& context); - void handleClockSetupCommand(JsonVariant contents, const MessageContext& context); - - // System sub-commands - void handlePingCommand(const MessageContext& context); - void handleStatusCommand(const MessageContext& context); - void handleIdentifyCommand(JsonVariant contents, const MessageContext& context); - void handleGetDeviceTimeCommand(const MessageContext& context); - void handleGetClockTimeCommand(const MessageContext& context); - - // Firmware management commands - void handleCommitFirmwareCommand(const MessageContext& context); - void handleRollbackFirmwareCommand(const MessageContext& context); - void handleGetFirmwareStatusCommand(const MessageContext& context); - - // Network configuration command - void handleSetNetworkConfigCommand(JsonVariant contents, const MessageContext& context); - - // File Manager sub-commands - void handleListMelodiesCommand(const MessageContext& context); - void handleDownloadMelodyCommand(JsonVariant contents, const MessageContext& context); - void handleDeleteMelodyCommand(JsonVariant contents, const MessageContext& context); - - // Relay Setup sub-commands - void handleSetRelayTimersCommand(JsonVariant contents, const MessageContext& context); - void handleSetRelayOutputsCommand(JsonVariant contents, const MessageContext& context); - - // Clock Setup sub-commands - void handleSetClockOutputsCommand(JsonVariant contents, const MessageContext& context); - void handleSetClockTimingsCommand(JsonVariant contents, const MessageContext& context); - void handleSetClockAlertsCommand(JsonVariant contents, const MessageContext& context); - void handleSetClockBacklightCommand(JsonVariant contents, const MessageContext& context); - void handleSetClockSilenceCommand(JsonVariant contents, const MessageContext& context); - void handleSetRtcTimeCommand(JsonVariant contents, const MessageContext& context); - void handleSetPhysicalClockTimeCommand(JsonVariant contents, const MessageContext& context); - void handlePauseClockUpdatesCommand(JsonVariant contents, const MessageContext& context); - void handleSetClockEnabledCommand(JsonVariant contents, const MessageContext& context); - - // Utility methods - String getPayloadContent(char* data, size_t len); - int extractBellNumber(const String& key); // Extract bell number from "b1", "c1", etc. -}; diff --git a/vesper/src/Communication/CommunicationRouter/CommunicationRouter.cpp b/vesper/src/Communication/CommunicationRouter/CommunicationRouter.cpp new file mode 100644 index 0000000..26f26a9 --- /dev/null +++ b/vesper/src/Communication/CommunicationRouter/CommunicationRouter.cpp @@ -0,0 +1,300 @@ +/* + * COMMUNICATIONROUTER.CPP - Communication Router Implementation + */ + +#include "CommunicationRouter.hpp" +#include "../../ConfigManager/ConfigManager.hpp" +#include "../../OTAManager/OTAManager.hpp" +#include "../../Networking/Networking.hpp" +#include "../../Logging/Logging.hpp" +#include "../../Player/Player.hpp" +#include "../../FileManager/FileManager.hpp" +#include "../../TimeKeeper/TimeKeeper.hpp" +#include "../../FirmwareValidator/FirmwareValidator.hpp" + +CommunicationRouter::CommunicationRouter(ConfigManager& configManager, + OTAManager& otaManager, + Networking& networking, + AsyncWebServer& server, + AsyncWebSocket& webSocket, + AsyncUDP& udp) + : _configManager(configManager) + , _otaManager(otaManager) + , _networking(networking) + , _server(server) + , _webSocket(webSocket) + , _udp(udp) + , _player(nullptr) + , _fileManager(nullptr) + , _timeKeeper(nullptr) + , _firmwareValidator(nullptr) + , _mqttClient(configManager, networking) + , _clientManager() + , _wsServer(webSocket, _clientManager) + , _commandHandler(configManager, otaManager) {} + +CommunicationRouter::~CommunicationRouter() {} + +void CommunicationRouter::begin() { + LOG_INFO("Initializing Communication Router v4.0 (Modular)"); + + // 🔥 CRITICAL: Initialize WebSocket FIRST to ensure it's always set up + // Even if MQTT fails, we want WebSocket to work! + LOG_INFO("Setting up WebSocket server..."); + + // Initialize WebSocket server + _wsServer.begin(); + _wsServer.setCallback([this](uint32_t clientId, const JsonDocument& message) { + onWebSocketMessage(clientId, message); + }); + + // 🔥 CRITICAL FIX: Attach WebSocket handler to AsyncWebServer + // This MUST happen before any potential failures! + _server.addHandler(&_webSocket); + LOG_INFO("✅ WebSocket handler attached to AsyncWebServer on /ws"); + + //Now initialize MQTT client (can fail without breaking WebSocket) + try { + LOG_INFO("Setting up MQTT client..."); + _mqttClient.begin(); + _mqttClient.setCallback([this](const String& topic, const String& payload) { + onMqttMessage(topic, payload); + }); + LOG_INFO("✅ MQTT client initialized"); + } catch (...) { + LOG_ERROR("❌ MQTT initialization failed, but WebSocket is still available"); + } + + // 🔥 CRITICAL FIX: Connect ClientManager to CommandHandler + _commandHandler.setClientManagerReference(&_clientManager); + LOG_INFO("ClientManager reference set for CommandHandler"); + + // Setup command handler response callback + _commandHandler.setResponseCallback([this](const String& response, const CommandHandler::MessageContext& context) { + sendResponse(response, context); + }); + + LOG_INFO("Communication Router initialized with modular architecture"); +} + +void CommunicationRouter::setPlayerReference(Player* player) { + _player = player; + _commandHandler.setPlayerReference(player); +} + +void CommunicationRouter::setFileManagerReference(FileManager* fm) { + _fileManager = fm; + _commandHandler.setFileManagerReference(fm); +} + +void CommunicationRouter::setTimeKeeperReference(Timekeeper* tk) { + _timeKeeper = tk; + _commandHandler.setTimeKeeperReference(tk); +} + +void CommunicationRouter::setFirmwareValidatorReference(FirmwareValidator* fv) { + _firmwareValidator = fv; + _commandHandler.setFirmwareValidatorReference(fv); +} + +void CommunicationRouter::setupUdpDiscovery() { + uint16_t discoveryPort = _configManager.getNetworkConfig().discoveryPort; + if (_udp.listen(discoveryPort)) { + LOG_INFO("UDP discovery listening on port %u", discoveryPort); + + _udp.onPacket([this](AsyncUDPPacket packet) { + String msg = String((const char*)packet.data(), packet.length()); + LOG_DEBUG("UDP from %s:%u -> %s", + packet.remoteIP().toString().c_str(), + packet.remotePort(), + msg.c_str()); + + bool shouldReply = false; + + if (msg.indexOf("discover") >= 0) { + shouldReply = true; + } else { + StaticJsonDocument<128> req; + DeserializationError err = deserializeJson(req, msg); + if (!err) { + shouldReply = (req["op"] == "discover" && req["svc"] == "vesper"); + } + } + + if (!shouldReply) return; + + StaticJsonDocument<256> doc; + doc["op"] = "discover_reply"; + doc["svc"] = "vesper"; + doc["ver"] = 1; + + doc["name"] = "Proj. Vesper v2.0"; + doc["id"] = _configManager.getDeviceUID(); + doc["ip"] = _networking.getLocalIP(); + char wsUrl[64]; + snprintf(wsUrl, sizeof(wsUrl), "ws://%s/ws", _networking.getLocalIP().c_str()); + doc["ws"] = wsUrl; + doc["port"] = 80; + doc["fw"] = "2.0"; + doc["clients"] = _clientManager.getClientCount(); + + String out; + serializeJson(doc, out); + + _udp.writeTo((const uint8_t*)out.c_str(), out.length(), + packet.remoteIP(), packet.remotePort()); + }); + } else { + LOG_ERROR("Failed to start UDP discovery."); + } +} + +bool CommunicationRouter::isMqttConnected() const { + return _mqttClient.isConnected(); +} + +bool CommunicationRouter::hasActiveWebSocketClients() const { + return _wsServer.hasClients(); +} + +size_t CommunicationRouter::getWebSocketClientCount() const { + return _wsServer.getClientCount(); +} + +bool CommunicationRouter::isHealthy() const { + // Check if required references are set + if (!_player || !_fileManager || !_timeKeeper) { + LOG_DEBUG("CommunicationRouter: Unhealthy - Missing references"); + return false; + } + + // Check if at least one protocol is connected + if (!isMqttConnected() && !hasActiveWebSocketClients()) { + LOG_DEBUG("CommunicationRouter: Unhealthy - No active connections"); + return false; + } + + // Check network connectivity + if (!_networking.isConnected()) { + LOG_DEBUG("CommunicationRouter: Unhealthy - No network connection"); + return false; + } + + return true; +} + +void CommunicationRouter::broadcastStatus(const String& statusMessage) { + publishToMqtt(statusMessage); + _wsServer.broadcastToAll(statusMessage); +} + +void CommunicationRouter::broadcastStatus(const JsonDocument& statusJson) { + String statusMessage; + serializeJson(statusJson, statusMessage); + broadcastStatus(statusMessage); +} + +void CommunicationRouter::broadcastToMasterClients(const String& message) { + _wsServer.broadcastToMaster(message); +} + +void CommunicationRouter::broadcastToSecondaryClients(const String& message) { + _wsServer.broadcastToSecondary(message); +} + +void CommunicationRouter::broadcastToAllWebSocketClients(const String& message) { + _wsServer.broadcastToAll(message); +} + +void CommunicationRouter::broadcastToAllWebSocketClients(const JsonDocument& message) { + String messageStr; + serializeJson(message, messageStr); + _wsServer.broadcastToAll(messageStr); +} + +void CommunicationRouter::publishToMqtt(const String& data) { + if (_mqttClient.isConnected()) { + _mqttClient.publish("data", data, 0, false); + LOG_DEBUG("Published to MQTT: %s", data.c_str()); + } else { + LOG_ERROR("MQTT Not Connected! Message Failed: %s", data.c_str()); + } +} + +void CommunicationRouter::sendBellOverloadNotification(const std::vector& bellNumbers, + const std::vector& bellLoads, + const String& severity) { + StaticJsonDocument<512> overloadMsg; + overloadMsg["status"] = "INFO"; + overloadMsg["type"] = "bell_overload"; + + JsonArray bellsArray = overloadMsg["payload"]["bells"].to(); + JsonArray loadsArray = overloadMsg["payload"]["loads"].to(); + + for (size_t i = 0; i < bellNumbers.size() && i < bellLoads.size(); i++) { + bellsArray.add(bellNumbers[i] + 1); + loadsArray.add(bellLoads[i]); + } + + overloadMsg["payload"]["severity"] = severity; + broadcastStatus(overloadMsg); + + LOG_WARNING("Bell overload notification sent: %d bells, severity: %s", + bellNumbers.size(), severity.c_str()); +} + +void CommunicationRouter::onNetworkConnected() { + LOG_DEBUG("Network connected - notifying MQTT client"); + _mqttClient.onNetworkConnected(); +} + +void CommunicationRouter::onNetworkDisconnected() { + LOG_DEBUG("Network disconnected - notifying MQTT client"); + _mqttClient.onNetworkDisconnected(); +} + +void CommunicationRouter::onMqttMessage(const String& topic, const String& payload) { + LOG_DEBUG("MQTT message received: %s", payload.c_str()); + + // Parse JSON + StaticJsonDocument<2048> doc; + DeserializationError error = deserializeJson(doc, payload); + + if (error) { + LOG_ERROR("Failed to parse MQTT JSON: %s", error.c_str()); + return; + } + + // Create message context for MQTT + CommandHandler::MessageContext context(CommandHandler::MessageSource::MQTT); + + // Forward to command handler + _commandHandler.processCommand(doc, context); +} + +void CommunicationRouter::onWebSocketMessage(uint32_t clientId, const JsonDocument& message) { + // Extract command for logging + String cmd = message["cmd"] | "unknown"; + LOG_INFO("📨 WebSocket message from client #%u: cmd=%s", clientId, cmd.c_str()); + + // Create message context for WebSocket with client ID + CommandHandler::MessageContext context(CommandHandler::MessageSource::WEBSOCKET, clientId); + + // Forward to command handler (need to cast away const for now) + JsonDocument& mutableDoc = const_cast(message); + _commandHandler.processCommand(mutableDoc, context); + + LOG_DEBUG("WebSocket message from client #%u processed", clientId); +} + +void CommunicationRouter::sendResponse(const String& response, const CommandHandler::MessageContext& context) { + if (context.source == CommandHandler::MessageSource::MQTT) { + LOG_DEBUG("↗️ Sending response via MQTT: %s", response.c_str()); + publishToMqtt(response); + } else if (context.source == CommandHandler::MessageSource::WEBSOCKET) { + LOG_DEBUG("↗️ Sending response to WebSocket client #%u: %s", context.clientId, response.c_str()); + _wsServer.sendToClient(context.clientId, response); + } else { + LOG_ERROR("❌ Unknown message source for response routing!"); + } +} diff --git a/vesper/src/Communication/CommunicationRouter/CommunicationRouter.hpp b/vesper/src/Communication/CommunicationRouter/CommunicationRouter.hpp new file mode 100644 index 0000000..4e52d87 --- /dev/null +++ b/vesper/src/Communication/CommunicationRouter/CommunicationRouter.hpp @@ -0,0 +1,118 @@ +/* + * ═══════════════════════════════════════════════════════════════════════════════════ + * COMMUNICATIONROUTER.HPP - Multi-Protocol Communication Router v4.0 + * ═══════════════════════════════════════════════════════════════════════════════════ + * + * 📡 THE COMMUNICATION ROUTER OF VESPER 📡 + * + * Routes messages between protocols and command handlers: + * • MQTTAsyncClient: AsyncMqttClient for non-blocking MQTT + * • WebSocketServer: Multi-client WebSocket management + * • CommandHandler: Unified command processing + * • ResponseBuilder: Structured response generation + * + * 🏗️ ARCHITECTURE: + * • Message routing between protocols and handlers + * • MQTT on dedicated RTOS task (Core 0) + * • Unified command processing + * • Thread-safe message routing + * + * 📡 SUPPORTED PROTOCOLS: + * • MQTT: AsyncMqttClient for reliable async connectivity + * • WebSocket: Real-time multi-client web interface + * • UDP Discovery: Auto-discovery service + * + * 📋 VERSION: 5.0 (AsyncMqttClient) + * 📅 DATE: 2025-10-01 + * 👨‍💻 AUTHOR: Advanced Bell Systems + * ═══════════════════════════════════════════════════════════════════════════════════ + */ + +#pragma once + +#include +#include +#include +#include +#include "../MQTTAsyncClient/MQTTAsyncClient.hpp" +#include "../WebSocketServer/WebSocketServer.hpp" +#include "../CommandHandler/CommandHandler.hpp" +#include "../ResponseBuilder/ResponseBuilder.hpp" +#include "../../ClientManager/ClientManager.hpp" + +class ConfigManager; +class OTAManager; +class Player; +class FileManager; +class Timekeeper; +class Networking; +class FirmwareValidator; + +class CommunicationRouter { +public: + explicit CommunicationRouter(ConfigManager& configManager, + OTAManager& otaManager, + Networking& networking, + AsyncWebServer& server, + AsyncWebSocket& webSocket, + AsyncUDP& udp); + + ~CommunicationRouter(); + + void begin(); + void setPlayerReference(Player* player); + void setFileManagerReference(FileManager* fm); + void setTimeKeeperReference(Timekeeper* tk); + void setFirmwareValidatorReference(FirmwareValidator* fv); + void setupUdpDiscovery(); + + // Status methods + bool isMqttConnected() const; + bool hasActiveWebSocketClients() const; + size_t getWebSocketClientCount() const; + bool isHealthy() const; + + // Broadcast methods + void broadcastStatus(const String& statusMessage); + void broadcastStatus(const JsonDocument& statusJson); + void broadcastToMasterClients(const String& message); + void broadcastToSecondaryClients(const String& message); + void broadcastToAllWebSocketClients(const String& message); + void broadcastToAllWebSocketClients(const JsonDocument& message); + void publishToMqtt(const String& data); + + // Bell overload notification + void sendBellOverloadNotification(const std::vector& bellNumbers, + const std::vector& bellLoads, + const String& severity); + + // Network connection callbacks + void onNetworkConnected(); + void onNetworkDisconnected(); + +private: + // Dependencies + ConfigManager& _configManager; + OTAManager& _otaManager; + Networking& _networking; + AsyncWebServer& _server; + AsyncWebSocket& _webSocket; + AsyncUDP& _udp; + Player* _player; + FileManager* _fileManager; + Timekeeper* _timeKeeper; + FirmwareValidator* _firmwareValidator; + + // Communication subsystems + MQTTAsyncClient _mqttClient; + ClientManager _clientManager; + WebSocketServer _wsServer; + CommandHandler _commandHandler; + + // Message handlers + void onMqttMessage(const String& topic, const String& payload); + void onWebSocketMessage(uint32_t clientId, const JsonDocument& message); + + // Response routing + void sendResponse(const String& response, const CommandHandler::MessageContext& context); +}; diff --git a/vesper/src/Communication/MQTTAsyncClient/MQTTAsyncClient.cpp b/vesper/src/Communication/MQTTAsyncClient/MQTTAsyncClient.cpp new file mode 100644 index 0000000..2660042 --- /dev/null +++ b/vesper/src/Communication/MQTTAsyncClient/MQTTAsyncClient.cpp @@ -0,0 +1,240 @@ +/* + * MQTTASYNCCLIENT.CPP - MQTT Client Implementation with AsyncMqttClient + */ + +#include "MQTTAsyncClient.hpp" +#include "../../ConfigManager/ConfigManager.hpp" +#include "../../Networking/Networking.hpp" +#include "../../Logging/Logging.hpp" + +MQTTAsyncClient* MQTTAsyncClient::_instance = nullptr; + +MQTTAsyncClient::MQTTAsyncClient(ConfigManager& configManager, Networking& networking) + : _configManager(configManager) + , _networking(networking) + , _messageCallback(nullptr) + , _mqttReconnectTimer(nullptr) { + + _instance = this; // Set static instance pointer + + // Create reconnection timer + _mqttReconnectTimer = xTimerCreate( + "mqttReconnect", // Timer name (for debugging) + pdMS_TO_TICKS(MQTT_RECONNECT_DELAY), // Period: 5000ms = 5 seconds + pdFALSE, // One-shot (false) or Auto-reload (true) + (void*)0, // Timer ID (can store data) + mqttReconnectTimerCallback // Callback function when timer expires + ); + +} + +MQTTAsyncClient::~MQTTAsyncClient() { + if (_mqttReconnectTimer) { + xTimerDelete(_mqttReconnectTimer, portMAX_DELAY); + } + _mqttClient.disconnect(); +} + +void MQTTAsyncClient::begin() { + LOG_INFO("Initializing MQTT Async Client"); + + auto& mqttConfig = _configManager.getMqttConfig(); + + // Build topic strings (cache for performance) + String deviceUID = _configManager.getDeviceUID(); + _controlTopic = "vesper/" + deviceUID + "/control"; + _dataTopic = "vesper/" + deviceUID + "/data"; + _clientId = "vesper-" + deviceUID; + + LOG_INFO("MQTT Topics: control=%s, data=%s", _controlTopic.c_str(), _dataTopic.c_str()); + + // Setup event handlers + _mqttClient.onConnect([this](bool sessionPresent) { + this->onMqttConnect(sessionPresent); + }); + + _mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { + this->onMqttDisconnect(reason); + }); + + _mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos) { + this->onMqttSubscribe(packetId, qos); + }); + + _mqttClient.onUnsubscribe([this](uint16_t packetId) { + this->onMqttUnsubscribe(packetId); + }); + + _mqttClient.onMessage([this](char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { + this->onMqttMessage(topic, payload, properties, len, index, total); + }); + + _mqttClient.onPublish([this](uint16_t packetId) { + this->onMqttPublish(packetId); + }); + + // Configure connection + _mqttClient.setServer(mqttConfig.host, mqttConfig.port); + _mqttClient.setCredentials(mqttConfig.user.c_str(), mqttConfig.password.c_str()); + _mqttClient.setClientId(_clientId.c_str()); // Use member variable + _mqttClient.setKeepAlive(15); + _mqttClient.setCleanSession(true); + + LOG_INFO("✅ MQTT Async Client initialized"); +} + +void MQTTAsyncClient::connect() { + if (_mqttClient.connected()) { + LOG_DEBUG("Already connected to MQTT"); + return; + } + + auto& mqttConfig = _configManager.getMqttConfig(); + + LOG_INFO("Free heap BEFORE MQTT connect: %d bytes", ESP.getFreeHeap()); + + _mqttClient.connect(); + + LOG_INFO("MQTT connect() called - waiting for async connection..."); +} + +void MQTTAsyncClient::disconnect() { + _mqttClient.disconnect(); + LOG_INFO("Disconnected from MQTT broker"); +} + +uint16_t MQTTAsyncClient::publish(const String& topic, const String& payload, int qos, bool retain) { + // Build full topic (if relative) + String fullTopic = topic.startsWith("vesper/") ? topic : _dataTopic; + + uint16_t packetId = _mqttClient.publish(fullTopic.c_str(), qos, retain, payload.c_str()); + + if (packetId > 0) { + LOG_DEBUG("Published to %s: %s (packetId=%d)", fullTopic.c_str(), payload.c_str(), packetId); + } else { + LOG_ERROR("Failed to publish to %s", fullTopic.c_str()); + } + + return packetId; +} + +void MQTTAsyncClient::setCallback(MessageCallback callback) { + _messageCallback = callback; +} + +bool MQTTAsyncClient::isConnected() const { + return _mqttClient.connected(); +} + +void MQTTAsyncClient::onNetworkConnected() { + LOG_DEBUG("Network connected - waiting 2 seconds for network stack to stabilize..."); + + // Small delay to ensure network stack is fully ready + delay(2000); + + LOG_DEBUG("Network stable - connecting to MQTT"); + connect(); +} + +void MQTTAsyncClient::onNetworkDisconnected() { + LOG_DEBUG("Network disconnected - MQTT will auto-reconnect when network returns"); + + if (_mqttClient.connected()) { + _mqttClient.disconnect(true); + } +} + +void MQTTAsyncClient::subscribe() { + uint16_t packetId = _mqttClient.subscribe(_controlTopic.c_str(), 0); + LOG_INFO("📬 Subscribing to control topic: %s (packetId=%d)", _controlTopic.c_str(), packetId); +} + +void MQTTAsyncClient::onMqttConnect(bool sessionPresent) { + LOG_INFO("✅ Connected to MQTT broker (session present: %s)", sessionPresent ? "yes" : "no"); + LOG_INFO("🔍 Free heap AFTER MQTT connect: %d bytes", ESP.getFreeHeap()); + + // Subscribe to control topic + subscribe(); +} + +void MQTTAsyncClient::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) { + const char* reasonStr; + switch(reason) { + case AsyncMqttClientDisconnectReason::TCP_DISCONNECTED: + reasonStr = "TCP disconnected"; + break; + case AsyncMqttClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: + reasonStr = "Unacceptable protocol version"; + break; + case AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED: + reasonStr = "Identifier rejected"; + break; + case AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE: + reasonStr = "Server unavailable"; + break; + case AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS: + reasonStr = "Malformed credentials"; + break; + case AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED: + reasonStr = "Not authorized"; + break; + default: + reasonStr = "Unknown"; + break; + } + + LOG_ERROR("❌ Disconnected from MQTT broker - Reason: %s (%d)", reasonStr, static_cast(reason)); + + if (_networking.isConnected()) { + LOG_INFO("Network still connected - scheduling MQTT reconnection in %d seconds", MQTT_RECONNECT_DELAY / 1000); + xTimerStart(_mqttReconnectTimer, 0); + } else { + LOG_INFO("Network is down - waiting for network to reconnect"); + } + +} + +void MQTTAsyncClient::onMqttSubscribe(uint16_t packetId, uint8_t qos) { + LOG_INFO("✅ Subscribed to topic (packetId=%d, QoS=%d)", packetId, qos); +} + +void MQTTAsyncClient::onMqttUnsubscribe(uint16_t packetId) { + LOG_DEBUG("Unsubscribed from topic (packetId=%d)", packetId); +} + +void MQTTAsyncClient::onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { + // Convert to String + String topicStr = String(topic); + String payloadStr = String(payload).substring(0, len); + + LOG_DEBUG("MQTT message received - topic: %s, payload: %s", topicStr.c_str(), payloadStr.c_str()); + + // Call user callback + if (_messageCallback) { + _messageCallback(topicStr, payloadStr); + } +} + +void MQTTAsyncClient::onMqttPublish(uint16_t packetId) { + LOG_DEBUG("MQTT publish acknowledged (packetId=%d)", packetId); +} + +void MQTTAsyncClient::attemptReconnection() { + // Double-check network is still up + if (_networking.isConnected()) { + LOG_INFO("Attempting MQTT reconnection..."); + connect(); + } else { + LOG_WARNING("Network down during reconnect attempt - aborting"); + } +} + +void MQTTAsyncClient::mqttReconnectTimerCallback(TimerHandle_t xTimer) { + // Get the MQTT instance from the timer's ID (set during timer creation) + // For now, we'll use a static instance pointer (similar to Networking) + // You'll need to add: static MQTTAsyncClient* _instance; to header + + if (MQTTAsyncClient::_instance) { + MQTTAsyncClient::_instance->attemptReconnection(); + } +} \ No newline at end of file diff --git a/vesper/src/Communication/MQTTAsyncClient/MQTTAsyncClient.hpp b/vesper/src/Communication/MQTTAsyncClient/MQTTAsyncClient.hpp new file mode 100644 index 0000000..db42fa1 --- /dev/null +++ b/vesper/src/Communication/MQTTAsyncClient/MQTTAsyncClient.hpp @@ -0,0 +1,116 @@ +/* + * ═══════════════════════════════════════════════════════════════════════════════════ + * MQTTASYNCCLIENT.HPP - MQTT Client with AsyncMqttClient + * ═══════════════════════════════════════════════════════════════════════════════════ + * + * 🔒 MQTT CONNECTION WITH ASYNC ARCHITECTURE 🔒 + * + * This class manages MQTT connections using AsyncMqttClient library: + * • Fully async/non-blocking operation + * • No conflicts with AsyncWebServer + * • Auto-reconnection built-in + * • SSL/TLS support (optional) + * • Perfect for Mosquitto broker + * + * 📋 VERSION: 3.0 (AsyncMqttClient-based) + * 📅 DATE: 2025-01-04 + * 👨‍💻 AUTHOR: Advanced Bell Systems + * ═══════════════════════════════════════════════════════════════════════════════════ + */ + +#pragma once + +#include +#include + +class ConfigManager; +class Networking; + +class MQTTAsyncClient { +public: + static MQTTAsyncClient* _instance; + + // Message callback type + using MessageCallback = std::function; + + explicit MQTTAsyncClient(ConfigManager& configManager, Networking& networking); + ~MQTTAsyncClient(); + + /** + * @brief Initialize MQTT client + */ + void begin(); + + /** + * @brief Connect to MQTT broker + */ + void connect(); + + /** + * @brief Disconnect from MQTT broker + */ + void disconnect(); + + /** + * @brief Publish message to MQTT + * @param topic Topic to publish to (relative, will prepend "vesper/{deviceID}/") + * @param payload Message payload + * @param qos QoS level (0, 1, or 2) + * @param retain Retain flag + * @return Packet ID (0 if failed) + */ + uint16_t publish(const String& topic, const String& payload, int qos = 0, bool retain = false); + + /** + * @brief Set message received callback + */ + void setCallback(MessageCallback callback); + + /** + * @brief Check if connected to MQTT broker + */ + bool isConnected() const; + + /** + * @brief Handle network connection callback + */ + void onNetworkConnected(); + + /** + * @brief Handle network disconnection callback + */ + void onNetworkDisconnected(); + +private: + ConfigManager& _configManager; + Networking& _networking; + AsyncMqttClient _mqttClient; + MessageCallback _messageCallback; + + + // Device topic strings (cached for performance) + String _controlTopic; + String _dataTopic; + String _clientId; // Store client ID to keep it alive + + /** + * @brief Subscribe to control topic + */ + void subscribe(); + + /** + * @brief MQTT event handlers + */ + void onMqttConnect(bool sessionPresent); + void onMqttDisconnect(AsyncMqttClientDisconnectReason reason); + void onMqttSubscribe(uint16_t packetId, uint8_t qos); + void onMqttUnsubscribe(uint16_t packetId); + void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total); + void onMqttPublish(uint16_t packetId); + + // Reconnection Timer + TimerHandle_t _mqttReconnectTimer; + static const unsigned long MQTT_RECONNECT_DELAY = 5000; // 5 seconds + void attemptReconnection(); + static void mqttReconnectTimerCallback(TimerHandle_t xTimer); +}; diff --git a/vesper/src/Communication/ResponseBuilder.cpp b/vesper/src/Communication/ResponseBuilder/ResponseBuilder.cpp similarity index 99% rename from vesper/src/Communication/ResponseBuilder.cpp rename to vesper/src/Communication/ResponseBuilder/ResponseBuilder.cpp index d1d0e8d..3923bf1 100644 --- a/vesper/src/Communication/ResponseBuilder.cpp +++ b/vesper/src/Communication/ResponseBuilder/ResponseBuilder.cpp @@ -1,5 +1,5 @@ #include "ResponseBuilder.hpp" -#include "../Logging/Logging.hpp" +#include "../../Logging/Logging.hpp" // Static member initialization StaticJsonDocument<512> ResponseBuilder::_responseDoc; diff --git a/vesper/src/Communication/ResponseBuilder.hpp b/vesper/src/Communication/ResponseBuilder/ResponseBuilder.hpp similarity index 98% rename from vesper/src/Communication/ResponseBuilder.hpp rename to vesper/src/Communication/ResponseBuilder/ResponseBuilder.hpp index 393075d..aa4cfc0 100644 --- a/vesper/src/Communication/ResponseBuilder.hpp +++ b/vesper/src/Communication/ResponseBuilder/ResponseBuilder.hpp @@ -43,7 +43,7 @@ #include #include -#include "../Player/Player.hpp" // For PlayerStatus enum +#include "../../Player/Player.hpp" // For PlayerStatus enum class ResponseBuilder { public: diff --git a/vesper/src/Communication/WebSocketServer/WebSocketServer.cpp b/vesper/src/Communication/WebSocketServer/WebSocketServer.cpp new file mode 100644 index 0000000..7fa8420 --- /dev/null +++ b/vesper/src/Communication/WebSocketServer/WebSocketServer.cpp @@ -0,0 +1,157 @@ +/* + * WEBSOCKETSERVER.CPP - WebSocket Server Implementation + */ + +#include "WebSocketServer.hpp" +#include "../../Logging/Logging.hpp" +#include "../ResponseBuilder/ResponseBuilder.hpp" + +// Static instance for callback +WebSocketServer* WebSocketServer::_instance = nullptr; + +WebSocketServer::WebSocketServer(AsyncWebSocket& webSocket, ClientManager& clientManager) + : _webSocket(webSocket) + , _clientManager(clientManager) + , _messageCallback(nullptr) { + + _instance = this; +} + +WebSocketServer::~WebSocketServer() { + _instance = nullptr; +} + +void WebSocketServer::begin() { + _webSocket.onEvent(onEvent); + LOG_INFO("WebSocket server initialized on /ws"); + + // 🔥 CRITICAL: This line was missing - attach WebSocket to the AsyncWebServer + // Without this, the server doesn't know about the WebSocket handler! + // Note: We can't access _server here directly, so this must be done in CommunicationRouter +} + +void WebSocketServer::setCallback(MessageCallback callback) { + _messageCallback = callback; +} + +void WebSocketServer::sendToClient(uint32_t clientId, const String& message) { + _clientManager.sendToClient(clientId, message); +} + +void WebSocketServer::broadcastToAll(const String& message) { + _clientManager.broadcastToAll(message); + LOG_DEBUG("Broadcast to all WebSocket clients: %s", message.c_str()); +} + +void WebSocketServer::broadcastToMaster(const String& message) { + _clientManager.sendToMasterClients(message); + LOG_DEBUG("Broadcast to master clients: %s", message.c_str()); +} + +void WebSocketServer::broadcastToSecondary(const String& message) { + _clientManager.sendToSecondaryClients(message); + LOG_DEBUG("Broadcast to secondary clients: %s", message.c_str()); +} + +bool WebSocketServer::hasClients() const { + return _clientManager.hasClients(); +} + +size_t WebSocketServer::getClientCount() const { + return _clientManager.getClientCount(); +} + +void WebSocketServer::onEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, + AwsEventType type, void* arg, uint8_t* data, size_t len) { + if (!_instance) { + LOG_ERROR("WebSocketServer static instance is NULL - callback ignored!"); + return; + } + + switch (type) { + case WS_EVT_CONNECT: + _instance->onConnect(client); + break; + + case WS_EVT_DISCONNECT: + _instance->onDisconnect(client); + break; + + case WS_EVT_DATA: + _instance->onData(client, arg, data, len); + break; + + case WS_EVT_ERROR: + LOG_ERROR("WebSocket client #%u error(%u): %s", + client->id(), *((uint16_t*)arg), (char*)data); + break; + + default: + break; + } +} + +void WebSocketServer::onConnect(AsyncWebSocketClient* client) { + LOG_INFO("WebSocket client #%u connected from %s", + client->id(), client->remoteIP().toString().c_str()); + + // Add client to manager (type UNKNOWN until they identify) + _clientManager.addClient(client, ClientManager::DeviceType::UNKNOWN); + + // Send welcome message + String welcomeMsg = ResponseBuilder::success("connection", "Connected to Vesper"); + _clientManager.sendToClient(client->id(), welcomeMsg); +} + +void WebSocketServer::onDisconnect(AsyncWebSocketClient* client) { + LOG_INFO("WebSocket client #%u disconnected", 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; + } + + memcpy(payload, data, len); + payload[len] = '\0'; + + LOG_DEBUG("WebSocket client #%u sent: %s", client->id(), payload); + + // Parse JSON + StaticJsonDocument<2048> doc; + DeserializationError error = deserializeJson(doc, payload); + + if (error) { + LOG_ERROR("Failed to parse WebSocket JSON from client #%u: %s", client->id(), error.c_str()); + String errorResponse = ResponseBuilder::error("parse_error", "Invalid JSON"); + _clientManager.sendToClient(client->id(), errorResponse); + } else { + // Update client last seen time + _clientManager.updateClientLastSeen(client->id()); + + // Call user callback if set + if (_messageCallback) { + LOG_DEBUG("Routing message from client #%u to callback handler", client->id()); + _messageCallback(client->id(), 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 new file mode 100644 index 0000000..3af8085 --- /dev/null +++ b/vesper/src/Communication/WebSocketServer/WebSocketServer.hpp @@ -0,0 +1,103 @@ +/* + * ═══════════════════════════════════════════════════════════════════════════════════ + * WEBSOCKETSERVER.HPP - WebSocket Server Manager + * ═══════════════════════════════════════════════════════════════════════════════════ + * + * 📡 WEBSOCKET MULTI-CLIENT MANAGER 📡 + * + * Handles WebSocket connections with: + * • Multi-client support with device type identification + * • Automatic client cleanup + * • Broadcast and targeted messaging + * • Integration with ClientManager + * + * 📋 VERSION: 1.0 + * 📅 DATE: 2025-10-01 + * 👨‍💻 AUTHOR: Advanced Bell Systems + * ═══════════════════════════════════════════════════════════════════════════════════ + */ + +#pragma once + +#include +#include +#include +#include "../../ClientManager/ClientManager.hpp" + +class WebSocketServer { +public: + // Message callback type + using MessageCallback = std::function; + + explicit WebSocketServer(AsyncWebSocket& webSocket, ClientManager& clientManager); + ~WebSocketServer(); + + /** + * @brief Initialize WebSocket server + */ + void begin(); + + /** + * @brief Set message received callback + */ + void setCallback(MessageCallback callback); + + /** + * @brief Send message to specific client + */ + void sendToClient(uint32_t clientId, const String& message); + + /** + * @brief Broadcast to all connected clients + */ + void broadcastToAll(const String& message); + + /** + * @brief Broadcast to master devices only + */ + void broadcastToMaster(const String& message); + + /** + * @brief Broadcast to secondary devices only + */ + void broadcastToSecondary(const String& message); + + /** + * @brief Check if any clients are connected + */ + bool hasClients() const; + + /** + * @brief Get number of connected clients + */ + size_t getClientCount() const; + +private: + AsyncWebSocket& _webSocket; + ClientManager& _clientManager; + MessageCallback _messageCallback; + + /** + * @brief Static WebSocket event handler + */ + static void onEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, + AwsEventType type, void* arg, uint8_t* data, size_t len); + + /** + * @brief Handle client connection + */ + void onConnect(AsyncWebSocketClient* client); + + /** + * @brief Handle client disconnection + */ + void onDisconnect(AsyncWebSocketClient* client); + + /** + * @brief Handle received data + */ + void onData(AsyncWebSocketClient* client, void* arg, uint8_t* data, size_t len); + + // Static instance for callback routing + static WebSocketServer* _instance; +}; diff --git a/vesper/src/ConfigManager/ConfigManager.cpp b/vesper/src/ConfigManager/ConfigManager.cpp index 58f087a..2d3c95b 100644 --- a/vesper/src/ConfigManager/ConfigManager.cpp +++ b/vesper/src/ConfigManager/ConfigManager.cpp @@ -44,7 +44,7 @@ void ConfigManager::createDefaultBellConfig() { // Initialize default durations (90ms for all bells) for (uint8_t i = 0; i < 16; i++) { bellConfig.durations[i] = 90; - bellConfig.outputs[i] = i; // Direct mapping by default + bellConfig.outputs[i] = i + 1; // 1-indexed mapping by default } } @@ -70,7 +70,8 @@ bool ConfigManager::begin() { // Step 4: Load device configuration from SD card (firmware version only) if (!loadDeviceConfig()) { - LOG_WARNING("ConfigManager: Could not load device config from SD card - using defaults"); + LOG_INFO("ConfigManager: Creating default device config file"); + saveDeviceConfig(); } // Step 5: Load update servers list @@ -78,12 +79,32 @@ bool ConfigManager::begin() { LOG_WARNING("ConfigManager: Could not load update servers - using fallback only"); } - // Step 6: Load user-configurable settings from SD + // Step 6: Load user-configurable settings from SD (and create if missing) loadFromSD(); - loadNetworkConfig(); // Load network configuration (hostname, static IP settings) - loadBellDurations(); - loadClockConfig(); // Load clock configuration (C1/C2 outputs, pulse durations) - loadClockState(); // Load physical clock state (hour, minute, position) + + // Load network config, save defaults if not found + if (!loadNetworkConfig()) { + LOG_INFO("ConfigManager: Creating default network config file"); + saveNetworkConfig(); + } + + // Load bell durations, save defaults if not found + if (!loadBellDurations()) { + LOG_INFO("ConfigManager: Creating default bell durations file"); + saveBellDurations(); + } + + // Load clock config, save defaults if not found + if (!loadClockConfig()) { + LOG_INFO("ConfigManager: Creating default clock config file"); + saveClockConfig(); + } + + // Load clock state, save defaults if not found + if (!loadClockState()) { + LOG_INFO("ConfigManager: Creating default clock state file"); + saveClockState(); + } LOG_INFO("ConfigManager: Initialization complete - UID: %s, Hostname: %s", deviceConfig.deviceUID.c_str(), networkConfig.hostname.c_str()); @@ -123,42 +144,35 @@ bool ConfigManager::loadDeviceIdentityFromNVS() { return false; } - deviceConfig.deviceUID = readNVSString(NVS_DEVICE_UID_KEY, "PV000000000000"); - deviceConfig.hwType = readNVSString(NVS_HW_TYPE_KEY, "BellSystems"); - deviceConfig.hwVersion = readNVSString(NVS_HW_VERSION_KEY, "0"); + // Read factory-set device identity from NVS (READ-ONLY) + deviceConfig.deviceUID = readNVSString(NVS_DEVICE_UID_KEY, ""); + deviceConfig.hwType = readNVSString(NVS_HW_TYPE_KEY, ""); + deviceConfig.hwVersion = readNVSString(NVS_HW_VERSION_KEY, ""); - LOG_INFO("ConfigManager: Device identity loaded from NVS - UID: %s, Type: %s, Version: %s", - deviceConfig.deviceUID.c_str(), - deviceConfig.hwType.c_str(), - deviceConfig.hwVersion.c_str()); + // Validate that factory identity exists + if (deviceConfig.deviceUID.isEmpty() || deviceConfig.hwType.isEmpty() || deviceConfig.hwVersion.isEmpty()) { + LOG_ERROR("═══════════════════════════════════════════════════════════════════════════"); + LOG_ERROR(" ⚠️ CRITICAL: DEVICE IDENTITY NOT FOUND IN NVS"); + LOG_ERROR(" ⚠️ This device has NOT been factory-programmed!"); + LOG_ERROR(" ⚠️ Please flash factory firmware to set device identity"); + LOG_ERROR("═══════════════════════════════════════════════════════════════════════════"); + return false; + } + + LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); + LOG_INFO(" 🏭 FACTORY DEVICE IDENTITY LOADED FROM NVS (READ-ONLY)"); + LOG_INFO(" 🆔 Device UID: %s", deviceConfig.deviceUID.c_str()); + LOG_INFO(" 🔧 Hardware Type: %s", deviceConfig.hwType.c_str()); + LOG_INFO(" 📐 Hardware Version: %s", deviceConfig.hwVersion.c_str()); + LOG_INFO(" 🔒 These values are PERMANENT and cannot be changed by production firmware"); + LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); return true; } -bool ConfigManager::saveDeviceIdentityToNVS() { - if (nvsHandle == 0) { - LOG_ERROR("ConfigManager: NVS not initialized, cannot save device identity"); - return false; - } - - bool success = true; - success &= writeNVSString(NVS_DEVICE_UID_KEY, deviceConfig.deviceUID); - success &= writeNVSString(NVS_HW_TYPE_KEY, deviceConfig.hwType); - success &= writeNVSString(NVS_HW_VERSION_KEY, deviceConfig.hwVersion); - - if (success) { - esp_err_t err = nvs_commit(nvsHandle); - if (err != ESP_OK) { - LOG_ERROR("ConfigManager: Failed to commit NVS changes: %s", esp_err_to_name(err)); - return false; - } - LOG_INFO("ConfigManager: Device identity saved to NVS"); - } else { - LOG_ERROR("ConfigManager: Failed to save device identity to NVS"); - } - - return success; -} +// REMOVED: saveDeviceIdentityToNVS() - Production firmware MUST NOT write device identity +// Device identity (UID, hwType, hwVersion) is factory-set ONLY and stored in NVS by factory firmware +// Production firmware reads these values once at boot and keeps them in RAM String ConfigManager::readNVSString(const char* key, const String& defaultValue) { if (nvsHandle == 0) { @@ -195,21 +209,8 @@ String ConfigManager::readNVSString(const char* key, const String& defaultValue) return result; } -bool ConfigManager::writeNVSString(const char* key, const String& value) { - if (nvsHandle == 0) { - LOG_ERROR("ConfigManager: NVS not initialized, cannot write key: %s", key); - return false; - } - - esp_err_t err = nvs_set_str(nvsHandle, key, value.c_str()); - if (err != ESP_OK) { - LOG_ERROR("ConfigManager: Failed to write NVS key '%s': %s", key, esp_err_to_name(err)); - return false; - } - - LOG_DEBUG("ConfigManager: Written NVS key '%s': %s", key, value.c_str()); - return true; -} +// REMOVED: writeNVSString() - Production firmware MUST NOT write to NVS +// All device identity is factory-set and read-only in production firmware // ════════════════════════════════════════════════════════════════════════════ // STANDARD SD CARD FUNCTIONALITY @@ -327,7 +328,7 @@ bool ConfigManager::loadBellDurations() { File file = SD.open("/settings/relayTimings.json", FILE_READ); if (!file) { - LOG_ERROR("ConfigManager: Settings file not found on SD. Using default bell durations."); + LOG_WARNING("ConfigManager: Settings file not found on SD. Using default bell durations."); return false; } @@ -681,8 +682,9 @@ void ConfigManager::updateTimeConfig(long gmtOffsetSec, int daylightOffsetSec) { gmtOffsetSec, daylightOffsetSec); } -void ConfigManager::updateNetworkConfig(bool useStaticIP, IPAddress ip, IPAddress gateway, +void ConfigManager::updateNetworkConfig(const String& hostname, bool useStaticIP, IPAddress ip, IPAddress gateway, IPAddress subnet, IPAddress dns1, IPAddress dns2) { + networkConfig.hostname = hostname; networkConfig.useStaticIP = useStaticIP; networkConfig.ip = ip; networkConfig.gateway = gateway; @@ -690,8 +692,8 @@ void ConfigManager::updateNetworkConfig(bool useStaticIP, IPAddress ip, IPAddres networkConfig.dns1 = dns1; networkConfig.dns2 = dns2; saveNetworkConfig(); // Save immediately to SD - LOG_INFO("ConfigManager: NetworkConfig updated - Static IP: %s, IP: %s", - useStaticIP ? "enabled" : "disabled", ip.toString().c_str()); + LOG_INFO("ConfigManager: NetworkConfig updated - Hostname: %s, Static IP: %s, IP: %s", + hostname.c_str(), useStaticIP ? "enabled" : "disabled", ip.toString().c_str()); } // ════════════════════════════════════════════════════════════════════════════ @@ -802,96 +804,28 @@ bool ConfigManager::saveNetworkConfig() { } // ═══════════════════════════════════════════════════════════════════════════════ -// FACTORY RESET IMPLEMENTATION +// SETTINGS RESET IMPLEMENTATION // ═══════════════════════════════════════════════════════════════════════════════ -bool ConfigManager::factoryReset() { - LOG_WARNING("═══════════════════════════════════════════════════════════════════════════"); - LOG_WARNING("🏭 FACTORY RESET INITIATED"); - LOG_WARNING("═══════════════════════════════════════════════════════════════════════════"); - - if (!ensureSDCard()) { - LOG_ERROR("❌ ConfigManager: Cannot perform factory reset - SD card not available"); - return false; - } - - // Step 1: Delete all configuration files - LOG_INFO("🗑️ Step 1: Deleting all configuration files from SD card..."); - bool deleteSuccess = clearAllSettings(); - - if (!deleteSuccess) { - LOG_ERROR("❌ ConfigManager: Factory reset failed - could not delete all settings"); - return false; - } - - // Step 2: Reset in-memory configuration to defaults - LOG_INFO("🔄 Step 2: Resetting in-memory configuration to defaults..."); - - // Reset network config (keep device-generated values) - networkConfig.useStaticIP = false; - networkConfig.ip = IPAddress(0, 0, 0, 0); - networkConfig.gateway = IPAddress(0, 0, 0, 0); - networkConfig.subnet = IPAddress(0, 0, 0, 0); - networkConfig.dns1 = IPAddress(0, 0, 0, 0); - networkConfig.dns2 = IPAddress(0, 0, 0, 0); - // hostname and apSsid are auto-generated from deviceUID, keep them - - // Reset time config - timeConfig.gmtOffsetSec = 0; - timeConfig.daylightOffsetSec = 0; - - // Reset bell config - createDefaultBellConfig(); - - // Reset clock config to defaults - clockConfig.c1output = 255; - clockConfig.c2output = 255; - clockConfig.pulseDuration = 5000; - clockConfig.pauseDuration = 2000; - clockConfig.physicalHour = 0; - clockConfig.physicalMinute = 0; - clockConfig.nextOutputIsC1 = true; - clockConfig.lastSyncTime = 0; - clockConfig.alertType = "OFF"; - clockConfig.alertRingInterval = 1200; - clockConfig.hourBell = 255; - clockConfig.halfBell = 255; - clockConfig.quarterBell = 255; - clockConfig.backlight = false; - clockConfig.backlightOutput = 255; - clockConfig.backlightOnTime = "18:00"; - clockConfig.backlightOffTime = "06:00"; - clockConfig.daytimeSilenceEnabled = false; - clockConfig.daytimeSilenceOnTime = "14:00"; - clockConfig.daytimeSilenceOffTime = "17:00"; - clockConfig.nighttimeSilenceEnabled = false; - clockConfig.nighttimeSilenceOnTime = "22:00"; - clockConfig.nighttimeSilenceOffTime = "07:00"; - - // Note: Device identity (deviceUID, hwType, hwVersion) in NVS is NOT reset - // Note: WiFi credentials are handled by WiFiManager, not reset here - - LOG_INFO("✅ Step 2: In-memory configuration reset to defaults"); - - LOG_WARNING("✅ FACTORY RESET COMPLETE"); - LOG_WARNING("🔄 Device will boot with default settings on next restart"); - LOG_WARNING("🆔 Device identity (UID) preserved in NVS"); - LOG_INFO("WiFi credentials should be cleared separately using WiFiManager"); - - return true; -} +bool ConfigManager::resetAllToDefaults() { + + LOG_WARNING("═══════════════════════════════════════════════════════════════════════════"); + LOG_WARNING(" 🏭 RESET SETTINGS TO DEFAULTS INITIATED"); + LOG_WARNING("═══════════════════════════════════════════════════════════════════════════"); -bool ConfigManager::clearAllSettings() { if (!ensureSDCard()) { - LOG_ERROR("ConfigManager: SD card not available for clearing settings"); + LOG_ERROR("❌ ConfigManager: Cannot perform reset - SD card not available"); return false; } - + bool allDeleted = true; int filesDeleted = 0; int filesFailed = 0; - // List of all configuration files to delete + // ════════════════════════════════════════════════════════════════════════════ + // STEP 1: Delete all configuration files + // ════════════════════════════════════════════════════════════════════════════ + const char* settingsFiles[] = { "/settings/deviceConfig.json", "/settings/networkConfig.json", @@ -903,15 +837,14 @@ bool ConfigManager::clearAllSettings() { int numFiles = sizeof(settingsFiles) / sizeof(settingsFiles[0]); - LOG_INFO("ConfigManager: Attempting to delete %d configuration files...", numFiles); + LOG_WARNING("🗑️ Step 1: Deleting %d configuration files...", numFiles); - // Delete each configuration file for (int i = 0; i < numFiles; i++) { const char* filepath = settingsFiles[i]; if (SD.exists(filepath)) { if (SD.remove(filepath)) { - LOG_INFO("✅ Deleted: %s", filepath); + LOG_DEBUG("✅ Deleted: %s", filepath); filesDeleted++; } else { LOG_ERROR("❌ Failed to delete: %s", filepath); @@ -923,22 +856,153 @@ bool ConfigManager::clearAllSettings() { } } - // Also delete the /melodies directory if you want a complete reset - // Uncomment if you want to delete melodies too: - /* + // ════════════════════════════════════════════════════════════════════════════ + // STEP 2: Delete all melodies recursively + // ════════════════════════════════════════════════════════════════════════════ + if (SD.exists("/melodies")) { - LOG_INFO("Deleting /melodies directory..."); - // Note: SD library doesn't have rmdir for non-empty dirs - // You'd need to implement recursive delete or just leave melodies + LOG_WARNING("🗑️ Step 2: Deleting melody files..."); + + File melodiesDir = SD.open("/melodies"); + if (melodiesDir && melodiesDir.isDirectory()) { + int melodiesDeleted = 0; + int melodiesFailed = 0; + + File entry = melodiesDir.openNextFile(); + while (entry) { + String entryPath = String("/melodies/") + entry.name(); + + if (!entry.isDirectory()) { + if (SD.remove(entryPath.c_str())) { + LOG_DEBUG("✅ Deleted melody: %s", entryPath.c_str()); + melodiesDeleted++; + } else { + LOG_ERROR("❌ Failed to delete melody: %s", entryPath.c_str()); + melodiesFailed++; + allDeleted = false; + } + } + + entry.close(); + entry = melodiesDir.openNextFile(); + } + + melodiesDir.close(); + + // Try to remove the empty directory + if (SD.rmdir("/melodies")) { + LOG_DEBUG("✅ Deleted /melodies directory"); + } else { + LOG_WARNING("⚠️ Could not delete /melodies directory (may not be empty)"); + } + + LOG_WARNING(" 🎵 Melodies deleted: %d, failed: %d", melodiesDeleted, melodiesFailed); + filesDeleted += melodiesDeleted; + filesFailed += melodiesFailed; + } + } else { + LOG_DEBUG("⏩ /melodies directory not found"); } - */ - LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); - LOG_INFO("📄 Settings cleanup summary:"); - LOG_INFO(" ✅ Files deleted: %d", filesDeleted); - LOG_INFO(" ❌ Files failed: %d", filesFailed); - LOG_INFO(" 🔄 Total processed: %d / %d", filesDeleted + filesFailed, numFiles); - LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); + // ════════════════════════════════════════════════════════════════════════════ + // SUMMARY + // ════════════════════════════════════════════════════════════════════════════ + LOG_WARNING("═══════════════════════════════════════════════════════════════════════════"); + LOG_WARNING("📄 Full reset summary:"); + LOG_WARNING(" ✅ Files deleted: %d", filesDeleted); + LOG_WARNING(" ❌ Files failed: %d", filesFailed); + LOG_WARNING(" 🔄 Total processed: %d", filesDeleted + filesFailed); + LOG_WARNING("═══════════════════════════════════════════════════════════════════════════"); + + LOG_WARNING("✅ RESET TO DEFAULT COMPLETE"); + LOG_WARNING("🔄 Device will boot with default settings on next restart"); + LOG_WARNING("🆔 Device identity (UID) preserved"); + return allDeleted; } + +// ═══════════════════════════════════════════════════════════════════════════════ +// GET ALL SETTINGS AS JSON +// ═══════════════════════════════════════════════════════════════════════════════ + +String ConfigManager::getAllSettingsAsJson() const { + // Use a large document to hold everything + DynamicJsonDocument doc(4096); + + // Device info + JsonObject device = doc.createNestedObject("device"); + device["uid"] = deviceConfig.deviceUID; + device["hwType"] = deviceConfig.hwType; + device["hwVersion"] = deviceConfig.hwVersion; + device["fwVersion"] = deviceConfig.fwVersion; + + // Network config + JsonObject network = doc.createNestedObject("network"); + network["hostname"] = networkConfig.hostname; + network["useStaticIP"] = networkConfig.useStaticIP; + network["ip"] = networkConfig.ip.toString(); + network["gateway"] = networkConfig.gateway.toString(); + network["subnet"] = networkConfig.subnet.toString(); + network["dns1"] = networkConfig.dns1.toString(); + network["dns2"] = networkConfig.dns2.toString(); + + // Time config + JsonObject time = doc.createNestedObject("time"); + time["ntpServer"] = timeConfig.ntpServer; + time["gmtOffsetSec"] = timeConfig.gmtOffsetSec; + time["daylightOffsetSec"] = timeConfig.daylightOffsetSec; + + // Bell durations (relay timings) + JsonObject bells = doc.createNestedObject("bells"); + for (uint8_t i = 0; i < 16; i++) { + String key = String("b") + (i + 1); + bells[key] = bellConfig.durations[i]; + } + + // Clock configuration + JsonObject clock = doc.createNestedObject("clock"); + clock["enabled"] = clockConfig.enabled; + clock["c1output"] = clockConfig.c1output; + clock["c2output"] = clockConfig.c2output; + clock["pulseDuration"] = clockConfig.pulseDuration; + clock["pauseDuration"] = clockConfig.pauseDuration; + + // Clock state + JsonObject clockState = doc.createNestedObject("clockState"); + clockState["physicalHour"] = clockConfig.physicalHour; + clockState["physicalMinute"] = clockConfig.physicalMinute; + clockState["nextOutputIsC1"] = clockConfig.nextOutputIsC1; + clockState["lastSyncTime"] = clockConfig.lastSyncTime; + + // Clock alerts + JsonObject alerts = doc.createNestedObject("alerts"); + alerts["alertType"] = clockConfig.alertType; + alerts["alertRingInterval"] = clockConfig.alertRingInterval; + alerts["hourBell"] = clockConfig.hourBell; + alerts["halfBell"] = clockConfig.halfBell; + alerts["quarterBell"] = clockConfig.quarterBell; + + // Clock backlight + JsonObject backlight = doc.createNestedObject("backlight"); + backlight["enabled"] = clockConfig.backlight; + backlight["output"] = clockConfig.backlightOutput; + backlight["onTime"] = clockConfig.backlightOnTime; + backlight["offTime"] = clockConfig.backlightOffTime; + + // Silence periods + JsonObject silence = doc.createNestedObject("silence"); + JsonObject daytime = silence.createNestedObject("daytime"); + daytime["enabled"] = clockConfig.daytimeSilenceEnabled; + daytime["onTime"] = clockConfig.daytimeSilenceOnTime; + daytime["offTime"] = clockConfig.daytimeSilenceOffTime; + JsonObject nighttime = silence.createNestedObject("nighttime"); + nighttime["enabled"] = clockConfig.nighttimeSilenceEnabled; + nighttime["onTime"] = clockConfig.nighttimeSilenceOnTime; + nighttime["offTime"] = clockConfig.nighttimeSilenceOffTime; + + // Serialize to string + String output; + serializeJson(doc, output); + return output; +} diff --git a/vesper/src/ConfigManager/ConfigManager.hpp b/vesper/src/ConfigManager/ConfigManager.hpp index 1ba29bd..cf0dc86 100644 --- a/vesper/src/ConfigManager/ConfigManager.hpp +++ b/vesper/src/ConfigManager/ConfigManager.hpp @@ -75,14 +75,15 @@ public: * @struct MqttConfig * @brief MQTT broker connection settings * - * Cloud broker as default, can be overridden via SD card. + * Default configured for local Mosquitto broker. * Username defaults to deviceUID for unique identification. */ struct MqttConfig { - String host = "j2f24f16.ala.eu-central-1.emqxsl.com"; // 📡 Cloud MQTT broker (default) - int port = 1883; // 🔌 Standard MQTT port (default) + IPAddress host = IPAddress(145, 223, 96, 251); // 📡 Local Mosquitto broker + int port = 1883; // 🔌 Standard MQTT port (non-SSL) String user; // 👤 Auto-set to deviceUID - String password = "vesper"; // 🔑 Default password - OK as is + String password = "vesper"; // 🔑 Default password + bool useSSL = false; // 🔒 SSL disabled for local broker }; /** @@ -249,12 +250,11 @@ private: bool ensureSDCard(); void createDefaultBellConfig(); - // NVS management (for factory-set device identity) + // NVS management (READ-ONLY for factory-set device identity) bool initializeNVS(); bool loadDeviceIdentityFromNVS(); - bool saveDeviceIdentityToNVS(); String readNVSString(const char* key, const String& defaultValue); - bool writeNVSString(const char* key, const String& value); + // REMOVED: saveDeviceIdentityToNVS() and writeNVSString() - Production firmware is READ-ONLY public: // ═══════════════════════════════════════════════════════════════════════════════ @@ -287,37 +287,20 @@ public: const BellConfig& getBellConfig() const { return bellConfig; } const ClockConfig& getClockConfig() const { return clockConfig; } - // Device identity methods (read-only - factory set via separate firmware) + // Device identity methods (READ-ONLY - factory set via separate factory firmware) + // These values are loaded ONCE at boot from NVS and kept in RAM + // Production firmware CANNOT modify device identity String getDeviceUID() const { return deviceConfig.deviceUID; } String getHwType() const { return deviceConfig.hwType; } String getHwVersion() const { return deviceConfig.hwVersion; } String getFwVersion() const { return deviceConfig.fwVersion; } - /** @brief Set device UID (factory programming only) */ - void setDeviceUID(const String& uid) { - deviceConfig.deviceUID = uid; - saveDeviceIdentityToNVS(); - generateNetworkIdentifiers(); - } - - /** @brief Set hardware type (factory programming only) */ - void setHwType(const String& type) { - deviceConfig.hwType = type; - saveDeviceIdentityToNVS(); - } - - /** @brief Set hardware version (factory programming only) */ - void setHwVersion(const String& version) { - deviceConfig.hwVersion = version; - saveDeviceIdentityToNVS(); - } - /** @brief Set firmware version (auto-updated after OTA) */ void setFwVersion(const String& version) { deviceConfig.fwVersion = version; } // Configuration update methods for app commands void updateTimeConfig(long gmtOffsetSec, int daylightOffsetSec); - void updateNetworkConfig(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 @@ -352,7 +335,7 @@ public: uint8_t getPhysicalClockMinute() const { return clockConfig.physicalMinute; } bool getNextOutputIsC1() const { return clockConfig.nextOutputIsC1; } uint32_t getLastSyncTime() const { return clockConfig.lastSyncTime; } - void setPhysicalClockHour(uint8_t hour) { clockConfig.physicalHour = hour % 12; } + void setPhysicalClockHour(uint8_t hour) { clockConfig.physicalHour = (hour % 12 == 0) ? 12 : hour % 12; } void setPhysicalClockMinute(uint8_t minute) { clockConfig.physicalMinute = minute % 60; } void setNextOutputIsC1(bool isC1) { clockConfig.nextOutputIsC1 = isC1; } void setLastSyncTime(uint32_t timestamp) { clockConfig.lastSyncTime = timestamp; } @@ -412,33 +395,34 @@ public: String getAPSSID() const { return networkConfig.apSsid; } bool isHealthy() const; + /** + * @brief Get all configuration settings as a JSON string + * @return JSON string containing all current settings + */ + String getAllSettingsAsJson() const; + // ═══════════════════════════════════════════════════════════════════════════════ // FACTORY RESET // ═══════════════════════════════════════════════════════════════════════════════ /** - * @brief Perform complete factory reset + * @brief Perform a full Reset of the Settings to Factory Defaults * * This method: - * 1. Deletes all configuration files from SD card - * 2. Does NOT touch NVS (device identity remains) - * 3. On next boot, all settings will be recreated with defaults + * 1. Clears all user Settings on the SD card + * 2. Deletes all Melodies on the SD card * - * @return true if factory reset successful + * @return true if reset successful, false otherwise */ - bool factoryReset(); + bool resetAllToDefaults(); - /** - * @brief Delete all settings files from SD card - * @return true if all files deleted successfully - */ - bool clearAllSettings(); + }; // ═══════════════════════════════════════════════════════════════════════════════════ // DEPLOYMENT NOTES: -// 2. USER SETTINGS: All loaded from SD card, configured via app -// 3. NETWORK: WiFiManager handles credentials, no hardcoded SSIDs/passwords +// 2. USER SETTINGS: All loaded from SD card, configurable via app +// 3. NETWORK: WiFiManager handles SSIDs/Passwords. IP Settigns loaded from SD, configurable via app // 4. IDENTIFIERS: Auto-generated from deviceUID for consistency // 5. DEFAULTS: Clean minimal defaults, everything configurable // ═══════════════════════════════════════════════════════════════════════════════════ diff --git a/vesper/src/HealthMonitor/HealthMonitor.cpp b/vesper/src/HealthMonitor/HealthMonitor.cpp index 3c967af..e327d3f 100644 --- a/vesper/src/HealthMonitor/HealthMonitor.cpp +++ b/vesper/src/HealthMonitor/HealthMonitor.cpp @@ -7,7 +7,7 @@ #include "HealthMonitor.hpp" #include "../BellEngine/BellEngine.hpp" #include "../OutputManager/OutputManager.hpp" -#include "../Communication/Communication.hpp" +#include "../Communication/CommunicationRouter/CommunicationRouter.hpp" #include "../Player/Player.hpp" #include "../TimeKeeper/TimeKeeper.hpp" #include "../Telemetry/Telemetry.hpp" diff --git a/vesper/src/HealthMonitor/HealthMonitor.hpp b/vesper/src/HealthMonitor/HealthMonitor.hpp index 5d5e93a..5061863 100644 --- a/vesper/src/HealthMonitor/HealthMonitor.hpp +++ b/vesper/src/HealthMonitor/HealthMonitor.hpp @@ -57,7 +57,7 @@ // Forward declarations for all monitored subsystems class BellEngine; class OutputManager; -class Communication; +class CommunicationRouter; class Player; class Timekeeper; class Telemetry; @@ -137,7 +137,7 @@ public: void setOutputManager(OutputManager* outputManager) { _outputManager = outputManager; } /** @brief Register Communication for monitoring */ - void setCommunication(Communication* communication) { _communication = communication; } + void setCommunication(CommunicationRouter* communication) { _communication = communication; } /** @brief Register Player for monitoring */ void setPlayer(Player* player) { _player = player; } @@ -256,7 +256,7 @@ private: // ═══════════════════════════════════════════════════════════════════════════════ BellEngine* _bellEngine = nullptr; OutputManager* _outputManager = nullptr; - Communication* _communication = nullptr; + CommunicationRouter* _communication = nullptr; Player* _player = nullptr; Timekeeper* _timeKeeper = nullptr; Telemetry* _telemetry = nullptr; diff --git a/vesper/src/MqttSSL/MqttSSL.cpp b/vesper/src/MqttSSL/MqttSSL.cpp deleted file mode 100644 index a64949a..0000000 --- a/vesper/src/MqttSSL/MqttSSL.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "MqttSSL.hpp" -#include "../Logging/Logging.hpp" - -// EMQX Cloud CA Certificate (DigiCert Global Root CA) -const char* MqttSSL::_emqxCloudCA = R"EOF( ------BEGIN CERTIFICATE----- -MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD -QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j -b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB -CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 -nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt -43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P -T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 -gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO -BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR -TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw -DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr -hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg -06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF -PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls -YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk -CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= ------END CERTIFICATE----- -)EOF"; - -MqttSSL::MqttSSL() { -} - -MqttSSL::~MqttSSL() { -} - -bool MqttSSL::isSSLAvailable() { -#ifdef ASYNC_TCP_SSL_ENABLED - return true; -#else - return false; -#endif -} - -const char* MqttSSL::getEMQXCA() { - return _emqxCloudCA; -} - -void MqttSSL::logSSLStatus(const AsyncMqttClient& client, int port) { - if (port == 8883) { - if (isSSLAvailable()) { - LOG_INFO("🔒 MQTT SSL/TLS enabled for port %d", port); - LOG_INFO("🔐 Certificate validation: Using DigiCert Global Root CA"); - } else { - LOG_ERROR("❌ SSL requested but not compiled in! Add ASYNC_TCP_SSL_ENABLED to build flags"); - } - } else { - LOG_WARNING("⚠️ MQTT using unencrypted connection on port %d", port); - } -} diff --git a/vesper/src/MqttSSL/MqttSSL.hpp b/vesper/src/MqttSSL/MqttSSL.hpp deleted file mode 100644 index 9652c67..0000000 --- a/vesper/src/MqttSSL/MqttSSL.hpp +++ /dev/null @@ -1,48 +0,0 @@ -/* - * ═══════════════════════════════════════════════════════════════════════════════════ - * MQTTSSL.HPP - EMQX Cloud SSL/TLS Certificate Management - * ═══════════════════════════════════════════════════════════════════════════════════ - * - * 🔒 SECURE MQTT CONNECTION FOR EMQX CLOUD 🔒 - * - * This class manages SSL/TLS certificates for EMQX Cloud connections. - * Note: AsyncMqttClient SSL is configured at compile time, not runtime. - * - * 📋 VERSION: 1.0 - * 📅 DATE: 2025-09-30 - * 👨‍💻 AUTHOR: Advanced Bell Systems - * ═══════════════════════════════════════════════════════════════════════════════════ - */ - -#pragma once - -#include -#include - -class MqttSSL { -public: - MqttSSL(); - ~MqttSSL(); - - /** - * @brief Check if SSL is available (compile-time check) - * @return true if SSL support is compiled in - */ - static bool isSSLAvailable(); - - /** - * @brief Get EMQX Cloud CA certificate - * @return CA certificate string - */ - static const char* getEMQXCA(); - - /** - * @brief Log SSL status - * @param client Reference to AsyncMqttClient - * @param port MQTT port being used - */ - static void logSSLStatus(const AsyncMqttClient& client, int port); - -private: - static const char* _emqxCloudCA; -}; diff --git a/vesper/src/Networking/Networking.cpp b/vesper/src/Networking/Networking.cpp index bd8cc9c..0c6de8c 100644 --- a/vesper/src/Networking/Networking.cpp +++ b/vesper/src/Networking/Networking.cpp @@ -12,9 +12,9 @@ Networking::Networking(ConfigManager& configManager) , _state(NetworkState::DISCONNECTED) , _activeConnection(ConnectionType::NONE) , _lastConnectionAttempt(0) - , _bootStartTime(0) , _bootSequenceComplete(false) , _ethernetCableConnected(false) + , _wifiConnectionFailures(0) , _wifiManager(nullptr) , _reconnectionTimer(nullptr) { @@ -46,11 +46,10 @@ Networking::~Networking() { } } + void Networking::begin() { LOG_INFO("Initializing Networking System"); - _bootStartTime = millis(); - // Create reconnection timer _reconnectionTimer = xTimerCreate("reconnectionTimer", pdMS_TO_TICKS(RECONNECTION_INTERVAL), pdTRUE, (void*)0, reconnectionTimerCallback); @@ -60,7 +59,10 @@ void Networking::begin() { // Configure WiFiManager _wifiManager->setDebugOutput(false); - _wifiManager->setConfigPortalTimeout(180); // 3 minutes + _wifiManager->setConfigPortalTimeout(300); // 5 minutes + + // Clear Previous Settings, USE once to test. + //_wifiManager->resetSettings(); // Start Ethernet hardware auto& hwConfig = _configManager.getHardwareConfig(); @@ -98,13 +100,16 @@ void Networking::startWiFiConnection() { if (!hasValidWiFiCredentials()) { LOG_WARNING("No valid WiFi credentials found"); - if (shouldStartPortal()) { + if (!_bootSequenceComplete) { + // No credentials during boot - start portal startWiFiPortal(); } return; } - LOG_INFO("Using WiFiManager saved credentials"); + // Get and log saved credentials (for debugging) + String savedSSID = _wifiManager->getWiFiSSID(true); + LOG_INFO("Using WiFiManager saved credentials - SSID: %s", savedSSID.c_str()); WiFi.mode(WIFI_STA); applyNetworkConfig(false); // false = WiFi config @@ -158,12 +163,36 @@ void Networking::handleReconnection() { return; // Still waiting for Ethernet } - // Check for WiFi timeout (try again) + // Check for WiFi timeout if (_state == NetworkState::CONNECTING_WIFI) { unsigned long now = millis(); if (now - _lastConnectionAttempt > 10000) { // 10 second timeout - LOG_INFO("WiFi connection timeout - retrying"); - startWiFiConnection(); // Retry WiFi + _wifiConnectionFailures++; + LOG_WARNING("WiFi connection timeout (failure #%d)", _wifiConnectionFailures); + + // After 3 failed attempts during boot, start portal + if (_wifiConnectionFailures >= MAX_WIFI_FAILURES) { + LOG_ERROR("Multiple WiFi connection failures - credentials may be invalid"); + + if (!_bootSequenceComplete) { + // Boot not complete yet - open portal + LOG_INFO("Opening WiFi portal for reconfiguration"); + _wifiConnectionFailures = 0; // Reset counter + startWiFiPortal(); + } else { + // Boot already complete - just keep retrying + LOG_WARNING("WiFi connection lost - continuing retry attempts"); + // Reset counter after extended failure to prevent overflow + if (_wifiConnectionFailures > 10) { + _wifiConnectionFailures = 3; + } + _lastConnectionAttempt = now; + startWiFiConnection(); + } + } else { + // Retry WiFi connection + startWiFiConnection(); + } } return; // Still waiting for WiFi } @@ -176,7 +205,8 @@ void Networking::handleReconnection() { LOG_INFO("No Ethernet - trying WiFi"); if (hasValidWiFiCredentials()) { startWiFiConnection(); - } else if (shouldStartPortal()) { + } else if (!_bootSequenceComplete) { + // No credentials during boot - start portal startWiFiPortal(); } else { LOG_WARNING("No WiFi credentials and boot sequence complete - waiting"); @@ -292,6 +322,9 @@ void Networking::onWiFiConnected() { setState(NetworkState::CONNECTED_WIFI); setActiveConnection(ConnectionType::WIFI); + // Reset failure counter on successful connection + _wifiConnectionFailures = 0; + // Stop reconnection timer xTimerStop(_reconnectionTimer, 0); @@ -347,18 +380,13 @@ void Networking::applyNetworkConfig(bool ethernet) { } bool Networking::hasValidWiFiCredentials() { - // Check if WiFiManager has saved credentials - return WiFi.SSID().length() > 0; + // Use WiFiManager's method to check if credentials are saved + return _wifiManager->getWiFiIsSaved(); } -bool Networking::shouldStartPortal() { - // Only start portal during boot sequence and if we're truly disconnected - return !_bootSequenceComplete && - (millis() - _bootStartTime < BOOT_TIMEOUT) && - _activeConnection == ConnectionType::NONE; -} -// Status methods + +// Returns if Networking is connected bool Networking::isConnected() const { return _activeConnection != ConnectionType::NONE; } @@ -445,12 +473,5 @@ void Networking::networkEventHandler(arduino_event_id_t event, arduino_event_inf void Networking::reconnectionTimerCallback(TimerHandle_t xTimer) { if (_instance) { _instance->handleReconnection(); - - // Check if boot sequence should be marked complete - if (!_instance->_bootSequenceComplete && - (millis() - _instance->_bootStartTime > BOOT_TIMEOUT)) { - _instance->_bootSequenceComplete = true; - LOG_INFO("Boot sequence timeout - no more portal attempts"); - } } } diff --git a/vesper/src/Networking/Networking.hpp b/vesper/src/Networking/Networking.hpp index 976dcf1..77c29fb 100644 --- a/vesper/src/Networking/Networking.hpp +++ b/vesper/src/Networking/Networking.hpp @@ -82,7 +82,7 @@ public: void begin(); - // Status methods + // Returns whether the network is currently connected bool isConnected() const; String getLocalIP() const; ConnectionType getActiveConnection() const { return _activeConnection; } @@ -119,9 +119,9 @@ private: NetworkState _state; ConnectionType _activeConnection; unsigned long _lastConnectionAttempt; - unsigned long _bootStartTime; bool _bootSequenceComplete; bool _ethernetCableConnected; + int _wifiConnectionFailures; // Track consecutive WiFi failures // Callbacks std::function _onNetworkConnected; @@ -151,12 +151,11 @@ private: // Utility methods void applyNetworkConfig(bool ethernet = false); bool hasValidWiFiCredentials(); - bool shouldStartPortal(); // Timer callback static void reconnectionTimerCallback(TimerHandle_t xTimer); // Constants static const unsigned long RECONNECTION_INTERVAL = 5000; // 5 seconds - static const unsigned long BOOT_TIMEOUT = 30000; // 30 seconds for boot sequence + static const int MAX_WIFI_FAILURES = 3; // Portal after 3 failures }; diff --git a/vesper/src/OTAManager/OTAManager.cpp b/vesper/src/OTAManager/OTAManager.cpp index a071f8f..189315d 100644 --- a/vesper/src/OTAManager/OTAManager.cpp +++ b/vesper/src/OTAManager/OTAManager.cpp @@ -1,33 +1,109 @@ #include "OTAManager.hpp" #include "../ConfigManager/ConfigManager.hpp" #include "../Logging/Logging.hpp" +#include "../Player/Player.hpp" #include #include OTAManager::OTAManager(ConfigManager& configManager) : _configManager(configManager) , _fileManager(nullptr) + , _player(nullptr) , _status(Status::IDLE) , _lastError(ErrorCode::NONE) , _availableVersion(0.0f) + , _minVersion(0.0f) + , _expectedFileSize(0) , _updateAvailable(false) , _availableChecksum("") , _updateChannel("stable") , _isMandatory(false) , _isEmergency(false) , _progressCallback(nullptr) - , _statusCallback(nullptr) { + , _statusCallback(nullptr) + , _scheduledCheckTimer(NULL) { +} + +OTAManager::~OTAManager() { + if (_scheduledCheckTimer != NULL) { + xTimerStop(_scheduledCheckTimer, 0); + xTimerDelete(_scheduledCheckTimer, portMAX_DELAY); + _scheduledCheckTimer = NULL; + } } void OTAManager::begin() { LOG_INFO("OTA Manager initialized"); setStatus(Status::IDLE); + + // Create timer for scheduled checks (checks every minute if it's 3:00 AM) + _scheduledCheckTimer = xTimerCreate( + "OTA_Schedule", + pdMS_TO_TICKS(60000), // Check every minute + pdTRUE, // Auto-reload (periodic) + this, // Timer ID (pass OTAManager instance) + scheduledCheckCallback + ); + + if (_scheduledCheckTimer != NULL) { + xTimerStart(_scheduledCheckTimer, 0); + LOG_INFO("OTA scheduled check timer started (will check at 3:00 AM)"); + } else { + LOG_ERROR("Failed to create OTA scheduled check timer!"); + } } void OTAManager::setFileManager(FileManager* fm) { _fileManager = fm; } +void OTAManager::setPlayer(Player* player) { + _player = player; +} + +// ✅ NEW: Static timer callback for scheduled checks +void OTAManager::scheduledCheckCallback(TimerHandle_t xTimer) { + OTAManager* ota = static_cast(pvTimerGetTimerID(xTimer)); + + // Get current time + time_t now = time(nullptr); + struct tm* timeinfo = localtime(&now); + + // Only proceed if it's exactly 3:00 AM + if (timeinfo->tm_hour == 3 && timeinfo->tm_min == 0) { + LOG_INFO("🕒 3:00 AM - Running scheduled OTA check"); + + // Check if player is idle before proceeding + if (!ota->isPlayerActive()) { + LOG_INFO("✅ Player is idle - checking for emergency updates"); + ota->checkForEmergencyUpdates(); + } else { + LOG_WARNING("⚠️ Player is active - skipping scheduled update check"); + } + } +} + +// ✅ NEW: Check for emergency updates only (called by scheduled timer) +void OTAManager::checkForEmergencyUpdates() { + if (_status != Status::IDLE) { + LOG_WARNING("OTA check already in progress"); + return; + } + + LOG_INFO("Checking for EMERGENCY updates only..."); + checkForUpdates("stable"); // Check stable channel + + // Only proceed if emergency flag is set + if (_updateAvailable && _isEmergency) { + LOG_INFO("🚨 EMERGENCY update detected during scheduled check - updating immediately"); + update("stable"); + } else if (_updateAvailable && _isMandatory) { + LOG_INFO("⚠️ Mandatory update available, but will wait for next boot"); + } else { + LOG_INFO("✅ No emergency updates available"); + } +} + void OTAManager::checkForUpdates() { // Boot-time check: only check stable channel for emergency/mandatory updates checkForUpdates("stable"); @@ -39,6 +115,12 @@ void OTAManager::checkForUpdates(const String& channel) { return; } + // 🔥 CRITICAL: Check network connectivity before attempting HTTP requests + if (WiFi.status() != WL_CONNECTED && !ETH.linkUp()) { + LOG_WARNING("OTA check skipped - no network connectivity"); + return; + } + setStatus(Status::CHECKING_VERSION); LOG_INFO("Checking for firmware updates in %s channel for %s...", channel.c_str(), _configManager.getHardwareVariant().c_str()); @@ -118,7 +200,7 @@ void OTAManager::notifyProgress(size_t current, size_t total) { } } -// Enhanced version checking with channel support and multiple servers +// ✅ ENHANCED: Version checking with full validation bool OTAManager::checkVersion(const String& channel) { std::vector servers = _configManager.getUpdateServers(); auto& updateConfig = _configManager.getUpdateConfig(); @@ -167,6 +249,16 @@ bool OTAManager::checkVersion(const String& channel) { _updateChannel = doc["channel"].as(); _isMandatory = doc["mandatory"].as(); _isEmergency = doc["emergency"].as(); + _minVersion = doc["minVersion"].as(); // ✅ NEW + _expectedFileSize = doc["fileSize"].as(); // ✅ NEW + + // ✅ NEW: Validate channel matches requested + if (_updateChannel != channel) { + LOG_ERROR("OTA: Channel mismatch! Requested: %s, Got: %s", + channel.c_str(), _updateChannel.c_str()); + _lastError = ErrorCode::CHANNEL_MISMATCH; + continue; // Try next server + } // Validate hardware variant matches String hwVariant = doc["hardwareVariant"].as(); @@ -177,6 +269,16 @@ bool OTAManager::checkVersion(const String& channel) { continue; // Try next server } + // ✅ NEW: Check minVersion compatibility + float currentVersion = getCurrentVersion(); + if (_minVersion > 0.0f && currentVersion < _minVersion) { + LOG_ERROR("OTA: Current version %.1f is below minimum required %.1f", + currentVersion, _minVersion); + LOG_ERROR("OTA: Intermediate update required first - cannot proceed"); + _lastError = ErrorCode::VERSION_TOO_LOW; + continue; // Try next server + } + if (_availableVersion == 0.0f) { LOG_ERROR("OTA: Invalid version in metadata from %s", baseUrl.c_str()); continue; // Try next server @@ -188,6 +290,8 @@ bool OTAManager::checkVersion(const String& channel) { } LOG_INFO("OTA: Successfully got metadata from %s", baseUrl.c_str()); + LOG_INFO("OTA: Expected file size: %u bytes, Min version: %.1f", + _expectedFileSize, _minVersion); return true; // Success! } else { LOG_ERROR("OTA: Server %s failed after %d retries. HTTP error: %d", @@ -202,7 +306,7 @@ bool OTAManager::checkVersion(const String& channel) { return false; } -// Enhanced download and install with channel support and multiple servers +// ✅ ENHANCED: Download and install with size validation bool OTAManager::downloadAndInstall(const String& channel) { std::vector servers = _configManager.getUpdateServers(); @@ -213,7 +317,7 @@ bool OTAManager::downloadAndInstall(const String& channel) { LOG_INFO("OTA: Trying firmware download from server %d/%d: %s", serverIndex + 1, servers.size(), baseUrl.c_str()); - if (downloadToSD(firmwareUrl, _availableChecksum)) { + if (downloadToSD(firmwareUrl, _availableChecksum, _expectedFileSize)) { // Success! Now install from SD return installFromSD("/firmware/staged_update.bin"); } else { @@ -226,9 +330,7 @@ bool OTAManager::downloadAndInstall(const String& channel) { return false; } -bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum) { - // This method now receives the exact firmware URL from downloadAndInstall - // The server selection logic is handled there +bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum, size_t expectedSize) { if (!_fileManager) { LOG_ERROR("FileManager not set!"); setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED); @@ -260,6 +362,24 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum) return false; } + // ✅ NEW: Validate file size against metadata + if (expectedSize > 0 && (size_t)contentLength != expectedSize) { + LOG_ERROR("OTA: File size mismatch! Expected: %u, Got: %d", expectedSize, contentLength); + setStatus(Status::FAILED, ErrorCode::SIZE_MISMATCH); + http.end(); + return false; + } + + // ✅ NEW: Check available SD card space + if (!checkAvailableSpace(contentLength)) { + LOG_ERROR("OTA: Insufficient SD card space for update"); + setStatus(Status::FAILED, ErrorCode::INSUFFICIENT_SPACE); + http.end(); + return false; + } + + LOG_INFO("OTA: Starting download of %d bytes...", contentLength); + // Open file for writing File file = SD.open(tempPath.c_str(), FILE_WRITE); if (!file) { @@ -272,8 +392,9 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum) WiFiClient* stream = http.getStreamPtr(); uint8_t buffer[1024]; size_t written = 0; + size_t lastLoggedPercent = 0; - while (http.connected() && written < contentLength) { + while (http.connected() && written < (size_t)contentLength) { size_t available = stream->available(); if (available) { size_t toRead = min(available, sizeof(buffer)); @@ -289,7 +410,17 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum) return false; } written += bytesWritten; + + // ✅ IMPROVED: Progress reporting with percentage notifyProgress(written, contentLength); + + // Log progress every 10% + size_t currentPercent = (written * 100) / contentLength; + if (currentPercent >= lastLoggedPercent + 10) { + LOG_INFO("OTA: Download progress: %u%% (%u/%u bytes)", + currentPercent, written, contentLength); + lastLoggedPercent = currentPercent; + } } } yield(); @@ -298,13 +429,13 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum) file.close(); http.end(); - if (written != contentLength) { - LOG_ERROR("Download incomplete: %d/%d bytes", written, contentLength); + if (written != (size_t)contentLength) { + LOG_ERROR("Download incomplete: %u/%d bytes", written, contentLength); setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED); return false; } - LOG_INFO("Download complete (%d bytes)", written); + LOG_INFO("Download complete (%u bytes)", written); // Verify checksum if (!verifyChecksum(tempPath, expectedChecksum)) { @@ -522,7 +653,7 @@ bool OTAManager::performManualUpdate(const String& channel) { String firmwareUrl = buildFirmwareUrl(channel); // Download to SD first - if (!downloadToSD(firmwareUrl, _availableChecksum)) { + if (!downloadToSD(firmwareUrl, _availableChecksum, _expectedFileSize)) { return false; } @@ -537,7 +668,6 @@ String OTAManager::getHardwareVariant() const { void OTAManager::setHardwareVariant(const String& variant) { LOG_WARNING("OTAManager::setHardwareVariant is deprecated. Use ConfigManager::setHardwareVariant instead"); - // For backward compatibility, we could call configManager, but it's better to use ConfigManager directly } // URL builders for multi-channel architecture @@ -556,6 +686,47 @@ String OTAManager::buildFirmwareUrl(const String& channel) const { return buildChannelUrl(channel) + "firmware.bin"; } +// ✅ NEW: Check if player is currently active +bool OTAManager::isPlayerActive() const { + if (!_player) { + // If player reference not set, assume it's safe to update + return false; + } + + // Player is active if it's playing or paused (not stopped) + return _player->isCurrentlyPlaying() || _player->isCurrentlyPaused(); +} + +// ✅ NEW: Check if SD card has enough free space +bool OTAManager::checkAvailableSpace(size_t requiredBytes) const { + if (!_fileManager) { + LOG_WARNING("OTA: FileManager not set, cannot check available space"); + return true; // Assume it's okay if we can't check + } + + // Add 10% safety margin + size_t requiredWithMargin = requiredBytes + (requiredBytes / 10); + + // Get SD card info + uint64_t totalBytes = SD.totalBytes(); + uint64_t usedBytes = SD.usedBytes(); + uint64_t freeBytes = totalBytes - usedBytes; + + LOG_INFO("OTA: SD card space - Total: %llu MB, Used: %llu MB, Free: %llu MB", + totalBytes / (1024 * 1024), + usedBytes / (1024 * 1024), + freeBytes / (1024 * 1024)); + + if (freeBytes < requiredWithMargin) { + LOG_ERROR("OTA: Insufficient space! Required: %u bytes (+10%% margin), Available: %llu bytes", + requiredWithMargin, freeBytes); + return false; + } + + LOG_INFO("OTA: Sufficient space available for update"); + return true; +} + // ════════════════════════════════════════════════════════════════════════════ // HEALTH CHECK IMPLEMENTATION // ════════════════════════════════════════════════════════════════════════════ @@ -599,5 +770,11 @@ bool OTAManager::isHealthy() const { return false; } + // Check if scheduled timer is running + if (_scheduledCheckTimer == NULL || xTimerIsTimerActive(_scheduledCheckTimer) == pdFALSE) { + LOG_DEBUG("OTAManager: Unhealthy - Scheduled check timer not running"); + return false; + } + return true; } diff --git a/vesper/src/OTAManager/OTAManager.hpp b/vesper/src/OTAManager/OTAManager.hpp index b2f0a0b..57015dc 100644 --- a/vesper/src/OTAManager/OTAManager.hpp +++ b/vesper/src/OTAManager/OTAManager.hpp @@ -8,7 +8,7 @@ * This class manages over-the-air firmware updates with safe, reliable * update mechanisms, version checking, and comprehensive error handling. * - * 📋 VERSION: 2.0 (Enhanced OTA management) + * 📋 VERSION: 2.1 (Enhanced with scheduled checks and full validation) * 📅 DATE: 2025 * 👨‍💻 AUTHOR: Advanced Bell Systems * ═══════════════════════════════════════════════════════════════════════════════════ @@ -24,9 +24,11 @@ #include #include #include +#include #include "../FileManager/FileManager.hpp" class ConfigManager; // Forward declaration +class Player; // Forward declaration for idle check class OTAManager { public: @@ -48,7 +50,11 @@ public: WRITE_FAILED, VERIFICATION_FAILED, CHECKSUM_MISMATCH, - METADATA_PARSE_FAILED + METADATA_PARSE_FAILED, + SIZE_MISMATCH, + VERSION_TOO_LOW, + CHANNEL_MISMATCH, + PLAYER_ACTIVE }; // Callback types @@ -56,11 +62,16 @@ public: using StatusCallback = std::function; explicit OTAManager(ConfigManager& configManager); + ~OTAManager(); void begin(); void setFileManager(FileManager* fm); + void setPlayer(Player* player); // NEW: Set player reference for idle check + void checkForUpdates(); void checkForUpdates(const String& channel); // Check specific channel + void checkForEmergencyUpdates(); // NEW: Scheduled emergency-only check + void update(); void update(const String& channel); // Update from specific channel void checkFirmwareUpdateFromSD(); // Check SD for firmware update @@ -92,9 +103,12 @@ public: private: ConfigManager& _configManager; FileManager* _fileManager; + Player* _player; // NEW: Player reference for idle check Status _status; ErrorCode _lastError; float _availableVersion; + float _minVersion; // NEW: Minimum required version + size_t _expectedFileSize; // NEW: Expected firmware file size bool _updateAvailable; String _availableChecksum; String _updateChannel; @@ -104,6 +118,10 @@ private: ProgressCallback _progressCallback; StatusCallback _statusCallback; + // NEW: Scheduled check timer + TimerHandle_t _scheduledCheckTimer; + static void scheduledCheckCallback(TimerHandle_t xTimer); + void setStatus(Status status, ErrorCode error = ErrorCode::NONE); void notifyProgress(size_t current, size_t total); bool checkVersion(); @@ -111,11 +129,15 @@ private: bool checkChannelsMetadata(); bool downloadAndInstall(); bool downloadAndInstall(const String& channel); - bool downloadToSD(const String& url, const String& expectedChecksum); + bool downloadToSD(const String& url, const String& expectedChecksum, size_t expectedSize); // NEW: Added size param bool verifyChecksum(const String& filePath, const String& expectedChecksum); String calculateSHA256(const String& filePath); bool installFromSD(const String& filePath); String buildChannelUrl(const String& channel) const; String buildMetadataUrl(const String& channel) const; String buildFirmwareUrl(const String& channel) const; + + // NEW: Helper methods + bool isPlayerActive() const; + bool checkAvailableSpace(size_t requiredBytes) const; }; diff --git a/vesper/src/Player/Player.cpp b/vesper/src/Player/Player.cpp index 6a64dd1..774d388 100644 --- a/vesper/src/Player/Player.cpp +++ b/vesper/src/Player/Player.cpp @@ -1,11 +1,11 @@ #include "Player.hpp" -#include "../Communication/Communication.hpp" +#include "../Communication/CommunicationRouter/CommunicationRouter.hpp" #include "../BellEngine/BellEngine.hpp" // Note: Removed global melody_steps dependency for cleaner architecture // Constructor with dependencies -Player::Player(Communication* comm, FileManager* fm) +Player::Player(CommunicationRouter* comm, FileManager* fm) : id(0) , name("melody1") , uid("x") @@ -58,7 +58,7 @@ Player::Player() , _durationTimerHandle(NULL) { } -void Player::setDependencies(Communication* comm, FileManager* fm) { +void Player::setDependencies(CommunicationRouter* comm, FileManager* fm) { _commManager = comm; _fileManager = fm; } diff --git a/vesper/src/Player/Player.hpp b/vesper/src/Player/Player.hpp index b76ce4f..0567a47 100644 --- a/vesper/src/Player/Player.hpp +++ b/vesper/src/Player/Player.hpp @@ -61,7 +61,7 @@ // ═════════════════════════════════════════════════════════════════════════════════ // FORWARD DECLARATIONS - Dependencies injected at runtime // ═════════════════════════════════════════════════════════════════════════════════ -class Communication; // Command handling and communication +class CommunicationRouter; // Command handling and communication class BellEngine; // High-precision timing engine // ═════════════════════════════════════════════════════════════════════════════════ @@ -104,7 +104,7 @@ public: * @param comm Pointer to communication manager * @param fm Pointer to file manager */ - Player(Communication* comm, FileManager* fm); + Player(CommunicationRouter* comm, FileManager* fm); /** * @brief Default constructor for backward compatibility @@ -118,7 +118,7 @@ public: * @param comm Pointer to communication manager * @param fm Pointer to file manager */ - void setDependencies(Communication* comm, FileManager* fm); + void setDependencies(CommunicationRouter* comm, FileManager* fm); /** * @brief Set BellEngine reference for precision timing @@ -238,7 +238,7 @@ private: // ═══════════════════════════════════════════════════════════════════════════════ // PRIVATE DEPENDENCIES AND DATA // ═══════════════════════════════════════════════════════════════════════════════ - Communication* _commManager; // 📡 Communication system reference + CommunicationRouter* _commManager; // 📡 Communication system reference FileManager* _fileManager; // 📁 File operations reference BellEngine* _bellEngine; // 🔥 High-precision timing engine reference diff --git a/vesper/src/Telemetry/Telemetry.cpp b/vesper/src/Telemetry/Telemetry.cpp index 04b582b..33166ba 100644 --- a/vesper/src/Telemetry/Telemetry.cpp +++ b/vesper/src/Telemetry/Telemetry.cpp @@ -1,5 +1,4 @@ #include "Telemetry.hpp" -#include "../Communication/Communication.hpp" void Telemetry::begin() { // Initialize arrays @@ -149,17 +148,8 @@ void Telemetry::checkBellLoads() { } } - // Send batch notifications if any bells are overloaded - if (!criticalBells.empty()) { - String severity = anyOverload ? "critical" : "warning"; - if (Communication::_instance) { - Communication::_instance->sendBellOverloadNotification(criticalBells, criticalLoads, severity); - } - } else if (!warningBells.empty()) { - if (Communication::_instance) { - Communication::_instance->sendBellOverloadNotification(warningBells, warningLoads, "warning"); - } - } + // Note: Notifications now handled by BellEngine which has Communication reference + // BellEngine monitors telemetry and sends notifications when overloads detected // Trigger force stop if any bell is actually overloaded if (anyOverload && forceStopCallback != nullptr) { diff --git a/vesper/src/TimeKeeper/TimeKeeper.cpp b/vesper/src/TimeKeeper/TimeKeeper.cpp index 2c08061..c77c8e3 100644 --- a/vesper/src/TimeKeeper/TimeKeeper.cpp +++ b/vesper/src/TimeKeeper/TimeKeeper.cpp @@ -267,9 +267,13 @@ void Timekeeper::checkAndSyncPhysicalClock() { // Calculate time difference (your exact logic!) int16_t timeDifference = (realHour * 60 + realMinute) - (physicalHour * 60 + physicalMinute); + LOG_VERBOSE("⏰ CHECK: Real time %02d:%02d vs Physical %02d:%02d - DIFF: %d mins", + realHour, realMinute, physicalHour, physicalMinute, timeDifference); + // Handle 12-hour rollover (if negative, add 12 hours) if (timeDifference < 0) { timeDifference += 12 * 60; // Add 12 hours to handle rollover + LOG_VERBOSE("⏰ DIFF: Adjusted for rollover, new difference %d minutes", timeDifference); } // If there's a difference, advance the clock by one minute (your runMotor equivalent) @@ -310,7 +314,7 @@ void Timekeeper::updatePhysicalClockTime() { currentMinute = 0; currentHour++; if (currentHour > 12) { // 12-hour clock (your code used 24, but clock face is 12) - currentHour = 0; + currentHour = 1; } } diff --git a/vesper/vesper.ino b/vesper/vesper.ino index 7ce47df..c1e2f24 100644 --- a/vesper/vesper.ino +++ b/vesper/vesper.ino @@ -33,7 +33,7 @@ * ✅ Comprehensive logging system * * 📡 COMMUNICATION PROTOCOLS: - * • MQTT (Primary control interface) + * • MQTT (SSL/TLS via PubSubClient on Core 0) * • WebSocket (Real-time web interface) * • UDP Discovery (Auto-discovery service) * • HTTP/HTTPS (OTA updates) @@ -48,12 +48,39 @@ * High-priority FreeRTOS tasks ensure microsecond timing precision. * Core 1 dedicated to BellEngine for maximum performance. * - * 📋 VERSION: 1.1 - * 📅 DATE: 2025-09-08 - * 👨‍💻 AUTHOR: Advanced Bell Systems * ═══════════════════════════════════════════════════════════════════════════════════ */ + + + + /* + * ═══════════════════════════════════════════════════════════════════════════════ + * 📋 VERSION CONFIGURATION + * ═══════════════════════════════════════════════════════════════════════════════ + * 📅 DATE: 2025-10-10 + * 👨‍💻 AUTHOR: BellSystems bonamin + */ + +#define FW_VERSION "0.1" + + +/* + * ═══════════════════════════════════════════════════════════════════════════════ + * 📅 VERSION HISTORY: + * ═══════════════════════════════════════════════════════════════════════════════ + * v0.1 - Vesper Launch Beta + * ═══════════════════════════════════════════════════════════════════════════════ + */ + + + + + + + + + // ═══════════════════════════════════════════════════════════════════════════════════ // SYSTEM LIBRARIES - Core ESP32 and Arduino functionality // ═══════════════════════════════════════════════════════════════════════════════════ @@ -71,7 +98,6 @@ // ═══════════════════════════════════════════════════════════════════════════════════ // NETWORKING LIBRARIES - Advanced networking and communication // ═══════════════════════════════════════════════════════════════════════════════════ -#include // High-performance async MQTT client #include // WiFi configuration portal #include // Async web server for WebSocket support #include // UDP for discovery service @@ -98,16 +124,15 @@ #include "src/Telemetry/Telemetry.hpp" #include "src/OTAManager/OTAManager.hpp" #include "src/Networking/Networking.hpp" -#include "src/Communication/Communication.hpp" +#include "src/Communication/CommunicationRouter/CommunicationRouter.hpp" #include "src/ClientManager/ClientManager.hpp" -#include "src/Communication/ResponseBuilder.hpp" +#include "src/Communication/ResponseBuilder/ResponseBuilder.hpp" #include "src/Player/Player.hpp" #include "src/BellEngine/BellEngine.hpp" #include "src/OutputManager/OutputManager.hpp" #include "src/HealthMonitor/HealthMonitor.hpp" #include "src/FirmwareValidator/FirmwareValidator.hpp" #include "src/InputManager/InputManager.hpp" -#include "src/MqttSSL/MqttSSL.hpp" // Class Constructors ConfigManager configManager; @@ -115,13 +140,12 @@ FileManager fileManager(&configManager); Timekeeper timekeeper; Telemetry telemetry; OTAManager otaManager(configManager); -AsyncMqttClient mqttClient; Player player; AsyncWebServer server(80); AsyncWebSocket ws("/ws"); AsyncUDP udp; Networking networking(configManager); -Communication communication(configManager, otaManager, networking, mqttClient, server, ws, udp); +CommunicationRouter communication(configManager, otaManager, networking, server, ws, udp); HealthMonitor healthMonitor; FirmwareValidator firmwareValidator; InputManager inputManager; @@ -158,7 +182,7 @@ TimerHandle_t schedulerTimer; void handleFactoryReset() { - if (configManager.factoryReset()) { + if (configManager.resetAllToDefaults()) { delay(3000); ESP.restart(); } @@ -176,21 +200,22 @@ void setup() SPI.begin(hwConfig.ethSpiSck, hwConfig.ethSpiMiso, hwConfig.ethSpiMosi); delay(50); - // Initialize Configuration (this loads device identity from SD card) + // Initialize Configuration (loads factory identity from NVS + user settings from SD) configManager.begin(); inputManager.begin(); inputManager.setFactoryResetLongPressCallback(handleFactoryReset); - - // Set factory values: + // ═══════════════════════════════════════════════════════════════════════════════ + // REMOVED: Manual device identity setters + // Device identity (UID, hwType, hwVersion) is now READ-ONLY in production firmware + // These values are set by factory firmware and stored permanently in NVS + // Production firmware loads them once at boot and keeps them in RAM + // ═══════════════════════════════════════════════════════════════════════════════ - - configManager.setDeviceUID("PV202508190002"); - configManager.setHwType("BellPlus"); - configManager.setHwVersion("1.0"); - configManager.setFwVersion("1.1"); - LOG_INFO("Device identity initialized"); + // Update firmware version (this is the ONLY identity field that can be set) + configManager.setFwVersion(FW_VERSION); + LOG_INFO("Firmware version: %s", FW_VERSION); // Display device information after configuration is loaded @@ -277,12 +302,13 @@ void setup() // BellEngine already initialized and registered earlier for health validation - // Initialize Communication Manager + // Initialize Communication Manager (now with PubSubClient MQTT) communication.begin(); communication.setPlayerReference(&player); communication.setFileManagerReference(&fileManager); communication.setTimeKeeperReference(&timekeeper); communication.setFirmwareValidatorReference(&firmwareValidator); + player.setDependencies(&communication, &fileManager); player.setBellEngine(&bellEngine); // Connect the beast! @@ -294,7 +320,15 @@ void setup() // Set up network callbacks networking.setNetworkCallbacks( - []() { communication.onNetworkConnected(); }, // onConnected + []() { + communication.onNetworkConnected(); + // Start AsyncWebServer when network becomes available + if (networking.getState() != NetworkState::WIFI_PORTAL_MODE) { + LOG_INFO("🚀 Starting AsyncWebServer on port 80..."); + server.begin(); + LOG_INFO("✅ AsyncWebServer started on http://%s", networking.getLocalIP().c_str()); + } + }, // onConnected []() { communication.onNetworkDisconnected(); } // onDisconnected ); @@ -302,6 +336,14 @@ void setup() if (networking.isConnected()) { LOG_INFO("Network already connected - triggering MQTT connection"); communication.onNetworkConnected(); + + // 🔥 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..."); + server.begin(); + LOG_INFO("✅ AsyncWebServer started and listening on http://%s", networking.getLocalIP().c_str()); + } else { + LOG_WARNING("⚠️ Network not ready - AsyncWebServer will start after connection"); } delay(500); @@ -309,6 +351,7 @@ void setup() // Initialize OTA Manager and check for updates otaManager.begin(); otaManager.setFileManager(&fileManager); + otaManager.setPlayer(&player); // Set player reference for idle check // 🔥 CRITICAL: Delay OTA check to avoid UDP socket race with MQTT // Both MQTT and OTA HTTP use UDP sockets, must sequence them! @@ -320,8 +363,9 @@ void setup() // Register OTA Manager with health monitor healthMonitor.setOTAManager(&otaManager); - // Start the server - server.begin(); + // Note: AsyncWebServer will be started by network callbacks when connection is ready + // This avoids port 80 conflicts with WiFiManager's captive portal + // 🔥 START RUNTIME VALIDATION: All subsystems are now initialized // Begin extended runtime validation if we're in testing mode @@ -333,6 +377,7 @@ void setup() LOG_INFO("✅ Firmware already validated - normal operation mode"); } + // ═══════════════════════════════════════════════════════════════════════════════ // INITIALIZATION COMPLETE // ═══════════════════════════════════════════════════════════════════════════════ @@ -340,7 +385,7 @@ void setup() // • BellEngine creates high-priority timing task on Core 1 // • Telemetry creates monitoring task for load tracking // • Player creates duration timer for playback control - // • Communication creates MQTT reconnection timers + // • Communication creates MQTT task on Core 0 with PubSubClient // • Networking creates connection management timers // ✅ Bell configuration automatically loaded by ConfigManager // ✅ System ready for MQTT commands, WebSocket connections, and UDP discovery @@ -360,7 +405,7 @@ void setup() * • BellEngine: High-priority task on Core 1 for microsecond timing * • Telemetry: Background monitoring task for system health * • Player: Timer-based duration control for melody playback - * • Communication: Event-driven MQTT/WebSocket handling + * • Communication: MQTT task on Core 0 + Event-driven WebSocket * • Networking: Automatic connection management * * The main loop only handles lightweight operations that don't require @@ -371,26 +416,6 @@ void setup() */ void loop() { - // ═══════════════════════════════════════════════════════════════════════════════ - // INTENTIONALLY MINIMAL - ALL WORK DONE BY DEDICATED TASKS - // ═══════════════════════════════════════════════════════════════════════════════ - // - // The loop() function is kept empty by design to ensure maximum - // performance for the high-precision BellEngine running on Core 1. - // - // All system functionality is handled by dedicated FreeRTOS tasks: - // • 🔥 BellEngine: Microsecond-precision timing (Core 1, Priority 6) - // • 📊 Telemetry: System monitoring (Background task) - // • 🎵 Player: Duration management (FreeRTOS timers) - // • 📡 Communication: MQTT/WebSocket (Event-driven) - // • 🌐 Networking: Connection management (Timer-based) - // - // If you need to add periodic functionality, consider creating a new - // dedicated task instead of putting it here. - - // Uncomment the line below for debugging system status: - // Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap()); - // Feed watchdog only during firmware validation if (firmwareValidator.isInTestingMode()) { esp_task_wdt_reset(); @@ -403,6 +428,13 @@ void loop() } } + // 🔥 DEBUG: Log every 10 seconds to verify we're still running + static unsigned long lastLog = 0; + if (millis() - lastLog > 10000) { + LOG_DEBUG("❤️ Loop alive, free heap: %d", ESP.getFreeHeap()); + lastLog = millis(); + } + // Keep the loop responsive but not busy delay(100); // ⏱️ 100ms delay to prevent busy waiting }