Fixed MQTT and WS Routing - w/o SSL

This commit is contained in:
2025-10-13 17:34:54 +03:00
parent f696984cd1
commit 956786321a
29 changed files with 2043 additions and 1210 deletions

9
.gitignore vendored
View File

@@ -1,6 +1,9 @@
.vscode/
docs/
html/
vesper/build/ vesper/build/
vesper/.vscode/
vesper/docs/
vesper/sd_settings/ vesper/sd_settings/
vesper/CLAUDE.md vesper/CLAUDE.md
vesper/flutter/
vesper/docs_manual/
Doxyfile

View File

@@ -26,7 +26,7 @@
#include "../ConfigManager/ConfigManager.hpp" // Configuration and settings #include "../ConfigManager/ConfigManager.hpp" // Configuration and settings
#include "../Telemetry/Telemetry.hpp" // System monitoring and analytics #include "../Telemetry/Telemetry.hpp" // System monitoring and analytics
#include "../OutputManager/OutputManager.hpp" // Hardware abstraction layer #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 // CONSTRUCTOR & DESTRUCTOR IMPLEMENTATION
@@ -94,7 +94,7 @@ void BellEngine::begin() {
/** /**
* @brief Set Communication manager reference for bell notifications * @brief Set Communication manager reference for bell notifications
*/ */
void BellEngine::setCommunicationManager(Communication* commManager) { void BellEngine::setCommunicationManager(CommunicationRouter* commManager) {
_communicationManager = commManager; _communicationManager = commManager;
LOG_DEBUG("BellEngine: Communication manager %s", LOG_DEBUG("BellEngine: Communication manager %s",
commManager ? "connected" : "disconnected"); commManager ? "connected" : "disconnected");
@@ -181,6 +181,9 @@ void BellEngine::engineLoop() {
playbackLoop(); playbackLoop();
// Check telemetry for overloads and send notifications if needed
checkAndNotifyOverloads();
// Pause handling AFTER complete loop - never interrupt mid-melody! // Pause handling AFTER complete loop - never interrupt mid-melody!
while (_player.isPaused && _player.isPlaying && !_player.hardStop) { while (_player.isPaused && _player.isPlaying && !_player.hardStop) {
LOG_DEBUG("⏸️ Pausing between melody loops"); LOG_DEBUG("⏸️ Pausing between melody loops");
@@ -241,15 +244,15 @@ void BellEngine::activateNote(uint16_t note) {
// Iterate through each bit position (note index) // Iterate through each bit position (note index)
for (uint8_t noteIndex = 0; noteIndex < 16; noteIndex++) { for (uint8_t noteIndex = 0; noteIndex < 16; noteIndex++) {
if (note & (1 << noteIndex)) { if (note & (1 << noteIndex)) {
// Get bell mapping // Get bell mapping (noteAssignments stored as 1-indexed)
uint8_t bellIndex = _player.noteAssignments[noteIndex]; uint8_t bellConfig = _player.noteAssignments[noteIndex];
// Skip if no bell assigned // Skip if no bell assigned
if (bellIndex == 0) continue; if (bellConfig == 0) continue;
// Convert to 0-based indexing // Convert 1-indexed config to 0-indexed bellIndex
bellIndex = bellIndex - 1; uint8_t bellIndex = bellConfig - 1;
// Additional safety check to prevent underflow crashes // Additional safety check to prevent underflow crashes
if (bellIndex >= 255) { if (bellIndex >= 255) {
@@ -373,12 +376,6 @@ bool BellEngine::isHealthy() const {
return false; 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 // Check if OutputManager is properly connected and healthy
if (!_outputManager.isInitialized()) { if (!_outputManager.isInitialized()) {
LOG_DEBUG("BellEngine: Unhealthy - OutputManager not initialized"); LOG_DEBUG("BellEngine: Unhealthy - OutputManager not initialized");
@@ -387,3 +384,48 @@ bool BellEngine::isHealthy() const {
return true; return true;
} }
void BellEngine::checkAndNotifyOverloads() {
if (!_communicationManager) {
return; // No communication manager available
}
// Collect overloaded bells from telemetry
std::vector<uint8_t> criticalBells;
std::vector<uint16_t> criticalLoads;
std::vector<uint8_t> warningBells;
std::vector<uint16_t> 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");
}
}

View File

@@ -60,7 +60,7 @@ class Player; // Melody playback controller
class ConfigManager; // Configuration and settings management class ConfigManager; // Configuration and settings management
class Telemetry; // System monitoring and analytics class Telemetry; // System monitoring and analytics
class OutputManager; // Hardware abstraction layer class OutputManager; // Hardware abstraction layer
class Communication; // Communication system for notifications class CommunicationRouter; // Communication system for notifications
// ═════════════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════════════
// ARCHITECTURE MIGRATION NOTE // ARCHITECTURE MIGRATION NOTE
@@ -221,7 +221,7 @@ public:
* @brief Set Communication manager reference for bell notifications * @brief Set Communication manager reference for bell notifications
* @param commManager Pointer to communication manager * @param commManager Pointer to communication manager
*/ */
void setCommunicationManager(Communication* commManager); void setCommunicationManager(CommunicationRouter* commManager);
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
// HEALTH CHECK METHOD // HEALTH CHECK METHOD
@@ -239,7 +239,7 @@ private:
ConfigManager& _configManager; // Configuration manager for bell settings ConfigManager& _configManager; // Configuration manager for bell settings
Telemetry& _telemetry; // System monitoring and strike tracking Telemetry& _telemetry; // System monitoring and strike tracking
OutputManager& _outputManager; // 🔥 Hardware abstraction layer for relay control 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) // ENGINE STATE (Atomic for thread safety)
@@ -348,6 +348,15 @@ private:
*/ */
void emergencyShutdown(); 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 * @brief Notify WebSocket clients of fired bells
* @param bellIndices Vector of bell indices that were fired (1-indexed) * @param bellIndices Vector of bell indices that were fired (1-indexed)

View File

@@ -1,421 +1,55 @@
#include "Communication.hpp" /*
#include "../ConfigManager/ConfigManager.hpp" * COMMANDHANDLER.CPP - Command Processing Implementation
#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 <WiFiClientSecure.h>
Communication* Communication::_instance = nullptr; #include "CommandHandler.hpp"
StaticJsonDocument<2048> Communication::_parseDocument; #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) { CommandHandler::CommandHandler(ConfigManager& configManager, OTAManager& otaManager)
if (Communication::_instance) {
Communication::_instance->connectToMqtt();
}
}
Communication::Communication(ConfigManager& configManager,
OTAManager& otaManager,
Networking& networking,
AsyncMqttClient& mqttClient,
AsyncWebServer& server,
AsyncWebSocket& webSocket,
AsyncUDP& udp)
: _configManager(configManager) : _configManager(configManager)
, _otaManager(otaManager) , _otaManager(otaManager)
, _networking(networking)
, _mqttClient(mqttClient)
, _server(server)
, _webSocket(webSocket)
, _udp(udp)
, _player(nullptr) , _player(nullptr)
, _fileManager(nullptr) , _fileManager(nullptr)
, _timeKeeper(nullptr) , _timeKeeper(nullptr)
, _firmwareValidator(nullptr) , _firmwareValidator(nullptr)
, _mqttReconnectTimer(nullptr) { , _clientManager(nullptr)
, _responseCallback(nullptr) {}
_instance = this; CommandHandler::~CommandHandler() {}
void CommandHandler::setPlayerReference(Player* player) {
_player = player;
} }
Communication::~Communication() { void CommandHandler::setFileManagerReference(FileManager* fm) {
if (_mqttReconnectTimer != nullptr) { _fileManager = fm;
xTimerDelete(_mqttReconnectTimer, portMAX_DELAY);
_mqttReconnectTimer = nullptr;
}
_instance = nullptr;
} }
void Communication::begin() { void CommandHandler::setTimeKeeperReference(Timekeeper* tk) {
LOG_INFO("Initializing Communication Manager v2.1"); _timeKeeper = tk;
initMqtt();
initWebSocket();
LOG_INFO("Communication Manager initialized with multi-client support");
} }
void Communication::initMqtt() { void CommandHandler::setFirmwareValidatorReference(FirmwareValidator* fv) {
_mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE, _firmwareValidator = fv;
(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 Communication::initWebSocket() { void CommandHandler::setClientManagerReference(ClientManager* cm) {
_webSocket.onEvent(onWebSocketEvent); _clientManager = cm;
_server.addHandler(&_webSocket);
} }
void Communication::connectToMqtt() { void CommandHandler::setResponseCallback(ResponseCallback callback) {
if (_networking.isConnected()) { _responseCallback = callback;
LOG_INFO("Connecting to MQTT...");
_mqttClient.connect();
} else {
LOG_WARNING("Cannot connect to MQTT: No network connection");
}
} }
void Communication::subscribeMqtt() { void CommandHandler::processCommand(JsonDocument& command, const MessageContext& context) {
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<uint8_t>& bellNumbers,
const std::vector<uint16_t>& bellLoads,
const String& severity) {
StaticJsonDocument<512> overloadMsg;
overloadMsg["status"] = "INFO";
overloadMsg["type"] = "bell_overload";
JsonArray bellsArray = overloadMsg["payload"]["bells"].to<JsonArray>();
JsonArray loadsArray = overloadMsg["payload"]["loads"].to<JsonArray>();
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) {
String cmd = command["cmd"]; String cmd = command["cmd"];
JsonVariant contents = command["contents"]; 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(); String response = ResponseBuilder::pong();
sendResponse(response, context); sendResponse(response, context);
} }
void Communication::handleStatusCommand(const MessageContext& context) { void CommandHandler::handleStatusCommand(const MessageContext& context) {
PlayerStatus playerStatus = _player ? _player->getStatus() : PlayerStatus::STOPPED; PlayerStatus playerStatus = _player ? _player->getStatus() : PlayerStatus::STOPPED;
uint32_t timeElapsedMs = 0; uint32_t timeElapsedMs = 0;
uint64_t projectedRunTime = 0; uint64_t projectedRunTime = 0;
@@ -468,12 +118,19 @@ void Communication::handleStatusCommand(const MessageContext& context) {
sendResponse(response, 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) { if (context.source != MessageSource::WEBSOCKET) {
sendErrorResponse("identify", "Identify command only available via WebSocket", context); sendErrorResponse("identify", "Identify command only available via WebSocket", context);
return; 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")) { if (!contents.containsKey("device_type")) {
sendErrorResponse("identify", "Missing device_type parameter", context); sendErrorResponse("identify", "Missing device_type parameter", context);
return; return;
@@ -489,7 +146,7 @@ void Communication::handleIdentifyCommand(JsonVariant contents, const MessageCon
} }
if (deviceType != ClientManager::DeviceType::UNKNOWN) { if (deviceType != ClientManager::DeviceType::UNKNOWN) {
_clientManager.updateClientType(context.clientId, deviceType); _clientManager->updateClientType(context.clientId, deviceType);
sendSuccessResponse("identify", "Device identified as " + deviceTypeStr, context); sendSuccessResponse("identify", "Device identified as " + deviceTypeStr, context);
LOG_INFO("Client #%u identified as %s device", context.clientId, deviceTypeStr.c_str()); LOG_INFO("Client #%u identified as %s device", context.clientId, deviceTypeStr.c_str());
} else { } 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) { if (_player) {
bool success = _player->command(contents); 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")) { if (!contents.containsKey("action")) {
sendErrorResponse("file_manager", "Missing action parameter", context); sendErrorResponse("file_manager", "Missing action parameter", context);
return; 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")) { if (!contents.containsKey("action")) {
sendErrorResponse("relay_setup", "Missing action parameter", context); sendErrorResponse("relay_setup", "Missing action parameter", context);
return; 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")) { if (!contents.containsKey("action")) {
sendErrorResponse("clock_setup", "Missing action parameter", context); sendErrorResponse("clock_setup", "Missing action parameter", context);
return; 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")) { if (!contents.containsKey("action")) {
sendErrorResponse("system_info", "Missing action parameter", context); sendErrorResponse("system_info", "Missing action parameter", context);
return; return;
@@ -606,13 +263,15 @@ void Communication::handleSystemInfoCommand(JsonVariant contents, const MessageC
handleRollbackFirmwareCommand(context); handleRollbackFirmwareCommand(context);
} else if (action == "get_firmware_status") { } else if (action == "get_firmware_status") {
handleGetFirmwareStatusCommand(context); handleGetFirmwareStatusCommand(context);
} else if (action == "get_full_settings") {
handleGetFullSettingsCommand(context);
} else { } else {
LOG_WARNING("Unknown system info action: %s", action.c_str()); LOG_WARNING("Unknown system info action: %s", action.c_str());
sendErrorResponse("system_info", "Unknown action: " + action, context); sendErrorResponse("system_info", "Unknown action: " + action, context);
} }
} }
void Communication::handleListMelodiesCommand(const MessageContext& context) { void CommandHandler::handleListMelodiesCommand(const MessageContext& context) {
if (!_fileManager) { if (!_fileManager) {
sendErrorResponse("list_melodies", "FileManager not available", context); sendErrorResponse("list_melodies", "FileManager not available", context);
return; return;
@@ -639,7 +298,7 @@ void Communication::handleListMelodiesCommand(const MessageContext& context) {
sendResponse(responseStr, context); sendResponse(responseStr, context);
} }
void Communication::handleDownloadMelodyCommand(JsonVariant contents, const MessageContext& context) { void CommandHandler::handleDownloadMelodyCommand(JsonVariant contents, const MessageContext& context) {
if (!_fileManager) { if (!_fileManager) {
sendErrorResponse("download_melody", "FileManager not available", context); sendErrorResponse("download_melody", "FileManager not available", context);
return; return;
@@ -653,7 +312,7 @@ void Communication::handleDownloadMelodyCommand(JsonVariant contents, const Mess
sendResponse(response, context); sendResponse(response, context);
} }
void Communication::handleDeleteMelodyCommand(JsonVariant contents, const MessageContext& context) { void CommandHandler::handleDeleteMelodyCommand(JsonVariant contents, const MessageContext& context) {
if (!_fileManager) { if (!_fileManager) {
sendErrorResponse("delete_melody", "FileManager not available", context); sendErrorResponse("delete_melody", "FileManager not available", context);
return; 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 { try {
_configManager.updateBellDurations(contents); _configManager.updateBellDurations(contents);
// Save configuration to ensure persistence // 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 { try {
_configManager.updateBellOutputs(contents); _configManager.updateBellOutputs(contents);
// Note: Bell outputs are typically not persisted to SD card as they're more of a mapping configuration // 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 { try {
_configManager.updateClockOutputs(contents); _configManager.updateClockOutputs(contents);
// Save configuration to ensure persistence // 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 { try {
_configManager.updateClockOutputs(contents); _configManager.updateClockOutputs(contents);
// Save configuration to ensure persistence // 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 { try {
_configManager.updateClockAlerts(contents); _configManager.updateClockAlerts(contents);
// Save configuration to ensure persistence // 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 { try {
_configManager.updateClockBacklight(contents); _configManager.updateClockBacklight(contents);
// Save configuration to ensure persistence // 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 { try {
_configManager.updateClockSilence(contents); _configManager.updateClockSilence(contents);
// Save configuration to ensure persistence // 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) { if (!_timeKeeper) {
sendErrorResponse("set_rtc_time", "TimeKeeper not available", context); sendErrorResponse("set_rtc_time", "TimeKeeper not available", context);
return; 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")) { if (!contents.containsKey("hour") || !contents.containsKey("minute")) {
sendErrorResponse("set_physical_clock_time", "Missing hour or minute parameter", context); sendErrorResponse("set_physical_clock_time", "Missing hour or minute parameter", context);
return; return;
@@ -886,8 +545,12 @@ void Communication::handleSetPhysicalClockTimeCommand(JsonVariant contents, cons
return; 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 // Set the physical clock time using ConfigManager
_configManager.setPhysicalClockHour(hour); _configManager.setPhysicalClockHour(clockHour);
_configManager.setPhysicalClockMinute(minute); _configManager.setPhysicalClockMinute(minute);
_configManager.setLastSyncTime(millis() / 1000); _configManager.setLastSyncTime(millis() / 1000);
@@ -896,15 +559,15 @@ void Communication::handleSetPhysicalClockTimeCommand(JsonVariant contents, cons
if (saved) { if (saved) {
sendSuccessResponse("set_physical_clock_time", "Physical clock time updated and saved successfully", context); 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 { } else {
sendErrorResponse("set_physical_clock_time", "Physical clock time updated but failed to save to SD card", context); 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); LOG_ERROR("Physical clock time set to %02d:%02d but failed to save to SD", hour, minute);
} }
} }
void CommandHandler::handlePauseClockUpdatesCommand(JsonVariant contents, const MessageContext& context) {
void Communication::handlePauseClockUpdatesCommand(JsonVariant contents, const MessageContext& context) {
if (!_timeKeeper) { if (!_timeKeeper) {
return; 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")) { if (!contents.containsKey("enabled")) {
sendErrorResponse("set_clock_enabled", "Missing enabled parameter", context); sendErrorResponse("set_clock_enabled", "Missing enabled parameter", context);
return; return;
@@ -948,29 +611,7 @@ void Communication::handleSetClockEnabledCommand(JsonVariant contents, const Mes
} }
} }
void CommandHandler::handleGetDeviceTimeCommand(const MessageContext& context) {
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) {
StaticJsonDocument<256> response; StaticJsonDocument<256> response;
response["status"] = "SUCCESS"; response["status"] = "SUCCESS";
response["type"] = "device_time"; response["type"] = "device_time";
@@ -1003,7 +644,7 @@ void Communication::handleGetDeviceTimeCommand(const MessageContext& context) {
LOG_DEBUG("Device time requested"); LOG_DEBUG("Device time requested");
} }
void Communication::handleGetClockTimeCommand(const MessageContext& context) { void CommandHandler::handleGetClockTimeCommand(const MessageContext& context) {
StaticJsonDocument<256> response; StaticJsonDocument<256> response;
response["status"] = "SUCCESS"; response["status"] = "SUCCESS";
response["type"] = "clock_time"; response["type"] = "clock_time";
@@ -1025,80 +666,11 @@ void Communication::handleGetClockTimeCommand(const MessageContext& context) {
_configManager.getLastSyncTime()); _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 // FIRMWARE MANAGEMENT IMPLEMENTATION
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
void Communication::handleCommitFirmwareCommand(const MessageContext& context) { void CommandHandler::handleCommitFirmwareCommand(const MessageContext& context) {
if (!_firmwareValidator) { if (!_firmwareValidator) {
sendErrorResponse("commit_firmware", "FirmwareValidator not available", context); sendErrorResponse("commit_firmware", "FirmwareValidator not available", context);
return; 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) { if (!_firmwareValidator) {
sendErrorResponse("rollback_firmware", "FirmwareValidator not available", context); sendErrorResponse("rollback_firmware", "FirmwareValidator not available", context);
return; 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) { if (!_firmwareValidator) {
sendErrorResponse("get_firmware_status", "FirmwareValidator not available", context); sendErrorResponse("get_firmware_status", "FirmwareValidator not available", context);
return; return;
@@ -1217,18 +789,44 @@ void Communication::handleGetFirmwareStatusCommand(const MessageContext& context
LOG_DEBUG("Firmware status requested: %s", stateStr.c_str()); LOG_DEBUG("Firmware status requested: %s", stateStr.c_str());
} }
// ════════════════════════════════════════════════════════════════════════════ void CommandHandler::handleGetFullSettingsCommand(const MessageContext& context) {
// NETWORK CONFIGURATION IMPLEMENTATION 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<JsonObject>();
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 // Validate that we have at least one parameter to update
bool hasHostname = contents.containsKey("hostname"); bool hasHostname = contents.containsKey("hostname");
bool hasStaticIPConfig = contents.containsKey("useStaticIP"); 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); sendErrorResponse("set_network_config", "No network parameters provided", context);
return; return;
} }
@@ -1248,8 +846,6 @@ void Communication::handleSetNetworkConfigCommand(JsonVariant contents, const Me
IPAddress dns1 = currentConfig.dns1; IPAddress dns1 = currentConfig.dns1;
IPAddress dns2 = currentConfig.dns2; IPAddress dns2 = currentConfig.dns2;
String hostname = currentConfig.hostname; String hostname = currentConfig.hostname;
String apPass = currentConfig.apPass;
uint16_t discoveryPort = currentConfig.discoveryPort;
// Update hostname if provided // Update hostname if provided
if (hasHostname) { if (hasHostname) {
@@ -1310,16 +906,10 @@ void Communication::handleSetNetworkConfigCommand(JsonVariant contents, const Me
// If anything changed, update and save configuration // If anything changed, update and save configuration
if (configChanged) { if (configChanged) {
// Update network config using existing method // Update network config (saves to SD internally)
_configManager.updateNetworkConfig(useStaticIP, ip, gateway, subnet, dns1, dns2); _configManager.updateNetworkConfig(hostname, useStaticIP, ip, gateway, subnet, dns1, dns2);
// Manually update fields not handled by updateNetworkConfig bool saved = true; // saveNetworkConfig() already called in updateNetworkConfig()
// Note: This is a workaround since NetworkConfig doesn't have setters for all fields
auto& writableConfig = const_cast<ConfigManager::NetworkConfig&>(_configManager.getNetworkConfig());
writableConfig.hostname = hostname;
// Save to SD card
bool saved = _configManager.saveNetworkConfig();
if (saved) { if (saved) {
String responseMsg = "Network configuration updated successfully"; String responseMsg = "Network configuration updated successfully";
@@ -1344,3 +934,63 @@ void Communication::handleSetNetworkConfigCommand(JsonVariant contents, const Me
LOG_ERROR("Unknown exception in handleSetNetworkConfigCommand"); 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);
}
}

View File

@@ -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 <Arduino.h>
#include <ArduinoJson.h>
// 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<void(const String& response, const MessageContext& context)>;
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);
};

View File

@@ -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 <Arduino.h>
#include <AsyncMqttClient.h>
#include <ESPAsyncWebServer.h>
#include <AsyncUDP.h>
#include <ArduinoJson.h>
#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<uint8_t>& bellNumbers,
const std::vector<uint16_t>& 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.
};

View File

@@ -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<uint8_t>& bellNumbers,
const std::vector<uint16_t>& bellLoads,
const String& severity) {
StaticJsonDocument<512> overloadMsg;
overloadMsg["status"] = "INFO";
overloadMsg["type"] = "bell_overload";
JsonArray bellsArray = overloadMsg["payload"]["bells"].to<JsonArray>();
JsonArray loadsArray = overloadMsg["payload"]["loads"].to<JsonArray>();
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<JsonDocument&>(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!");
}
}

View File

@@ -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 <Arduino.h>
#include <ESPAsyncWebServer.h>
#include <AsyncUDP.h>
#include <ArduinoJson.h>
#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<uint8_t>& bellNumbers,
const std::vector<uint16_t>& 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);
};

View File

@@ -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<int>(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();
}
}

View File

@@ -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 <Arduino.h>
#include <AsyncMqttClient.h>
class ConfigManager;
class Networking;
class MQTTAsyncClient {
public:
static MQTTAsyncClient* _instance;
// Message callback type
using MessageCallback = std::function<void(const String& topic, const String& payload)>;
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);
};

View File

@@ -1,5 +1,5 @@
#include "ResponseBuilder.hpp" #include "ResponseBuilder.hpp"
#include "../Logging/Logging.hpp" #include "../../Logging/Logging.hpp"
// Static member initialization // Static member initialization
StaticJsonDocument<512> ResponseBuilder::_responseDoc; StaticJsonDocument<512> ResponseBuilder::_responseDoc;

View File

@@ -43,7 +43,7 @@
#include <Arduino.h> #include <Arduino.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "../Player/Player.hpp" // For PlayerStatus enum #include "../../Player/Player.hpp" // For PlayerStatus enum
class ResponseBuilder { class ResponseBuilder {
public: public:

View File

@@ -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());
}
}

View File

@@ -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 <Arduino.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
#include "../../ClientManager/ClientManager.hpp"
class WebSocketServer {
public:
// Message callback type
using MessageCallback = std::function<void(uint32_t clientId, const JsonDocument& message)>;
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;
};

View File

@@ -44,7 +44,7 @@ void ConfigManager::createDefaultBellConfig() {
// Initialize default durations (90ms for all bells) // Initialize default durations (90ms for all bells)
for (uint8_t i = 0; i < 16; i++) { for (uint8_t i = 0; i < 16; i++) {
bellConfig.durations[i] = 90; 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) // Step 4: Load device configuration from SD card (firmware version only)
if (!loadDeviceConfig()) { 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 // Step 5: Load update servers list
@@ -78,12 +79,32 @@ bool ConfigManager::begin() {
LOG_WARNING("ConfigManager: Could not load update servers - using fallback only"); 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(); loadFromSD();
loadNetworkConfig(); // Load network configuration (hostname, static IP settings)
loadBellDurations(); // Load network config, save defaults if not found
loadClockConfig(); // Load clock configuration (C1/C2 outputs, pulse durations) if (!loadNetworkConfig()) {
loadClockState(); // Load physical clock state (hour, minute, position) 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", LOG_INFO("ConfigManager: Initialization complete - UID: %s, Hostname: %s",
deviceConfig.deviceUID.c_str(), networkConfig.hostname.c_str()); deviceConfig.deviceUID.c_str(), networkConfig.hostname.c_str());
@@ -123,42 +144,35 @@ bool ConfigManager::loadDeviceIdentityFromNVS() {
return false; return false;
} }
deviceConfig.deviceUID = readNVSString(NVS_DEVICE_UID_KEY, "PV000000000000"); // Read factory-set device identity from NVS (READ-ONLY)
deviceConfig.hwType = readNVSString(NVS_HW_TYPE_KEY, "BellSystems"); deviceConfig.deviceUID = readNVSString(NVS_DEVICE_UID_KEY, "");
deviceConfig.hwVersion = readNVSString(NVS_HW_VERSION_KEY, "0"); 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", // Validate that factory identity exists
deviceConfig.deviceUID.c_str(), if (deviceConfig.deviceUID.isEmpty() || deviceConfig.hwType.isEmpty() || deviceConfig.hwVersion.isEmpty()) {
deviceConfig.hwType.c_str(), LOG_ERROR("═══════════════════════════════════════════════════════════════════════════");
deviceConfig.hwVersion.c_str()); 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; return true;
} }
bool ConfigManager::saveDeviceIdentityToNVS() { // REMOVED: saveDeviceIdentityToNVS() - Production firmware MUST NOT write device identity
if (nvsHandle == 0) { // Device identity (UID, hwType, hwVersion) is factory-set ONLY and stored in NVS by factory firmware
LOG_ERROR("ConfigManager: NVS not initialized, cannot save device identity"); // Production firmware reads these values once at boot and keeps them in RAM
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;
}
String ConfigManager::readNVSString(const char* key, const String& defaultValue) { String ConfigManager::readNVSString(const char* key, const String& defaultValue) {
if (nvsHandle == 0) { if (nvsHandle == 0) {
@@ -195,21 +209,8 @@ String ConfigManager::readNVSString(const char* key, const String& defaultValue)
return result; return result;
} }
bool ConfigManager::writeNVSString(const char* key, const String& value) { // REMOVED: writeNVSString() - Production firmware MUST NOT write to NVS
if (nvsHandle == 0) { // All device identity is factory-set and read-only in production firmware
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;
}
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
// STANDARD SD CARD FUNCTIONALITY // STANDARD SD CARD FUNCTIONALITY
@@ -327,7 +328,7 @@ bool ConfigManager::loadBellDurations() {
File file = SD.open("/settings/relayTimings.json", FILE_READ); File file = SD.open("/settings/relayTimings.json", FILE_READ);
if (!file) { 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; return false;
} }
@@ -681,8 +682,9 @@ void ConfigManager::updateTimeConfig(long gmtOffsetSec, int daylightOffsetSec) {
gmtOffsetSec, 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) { IPAddress subnet, IPAddress dns1, IPAddress dns2) {
networkConfig.hostname = hostname;
networkConfig.useStaticIP = useStaticIP; networkConfig.useStaticIP = useStaticIP;
networkConfig.ip = ip; networkConfig.ip = ip;
networkConfig.gateway = gateway; networkConfig.gateway = gateway;
@@ -690,8 +692,8 @@ void ConfigManager::updateNetworkConfig(bool useStaticIP, IPAddress ip, IPAddres
networkConfig.dns1 = dns1; networkConfig.dns1 = dns1;
networkConfig.dns2 = dns2; networkConfig.dns2 = dns2;
saveNetworkConfig(); // Save immediately to SD saveNetworkConfig(); // Save immediately to SD
LOG_INFO("ConfigManager: NetworkConfig updated - Static IP: %s, IP: %s", LOG_INFO("ConfigManager: NetworkConfig updated - Hostname: %s, Static IP: %s, IP: %s",
useStaticIP ? "enabled" : "disabled", ip.toString().c_str()); 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() { bool ConfigManager::resetAllToDefaults() {
LOG_WARNING("═══════════════════════════════════════════════════════════════════════════");
LOG_WARNING("🏭 FACTORY RESET INITIATED"); LOG_WARNING("═══════════════════════════════════════════════════════════════════════════");
LOG_WARNING("═══════════════════════════════════════════════════════════════════════════"); LOG_WARNING(" 🏭 RESET SETTINGS TO DEFAULTS 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::clearAllSettings() {
if (!ensureSDCard()) { if (!ensureSDCard()) {
LOG_ERROR("ConfigManager: SD card not available for clearing settings"); LOG_ERROR("ConfigManager: Cannot perform reset - SD card not available");
return false; return false;
} }
bool allDeleted = true; bool allDeleted = true;
int filesDeleted = 0; int filesDeleted = 0;
int filesFailed = 0; int filesFailed = 0;
// List of all configuration files to delete // ════════════════════════════════════════════════════════════════════════════
// STEP 1: Delete all configuration files
// ════════════════════════════════════════════════════════════════════════════
const char* settingsFiles[] = { const char* settingsFiles[] = {
"/settings/deviceConfig.json", "/settings/deviceConfig.json",
"/settings/networkConfig.json", "/settings/networkConfig.json",
@@ -903,15 +837,14 @@ bool ConfigManager::clearAllSettings() {
int numFiles = sizeof(settingsFiles) / sizeof(settingsFiles[0]); 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++) { for (int i = 0; i < numFiles; i++) {
const char* filepath = settingsFiles[i]; const char* filepath = settingsFiles[i];
if (SD.exists(filepath)) { if (SD.exists(filepath)) {
if (SD.remove(filepath)) { if (SD.remove(filepath)) {
LOG_INFO("✅ Deleted: %s", filepath); LOG_DEBUG("✅ Deleted: %s", filepath);
filesDeleted++; filesDeleted++;
} else { } else {
LOG_ERROR("❌ Failed to delete: %s", filepath); 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")) { if (SD.exists("/melodies")) {
LOG_INFO("Deleting /melodies directory..."); LOG_WARNING("🗑️ Step 2: Deleting melody files...");
// Note: SD library doesn't have rmdir for non-empty dirs
// You'd need to implement recursive delete or just leave melodies 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:"); // SUMMARY
LOG_INFO(" ✅ Files deleted: %d", filesDeleted); // ════════════════════════════════════════════════════════════════════════════
LOG_INFO(" ❌ Files failed: %d", filesFailed);
LOG_INFO(" 🔄 Total processed: %d / %d", filesDeleted + filesFailed, numFiles);
LOG_INFO("═══════════════════════════════════════════════════════════════════════════");
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; 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;
}

View File

@@ -75,14 +75,15 @@ public:
* @struct MqttConfig * @struct MqttConfig
* @brief MQTT broker connection settings * @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. * Username defaults to deviceUID for unique identification.
*/ */
struct MqttConfig { struct MqttConfig {
String host = "j2f24f16.ala.eu-central-1.emqxsl.com"; // 📡 Cloud MQTT broker (default) IPAddress host = IPAddress(145, 223, 96, 251); // 📡 Local Mosquitto broker
int port = 1883; // 🔌 Standard MQTT port (default) int port = 1883; // 🔌 Standard MQTT port (non-SSL)
String user; // 👤 Auto-set to deviceUID String user; // 👤 Auto-set to deviceUID
String password = "vesper"; // 🔑 Default password - OK as is String password = "vesper"; // 🔑 Default password
bool useSSL = false; // 🔒 SSL disabled for local broker
}; };
/** /**
@@ -249,12 +250,11 @@ private:
bool ensureSDCard(); bool ensureSDCard();
void createDefaultBellConfig(); void createDefaultBellConfig();
// NVS management (for factory-set device identity) // NVS management (READ-ONLY for factory-set device identity)
bool initializeNVS(); bool initializeNVS();
bool loadDeviceIdentityFromNVS(); bool loadDeviceIdentityFromNVS();
bool saveDeviceIdentityToNVS();
String readNVSString(const char* key, const String& defaultValue); 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: public:
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
@@ -287,37 +287,20 @@ public:
const BellConfig& getBellConfig() const { return bellConfig; } const BellConfig& getBellConfig() const { return bellConfig; }
const ClockConfig& getClockConfig() const { return clockConfig; } 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 getDeviceUID() const { return deviceConfig.deviceUID; }
String getHwType() const { return deviceConfig.hwType; } String getHwType() const { return deviceConfig.hwType; }
String getHwVersion() const { return deviceConfig.hwVersion; } String getHwVersion() const { return deviceConfig.hwVersion; }
String getFwVersion() const { return deviceConfig.fwVersion; } 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) */ /** @brief Set firmware version (auto-updated after OTA) */
void setFwVersion(const String& version) { deviceConfig.fwVersion = version; } void setFwVersion(const String& version) { deviceConfig.fwVersion = version; }
// Configuration update methods for app commands // Configuration update methods for app commands
void updateTimeConfig(long gmtOffsetSec, int daylightOffsetSec); 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); IPAddress subnet, IPAddress dns1, IPAddress dns2);
// Network configuration persistence // Network configuration persistence
@@ -352,7 +335,7 @@ public:
uint8_t getPhysicalClockMinute() const { return clockConfig.physicalMinute; } uint8_t getPhysicalClockMinute() const { return clockConfig.physicalMinute; }
bool getNextOutputIsC1() const { return clockConfig.nextOutputIsC1; } bool getNextOutputIsC1() const { return clockConfig.nextOutputIsC1; }
uint32_t getLastSyncTime() const { return clockConfig.lastSyncTime; } 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 setPhysicalClockMinute(uint8_t minute) { clockConfig.physicalMinute = minute % 60; }
void setNextOutputIsC1(bool isC1) { clockConfig.nextOutputIsC1 = isC1; } void setNextOutputIsC1(bool isC1) { clockConfig.nextOutputIsC1 = isC1; }
void setLastSyncTime(uint32_t timestamp) { clockConfig.lastSyncTime = timestamp; } void setLastSyncTime(uint32_t timestamp) { clockConfig.lastSyncTime = timestamp; }
@@ -412,33 +395,34 @@ public:
String getAPSSID() const { return networkConfig.apSsid; } String getAPSSID() const { return networkConfig.apSsid; }
bool isHealthy() const; bool isHealthy() const;
/**
* @brief Get all configuration settings as a JSON string
* @return JSON string containing all current settings
*/
String getAllSettingsAsJson() const;
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
// FACTORY RESET // FACTORY RESET
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
/** /**
* @brief Perform complete factory reset * @brief Perform a full Reset of the Settings to Factory Defaults
* *
* This method: * This method:
* 1. Deletes all configuration files from SD card * 1. Clears all user Settings on the SD card
* 2. Does NOT touch NVS (device identity remains) * 2. Deletes all Melodies on the SD card
* 3. On next boot, all settings will be recreated with defaults
* *
* @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: // DEPLOYMENT NOTES:
// 2. USER SETTINGS: All loaded from SD card, configured via app // 2. USER SETTINGS: All loaded from SD card, configurable via app
// 3. NETWORK: WiFiManager handles credentials, no hardcoded SSIDs/passwords // 3. NETWORK: WiFiManager handles SSIDs/Passwords. IP Settigns loaded from SD, configurable via app
// 4. IDENTIFIERS: Auto-generated from deviceUID for consistency // 4. IDENTIFIERS: Auto-generated from deviceUID for consistency
// 5. DEFAULTS: Clean minimal defaults, everything configurable // 5. DEFAULTS: Clean minimal defaults, everything configurable
// ═══════════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════════

View File

@@ -7,7 +7,7 @@
#include "HealthMonitor.hpp" #include "HealthMonitor.hpp"
#include "../BellEngine/BellEngine.hpp" #include "../BellEngine/BellEngine.hpp"
#include "../OutputManager/OutputManager.hpp" #include "../OutputManager/OutputManager.hpp"
#include "../Communication/Communication.hpp" #include "../Communication/CommunicationRouter/CommunicationRouter.hpp"
#include "../Player/Player.hpp" #include "../Player/Player.hpp"
#include "../TimeKeeper/TimeKeeper.hpp" #include "../TimeKeeper/TimeKeeper.hpp"
#include "../Telemetry/Telemetry.hpp" #include "../Telemetry/Telemetry.hpp"

View File

@@ -57,7 +57,7 @@
// Forward declarations for all monitored subsystems // Forward declarations for all monitored subsystems
class BellEngine; class BellEngine;
class OutputManager; class OutputManager;
class Communication; class CommunicationRouter;
class Player; class Player;
class Timekeeper; class Timekeeper;
class Telemetry; class Telemetry;
@@ -137,7 +137,7 @@ public:
void setOutputManager(OutputManager* outputManager) { _outputManager = outputManager; } void setOutputManager(OutputManager* outputManager) { _outputManager = outputManager; }
/** @brief Register Communication for monitoring */ /** @brief Register Communication for monitoring */
void setCommunication(Communication* communication) { _communication = communication; } void setCommunication(CommunicationRouter* communication) { _communication = communication; }
/** @brief Register Player for monitoring */ /** @brief Register Player for monitoring */
void setPlayer(Player* player) { _player = player; } void setPlayer(Player* player) { _player = player; }
@@ -256,7 +256,7 @@ private:
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
BellEngine* _bellEngine = nullptr; BellEngine* _bellEngine = nullptr;
OutputManager* _outputManager = nullptr; OutputManager* _outputManager = nullptr;
Communication* _communication = nullptr; CommunicationRouter* _communication = nullptr;
Player* _player = nullptr; Player* _player = nullptr;
Timekeeper* _timeKeeper = nullptr; Timekeeper* _timeKeeper = nullptr;
Telemetry* _telemetry = nullptr; Telemetry* _telemetry = nullptr;

View File

@@ -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);
}
}

View File

@@ -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 <Arduino.h>
#include <AsyncMqttClient.h>
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;
};

View File

@@ -12,9 +12,9 @@ Networking::Networking(ConfigManager& configManager)
, _state(NetworkState::DISCONNECTED) , _state(NetworkState::DISCONNECTED)
, _activeConnection(ConnectionType::NONE) , _activeConnection(ConnectionType::NONE)
, _lastConnectionAttempt(0) , _lastConnectionAttempt(0)
, _bootStartTime(0)
, _bootSequenceComplete(false) , _bootSequenceComplete(false)
, _ethernetCableConnected(false) , _ethernetCableConnected(false)
, _wifiConnectionFailures(0)
, _wifiManager(nullptr) , _wifiManager(nullptr)
, _reconnectionTimer(nullptr) { , _reconnectionTimer(nullptr) {
@@ -46,11 +46,10 @@ Networking::~Networking() {
} }
} }
void Networking::begin() { void Networking::begin() {
LOG_INFO("Initializing Networking System"); LOG_INFO("Initializing Networking System");
_bootStartTime = millis();
// Create reconnection timer // Create reconnection timer
_reconnectionTimer = xTimerCreate("reconnectionTimer", pdMS_TO_TICKS(RECONNECTION_INTERVAL), _reconnectionTimer = xTimerCreate("reconnectionTimer", pdMS_TO_TICKS(RECONNECTION_INTERVAL),
pdTRUE, (void*)0, reconnectionTimerCallback); pdTRUE, (void*)0, reconnectionTimerCallback);
@@ -60,7 +59,10 @@ void Networking::begin() {
// Configure WiFiManager // Configure WiFiManager
_wifiManager->setDebugOutput(false); _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 // Start Ethernet hardware
auto& hwConfig = _configManager.getHardwareConfig(); auto& hwConfig = _configManager.getHardwareConfig();
@@ -98,13 +100,16 @@ void Networking::startWiFiConnection() {
if (!hasValidWiFiCredentials()) { if (!hasValidWiFiCredentials()) {
LOG_WARNING("No valid WiFi credentials found"); LOG_WARNING("No valid WiFi credentials found");
if (shouldStartPortal()) { if (!_bootSequenceComplete) {
// No credentials during boot - start portal
startWiFiPortal(); startWiFiPortal();
} }
return; 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); WiFi.mode(WIFI_STA);
applyNetworkConfig(false); // false = WiFi config applyNetworkConfig(false); // false = WiFi config
@@ -158,12 +163,36 @@ void Networking::handleReconnection() {
return; // Still waiting for Ethernet return; // Still waiting for Ethernet
} }
// Check for WiFi timeout (try again) // Check for WiFi timeout
if (_state == NetworkState::CONNECTING_WIFI) { if (_state == NetworkState::CONNECTING_WIFI) {
unsigned long now = millis(); unsigned long now = millis();
if (now - _lastConnectionAttempt > 10000) { // 10 second timeout if (now - _lastConnectionAttempt > 10000) { // 10 second timeout
LOG_INFO("WiFi connection timeout - retrying"); _wifiConnectionFailures++;
startWiFiConnection(); // Retry WiFi 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 return; // Still waiting for WiFi
} }
@@ -176,7 +205,8 @@ void Networking::handleReconnection() {
LOG_INFO("No Ethernet - trying WiFi"); LOG_INFO("No Ethernet - trying WiFi");
if (hasValidWiFiCredentials()) { if (hasValidWiFiCredentials()) {
startWiFiConnection(); startWiFiConnection();
} else if (shouldStartPortal()) { } else if (!_bootSequenceComplete) {
// No credentials during boot - start portal
startWiFiPortal(); startWiFiPortal();
} else { } else {
LOG_WARNING("No WiFi credentials and boot sequence complete - waiting"); LOG_WARNING("No WiFi credentials and boot sequence complete - waiting");
@@ -292,6 +322,9 @@ void Networking::onWiFiConnected() {
setState(NetworkState::CONNECTED_WIFI); setState(NetworkState::CONNECTED_WIFI);
setActiveConnection(ConnectionType::WIFI); setActiveConnection(ConnectionType::WIFI);
// Reset failure counter on successful connection
_wifiConnectionFailures = 0;
// Stop reconnection timer // Stop reconnection timer
xTimerStop(_reconnectionTimer, 0); xTimerStop(_reconnectionTimer, 0);
@@ -347,18 +380,13 @@ void Networking::applyNetworkConfig(bool ethernet) {
} }
bool Networking::hasValidWiFiCredentials() { bool Networking::hasValidWiFiCredentials() {
// Check if WiFiManager has saved credentials // Use WiFiManager's method to check if credentials are saved
return WiFi.SSID().length() > 0; 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 { bool Networking::isConnected() const {
return _activeConnection != ConnectionType::NONE; 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) { void Networking::reconnectionTimerCallback(TimerHandle_t xTimer) {
if (_instance) { if (_instance) {
_instance->handleReconnection(); _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");
}
} }
} }

View File

@@ -82,7 +82,7 @@ public:
void begin(); void begin();
// Status methods // Returns whether the network is currently connected
bool isConnected() const; bool isConnected() const;
String getLocalIP() const; String getLocalIP() const;
ConnectionType getActiveConnection() const { return _activeConnection; } ConnectionType getActiveConnection() const { return _activeConnection; }
@@ -119,9 +119,9 @@ private:
NetworkState _state; NetworkState _state;
ConnectionType _activeConnection; ConnectionType _activeConnection;
unsigned long _lastConnectionAttempt; unsigned long _lastConnectionAttempt;
unsigned long _bootStartTime;
bool _bootSequenceComplete; bool _bootSequenceComplete;
bool _ethernetCableConnected; bool _ethernetCableConnected;
int _wifiConnectionFailures; // Track consecutive WiFi failures
// Callbacks // Callbacks
std::function<void()> _onNetworkConnected; std::function<void()> _onNetworkConnected;
@@ -151,12 +151,11 @@ private:
// Utility methods // Utility methods
void applyNetworkConfig(bool ethernet = false); void applyNetworkConfig(bool ethernet = false);
bool hasValidWiFiCredentials(); bool hasValidWiFiCredentials();
bool shouldStartPortal();
// Timer callback // Timer callback
static void reconnectionTimerCallback(TimerHandle_t xTimer); static void reconnectionTimerCallback(TimerHandle_t xTimer);
// Constants // Constants
static const unsigned long RECONNECTION_INTERVAL = 5000; // 5 seconds 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
}; };

View File

@@ -1,33 +1,109 @@
#include "OTAManager.hpp" #include "OTAManager.hpp"
#include "../ConfigManager/ConfigManager.hpp" #include "../ConfigManager/ConfigManager.hpp"
#include "../Logging/Logging.hpp" #include "../Logging/Logging.hpp"
#include "../Player/Player.hpp"
#include <nvs_flash.h> #include <nvs_flash.h>
#include <nvs.h> #include <nvs.h>
OTAManager::OTAManager(ConfigManager& configManager) OTAManager::OTAManager(ConfigManager& configManager)
: _configManager(configManager) : _configManager(configManager)
, _fileManager(nullptr) , _fileManager(nullptr)
, _player(nullptr)
, _status(Status::IDLE) , _status(Status::IDLE)
, _lastError(ErrorCode::NONE) , _lastError(ErrorCode::NONE)
, _availableVersion(0.0f) , _availableVersion(0.0f)
, _minVersion(0.0f)
, _expectedFileSize(0)
, _updateAvailable(false) , _updateAvailable(false)
, _availableChecksum("") , _availableChecksum("")
, _updateChannel("stable") , _updateChannel("stable")
, _isMandatory(false) , _isMandatory(false)
, _isEmergency(false) , _isEmergency(false)
, _progressCallback(nullptr) , _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() { void OTAManager::begin() {
LOG_INFO("OTA Manager initialized"); LOG_INFO("OTA Manager initialized");
setStatus(Status::IDLE); 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) { void OTAManager::setFileManager(FileManager* fm) {
_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<OTAManager*>(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() { void OTAManager::checkForUpdates() {
// Boot-time check: only check stable channel for emergency/mandatory updates // Boot-time check: only check stable channel for emergency/mandatory updates
checkForUpdates("stable"); checkForUpdates("stable");
@@ -39,6 +115,12 @@ void OTAManager::checkForUpdates(const String& channel) {
return; 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); setStatus(Status::CHECKING_VERSION);
LOG_INFO("Checking for firmware updates in %s channel for %s...", LOG_INFO("Checking for firmware updates in %s channel for %s...",
channel.c_str(), _configManager.getHardwareVariant().c_str()); 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) { bool OTAManager::checkVersion(const String& channel) {
std::vector<String> servers = _configManager.getUpdateServers(); std::vector<String> servers = _configManager.getUpdateServers();
auto& updateConfig = _configManager.getUpdateConfig(); auto& updateConfig = _configManager.getUpdateConfig();
@@ -167,6 +249,16 @@ bool OTAManager::checkVersion(const String& channel) {
_updateChannel = doc["channel"].as<String>(); _updateChannel = doc["channel"].as<String>();
_isMandatory = doc["mandatory"].as<bool>(); _isMandatory = doc["mandatory"].as<bool>();
_isEmergency = doc["emergency"].as<bool>(); _isEmergency = doc["emergency"].as<bool>();
_minVersion = doc["minVersion"].as<float>(); // ✅ NEW
_expectedFileSize = doc["fileSize"].as<size_t>(); // ✅ 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 // Validate hardware variant matches
String hwVariant = doc["hardwareVariant"].as<String>(); String hwVariant = doc["hardwareVariant"].as<String>();
@@ -177,6 +269,16 @@ bool OTAManager::checkVersion(const String& channel) {
continue; // Try next server 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) { if (_availableVersion == 0.0f) {
LOG_ERROR("OTA: Invalid version in metadata from %s", baseUrl.c_str()); LOG_ERROR("OTA: Invalid version in metadata from %s", baseUrl.c_str());
continue; // Try next server 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: Successfully got metadata from %s", baseUrl.c_str());
LOG_INFO("OTA: Expected file size: %u bytes, Min version: %.1f",
_expectedFileSize, _minVersion);
return true; // Success! return true; // Success!
} else { } else {
LOG_ERROR("OTA: Server %s failed after %d retries. HTTP error: %d", LOG_ERROR("OTA: Server %s failed after %d retries. HTTP error: %d",
@@ -202,7 +306,7 @@ bool OTAManager::checkVersion(const String& channel) {
return false; 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) { bool OTAManager::downloadAndInstall(const String& channel) {
std::vector<String> servers = _configManager.getUpdateServers(); std::vector<String> servers = _configManager.getUpdateServers();
@@ -213,7 +317,7 @@ bool OTAManager::downloadAndInstall(const String& channel) {
LOG_INFO("OTA: Trying firmware download from server %d/%d: %s", LOG_INFO("OTA: Trying firmware download from server %d/%d: %s",
serverIndex + 1, servers.size(), baseUrl.c_str()); serverIndex + 1, servers.size(), baseUrl.c_str());
if (downloadToSD(firmwareUrl, _availableChecksum)) { if (downloadToSD(firmwareUrl, _availableChecksum, _expectedFileSize)) {
// Success! Now install from SD // Success! Now install from SD
return installFromSD("/firmware/staged_update.bin"); return installFromSD("/firmware/staged_update.bin");
} else { } else {
@@ -226,9 +330,7 @@ bool OTAManager::downloadAndInstall(const String& channel) {
return false; return false;
} }
bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum) { bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum, size_t expectedSize) {
// This method now receives the exact firmware URL from downloadAndInstall
// The server selection logic is handled there
if (!_fileManager) { if (!_fileManager) {
LOG_ERROR("FileManager not set!"); LOG_ERROR("FileManager not set!");
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED); setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
@@ -260,6 +362,24 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum)
return false; 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 // Open file for writing
File file = SD.open(tempPath.c_str(), FILE_WRITE); File file = SD.open(tempPath.c_str(), FILE_WRITE);
if (!file) { if (!file) {
@@ -272,8 +392,9 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum)
WiFiClient* stream = http.getStreamPtr(); WiFiClient* stream = http.getStreamPtr();
uint8_t buffer[1024]; uint8_t buffer[1024];
size_t written = 0; 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(); size_t available = stream->available();
if (available) { if (available) {
size_t toRead = min(available, sizeof(buffer)); size_t toRead = min(available, sizeof(buffer));
@@ -289,7 +410,17 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum)
return false; return false;
} }
written += bytesWritten; written += bytesWritten;
// ✅ IMPROVED: Progress reporting with percentage
notifyProgress(written, contentLength); 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(); yield();
@@ -298,13 +429,13 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum)
file.close(); file.close();
http.end(); http.end();
if (written != contentLength) { if (written != (size_t)contentLength) {
LOG_ERROR("Download incomplete: %d/%d bytes", written, contentLength); LOG_ERROR("Download incomplete: %u/%d bytes", written, contentLength);
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED); setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
return false; return false;
} }
LOG_INFO("Download complete (%d bytes)", written); LOG_INFO("Download complete (%u bytes)", written);
// Verify checksum // Verify checksum
if (!verifyChecksum(tempPath, expectedChecksum)) { if (!verifyChecksum(tempPath, expectedChecksum)) {
@@ -522,7 +653,7 @@ bool OTAManager::performManualUpdate(const String& channel) {
String firmwareUrl = buildFirmwareUrl(channel); String firmwareUrl = buildFirmwareUrl(channel);
// Download to SD first // Download to SD first
if (!downloadToSD(firmwareUrl, _availableChecksum)) { if (!downloadToSD(firmwareUrl, _availableChecksum, _expectedFileSize)) {
return false; return false;
} }
@@ -537,7 +668,6 @@ String OTAManager::getHardwareVariant() const {
void OTAManager::setHardwareVariant(const String& variant) { void OTAManager::setHardwareVariant(const String& variant) {
LOG_WARNING("OTAManager::setHardwareVariant is deprecated. Use ConfigManager::setHardwareVariant instead"); 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 // URL builders for multi-channel architecture
@@ -556,6 +686,47 @@ String OTAManager::buildFirmwareUrl(const String& channel) const {
return buildChannelUrl(channel) + "firmware.bin"; 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 // HEALTH CHECK IMPLEMENTATION
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
@@ -599,5 +770,11 @@ bool OTAManager::isHealthy() const {
return false; 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; return true;
} }

View File

@@ -8,7 +8,7 @@
* This class manages over-the-air firmware updates with safe, reliable * This class manages over-the-air firmware updates with safe, reliable
* update mechanisms, version checking, and comprehensive error handling. * 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 * 📅 DATE: 2025
* 👨‍💻 AUTHOR: Advanced Bell Systems * 👨‍💻 AUTHOR: Advanced Bell Systems
* ═══════════════════════════════════════════════════════════════════════════════════ * ═══════════════════════════════════════════════════════════════════════════════════
@@ -24,9 +24,11 @@
#include <mbedtls/md.h> #include <mbedtls/md.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <functional> #include <functional>
#include <time.h>
#include "../FileManager/FileManager.hpp" #include "../FileManager/FileManager.hpp"
class ConfigManager; // Forward declaration class ConfigManager; // Forward declaration
class Player; // Forward declaration for idle check
class OTAManager { class OTAManager {
public: public:
@@ -48,7 +50,11 @@ public:
WRITE_FAILED, WRITE_FAILED,
VERIFICATION_FAILED, VERIFICATION_FAILED,
CHECKSUM_MISMATCH, CHECKSUM_MISMATCH,
METADATA_PARSE_FAILED METADATA_PARSE_FAILED,
SIZE_MISMATCH,
VERSION_TOO_LOW,
CHANNEL_MISMATCH,
PLAYER_ACTIVE
}; };
// Callback types // Callback types
@@ -56,11 +62,16 @@ public:
using StatusCallback = std::function<void(Status status, ErrorCode error)>; using StatusCallback = std::function<void(Status status, ErrorCode error)>;
explicit OTAManager(ConfigManager& configManager); explicit OTAManager(ConfigManager& configManager);
~OTAManager();
void begin(); void begin();
void setFileManager(FileManager* fm); void setFileManager(FileManager* fm);
void setPlayer(Player* player); // NEW: Set player reference for idle check
void checkForUpdates(); void checkForUpdates();
void checkForUpdates(const String& channel); // Check specific channel void checkForUpdates(const String& channel); // Check specific channel
void checkForEmergencyUpdates(); // NEW: Scheduled emergency-only check
void update(); void update();
void update(const String& channel); // Update from specific channel void update(const String& channel); // Update from specific channel
void checkFirmwareUpdateFromSD(); // Check SD for firmware update void checkFirmwareUpdateFromSD(); // Check SD for firmware update
@@ -92,9 +103,12 @@ public:
private: private:
ConfigManager& _configManager; ConfigManager& _configManager;
FileManager* _fileManager; FileManager* _fileManager;
Player* _player; // NEW: Player reference for idle check
Status _status; Status _status;
ErrorCode _lastError; ErrorCode _lastError;
float _availableVersion; float _availableVersion;
float _minVersion; // NEW: Minimum required version
size_t _expectedFileSize; // NEW: Expected firmware file size
bool _updateAvailable; bool _updateAvailable;
String _availableChecksum; String _availableChecksum;
String _updateChannel; String _updateChannel;
@@ -104,6 +118,10 @@ private:
ProgressCallback _progressCallback; ProgressCallback _progressCallback;
StatusCallback _statusCallback; StatusCallback _statusCallback;
// NEW: Scheduled check timer
TimerHandle_t _scheduledCheckTimer;
static void scheduledCheckCallback(TimerHandle_t xTimer);
void setStatus(Status status, ErrorCode error = ErrorCode::NONE); void setStatus(Status status, ErrorCode error = ErrorCode::NONE);
void notifyProgress(size_t current, size_t total); void notifyProgress(size_t current, size_t total);
bool checkVersion(); bool checkVersion();
@@ -111,11 +129,15 @@ private:
bool checkChannelsMetadata(); bool checkChannelsMetadata();
bool downloadAndInstall(); bool downloadAndInstall();
bool downloadAndInstall(const String& channel); 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); bool verifyChecksum(const String& filePath, const String& expectedChecksum);
String calculateSHA256(const String& filePath); String calculateSHA256(const String& filePath);
bool installFromSD(const String& filePath); bool installFromSD(const String& filePath);
String buildChannelUrl(const String& channel) const; String buildChannelUrl(const String& channel) const;
String buildMetadataUrl(const String& channel) const; String buildMetadataUrl(const String& channel) const;
String buildFirmwareUrl(const String& channel) const; String buildFirmwareUrl(const String& channel) const;
// NEW: Helper methods
bool isPlayerActive() const;
bool checkAvailableSpace(size_t requiredBytes) const;
}; };

View File

@@ -1,11 +1,11 @@
#include "Player.hpp" #include "Player.hpp"
#include "../Communication/Communication.hpp" #include "../Communication/CommunicationRouter/CommunicationRouter.hpp"
#include "../BellEngine/BellEngine.hpp" #include "../BellEngine/BellEngine.hpp"
// Note: Removed global melody_steps dependency for cleaner architecture // Note: Removed global melody_steps dependency for cleaner architecture
// Constructor with dependencies // Constructor with dependencies
Player::Player(Communication* comm, FileManager* fm) Player::Player(CommunicationRouter* comm, FileManager* fm)
: id(0) : id(0)
, name("melody1") , name("melody1")
, uid("x") , uid("x")
@@ -58,7 +58,7 @@ Player::Player()
, _durationTimerHandle(NULL) { , _durationTimerHandle(NULL) {
} }
void Player::setDependencies(Communication* comm, FileManager* fm) { void Player::setDependencies(CommunicationRouter* comm, FileManager* fm) {
_commManager = comm; _commManager = comm;
_fileManager = fm; _fileManager = fm;
} }

View File

@@ -61,7 +61,7 @@
// ═════════════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════════════
// FORWARD DECLARATIONS - Dependencies injected at runtime // FORWARD DECLARATIONS - Dependencies injected at runtime
// ═════════════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════════════
class Communication; // Command handling and communication class CommunicationRouter; // Command handling and communication
class BellEngine; // High-precision timing engine class BellEngine; // High-precision timing engine
// ═════════════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════════════
@@ -104,7 +104,7 @@ public:
* @param comm Pointer to communication manager * @param comm Pointer to communication manager
* @param fm Pointer to file manager * @param fm Pointer to file manager
*/ */
Player(Communication* comm, FileManager* fm); Player(CommunicationRouter* comm, FileManager* fm);
/** /**
* @brief Default constructor for backward compatibility * @brief Default constructor for backward compatibility
@@ -118,7 +118,7 @@ public:
* @param comm Pointer to communication manager * @param comm Pointer to communication manager
* @param fm Pointer to file 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 * @brief Set BellEngine reference for precision timing
@@ -238,7 +238,7 @@ private:
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
// PRIVATE DEPENDENCIES AND DATA // PRIVATE DEPENDENCIES AND DATA
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
Communication* _commManager; // 📡 Communication system reference CommunicationRouter* _commManager; // 📡 Communication system reference
FileManager* _fileManager; // 📁 File operations reference FileManager* _fileManager; // 📁 File operations reference
BellEngine* _bellEngine; // 🔥 High-precision timing engine reference BellEngine* _bellEngine; // 🔥 High-precision timing engine reference

View File

@@ -1,5 +1,4 @@
#include "Telemetry.hpp" #include "Telemetry.hpp"
#include "../Communication/Communication.hpp"
void Telemetry::begin() { void Telemetry::begin() {
// Initialize arrays // Initialize arrays
@@ -149,17 +148,8 @@ void Telemetry::checkBellLoads() {
} }
} }
// Send batch notifications if any bells are overloaded // Note: Notifications now handled by BellEngine which has Communication reference
if (!criticalBells.empty()) { // BellEngine monitors telemetry and sends notifications when overloads detected
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");
}
}
// Trigger force stop if any bell is actually overloaded // Trigger force stop if any bell is actually overloaded
if (anyOverload && forceStopCallback != nullptr) { if (anyOverload && forceStopCallback != nullptr) {

View File

@@ -267,9 +267,13 @@ void Timekeeper::checkAndSyncPhysicalClock() {
// Calculate time difference (your exact logic!) // Calculate time difference (your exact logic!)
int16_t timeDifference = (realHour * 60 + realMinute) - (physicalHour * 60 + physicalMinute); 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) // Handle 12-hour rollover (if negative, add 12 hours)
if (timeDifference < 0) { if (timeDifference < 0) {
timeDifference += 12 * 60; // Add 12 hours to handle rollover 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) // If there's a difference, advance the clock by one minute (your runMotor equivalent)
@@ -310,7 +314,7 @@ void Timekeeper::updatePhysicalClockTime() {
currentMinute = 0; currentMinute = 0;
currentHour++; currentHour++;
if (currentHour > 12) { // 12-hour clock (your code used 24, but clock face is 12) if (currentHour > 12) { // 12-hour clock (your code used 24, but clock face is 12)
currentHour = 0; currentHour = 1;
} }
} }

View File

@@ -33,7 +33,7 @@
* ✅ Comprehensive logging system * ✅ Comprehensive logging system
* *
* 📡 COMMUNICATION PROTOCOLS: * 📡 COMMUNICATION PROTOCOLS:
* • MQTT (Primary control interface) * • MQTT (SSL/TLS via PubSubClient on Core 0)
* • WebSocket (Real-time web interface) * • WebSocket (Real-time web interface)
* • UDP Discovery (Auto-discovery service) * • UDP Discovery (Auto-discovery service)
* • HTTP/HTTPS (OTA updates) * • HTTP/HTTPS (OTA updates)
@@ -48,12 +48,39 @@
* High-priority FreeRTOS tasks ensure microsecond timing precision. * High-priority FreeRTOS tasks ensure microsecond timing precision.
* Core 1 dedicated to BellEngine for maximum performance. * 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 // SYSTEM LIBRARIES - Core ESP32 and Arduino functionality
// ═══════════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════════
@@ -71,7 +98,6 @@
// ═══════════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════════
// NETWORKING LIBRARIES - Advanced networking and communication // NETWORKING LIBRARIES - Advanced networking and communication
// ═══════════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════════
#include <AsyncMqttClient.h> // High-performance async MQTT client
#include <WiFiManager.h> // WiFi configuration portal #include <WiFiManager.h> // WiFi configuration portal
#include <ESPAsyncWebServer.h> // Async web server for WebSocket support #include <ESPAsyncWebServer.h> // Async web server for WebSocket support
#include <AsyncUDP.h> // UDP for discovery service #include <AsyncUDP.h> // UDP for discovery service
@@ -98,16 +124,15 @@
#include "src/Telemetry/Telemetry.hpp" #include "src/Telemetry/Telemetry.hpp"
#include "src/OTAManager/OTAManager.hpp" #include "src/OTAManager/OTAManager.hpp"
#include "src/Networking/Networking.hpp" #include "src/Networking/Networking.hpp"
#include "src/Communication/Communication.hpp" #include "src/Communication/CommunicationRouter/CommunicationRouter.hpp"
#include "src/ClientManager/ClientManager.hpp" #include "src/ClientManager/ClientManager.hpp"
#include "src/Communication/ResponseBuilder.hpp" #include "src/Communication/ResponseBuilder/ResponseBuilder.hpp"
#include "src/Player/Player.hpp" #include "src/Player/Player.hpp"
#include "src/BellEngine/BellEngine.hpp" #include "src/BellEngine/BellEngine.hpp"
#include "src/OutputManager/OutputManager.hpp" #include "src/OutputManager/OutputManager.hpp"
#include "src/HealthMonitor/HealthMonitor.hpp" #include "src/HealthMonitor/HealthMonitor.hpp"
#include "src/FirmwareValidator/FirmwareValidator.hpp" #include "src/FirmwareValidator/FirmwareValidator.hpp"
#include "src/InputManager/InputManager.hpp" #include "src/InputManager/InputManager.hpp"
#include "src/MqttSSL/MqttSSL.hpp"
// Class Constructors // Class Constructors
ConfigManager configManager; ConfigManager configManager;
@@ -115,13 +140,12 @@ FileManager fileManager(&configManager);
Timekeeper timekeeper; Timekeeper timekeeper;
Telemetry telemetry; Telemetry telemetry;
OTAManager otaManager(configManager); OTAManager otaManager(configManager);
AsyncMqttClient mqttClient;
Player player; Player player;
AsyncWebServer server(80); AsyncWebServer server(80);
AsyncWebSocket ws("/ws"); AsyncWebSocket ws("/ws");
AsyncUDP udp; AsyncUDP udp;
Networking networking(configManager); Networking networking(configManager);
Communication communication(configManager, otaManager, networking, mqttClient, server, ws, udp); CommunicationRouter communication(configManager, otaManager, networking, server, ws, udp);
HealthMonitor healthMonitor; HealthMonitor healthMonitor;
FirmwareValidator firmwareValidator; FirmwareValidator firmwareValidator;
InputManager inputManager; InputManager inputManager;
@@ -158,7 +182,7 @@ TimerHandle_t schedulerTimer;
void handleFactoryReset() { void handleFactoryReset() {
if (configManager.factoryReset()) { if (configManager.resetAllToDefaults()) {
delay(3000); delay(3000);
ESP.restart(); ESP.restart();
} }
@@ -176,21 +200,22 @@ void setup()
SPI.begin(hwConfig.ethSpiSck, hwConfig.ethSpiMiso, hwConfig.ethSpiMosi); SPI.begin(hwConfig.ethSpiSck, hwConfig.ethSpiMiso, hwConfig.ethSpiMosi);
delay(50); delay(50);
// Initialize Configuration (this loads device identity from SD card) // Initialize Configuration (loads factory identity from NVS + user settings from SD)
configManager.begin(); configManager.begin();
inputManager.begin(); inputManager.begin();
inputManager.setFactoryResetLongPressCallback(handleFactoryReset); 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
// ═══════════════════════════════════════════════════════════════════════════════
// Update firmware version (this is the ONLY identity field that can be set)
configManager.setDeviceUID("PV202508190002"); configManager.setFwVersion(FW_VERSION);
configManager.setHwType("BellPlus"); LOG_INFO("Firmware version: %s", FW_VERSION);
configManager.setHwVersion("1.0");
configManager.setFwVersion("1.1");
LOG_INFO("Device identity initialized");
// Display device information after configuration is loaded // Display device information after configuration is loaded
@@ -277,12 +302,13 @@ void setup()
// BellEngine already initialized and registered earlier for health validation // BellEngine already initialized and registered earlier for health validation
// Initialize Communication Manager // Initialize Communication Manager (now with PubSubClient MQTT)
communication.begin(); communication.begin();
communication.setPlayerReference(&player); communication.setPlayerReference(&player);
communication.setFileManagerReference(&fileManager); communication.setFileManagerReference(&fileManager);
communication.setTimeKeeperReference(&timekeeper); communication.setTimeKeeperReference(&timekeeper);
communication.setFirmwareValidatorReference(&firmwareValidator); communication.setFirmwareValidatorReference(&firmwareValidator);
player.setDependencies(&communication, &fileManager); player.setDependencies(&communication, &fileManager);
player.setBellEngine(&bellEngine); // Connect the beast! player.setBellEngine(&bellEngine); // Connect the beast!
@@ -294,7 +320,15 @@ void setup()
// Set up network callbacks // Set up network callbacks
networking.setNetworkCallbacks( 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 []() { communication.onNetworkDisconnected(); } // onDisconnected
); );
@@ -302,6 +336,14 @@ void setup()
if (networking.isConnected()) { if (networking.isConnected()) {
LOG_INFO("Network already connected - triggering MQTT connection"); LOG_INFO("Network already connected - triggering MQTT connection");
communication.onNetworkConnected(); 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); delay(500);
@@ -309,6 +351,7 @@ void setup()
// Initialize OTA Manager and check for updates // Initialize OTA Manager and check for updates
otaManager.begin(); otaManager.begin();
otaManager.setFileManager(&fileManager); otaManager.setFileManager(&fileManager);
otaManager.setPlayer(&player); // Set player reference for idle check
// 🔥 CRITICAL: Delay OTA check to avoid UDP socket race with MQTT // 🔥 CRITICAL: Delay OTA check to avoid UDP socket race with MQTT
// Both MQTT and OTA HTTP use UDP sockets, must sequence them! // Both MQTT and OTA HTTP use UDP sockets, must sequence them!
@@ -320,8 +363,9 @@ void setup()
// Register OTA Manager with health monitor // Register OTA Manager with health monitor
healthMonitor.setOTAManager(&otaManager); healthMonitor.setOTAManager(&otaManager);
// Start the server // Note: AsyncWebServer will be started by network callbacks when connection is ready
server.begin(); // This avoids port 80 conflicts with WiFiManager's captive portal
// 🔥 START RUNTIME VALIDATION: All subsystems are now initialized // 🔥 START RUNTIME VALIDATION: All subsystems are now initialized
// Begin extended runtime validation if we're in testing mode // Begin extended runtime validation if we're in testing mode
@@ -333,6 +377,7 @@ void setup()
LOG_INFO("✅ Firmware already validated - normal operation mode"); LOG_INFO("✅ Firmware already validated - normal operation mode");
} }
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
// INITIALIZATION COMPLETE // INITIALIZATION COMPLETE
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
@@ -340,7 +385,7 @@ void setup()
// • BellEngine creates high-priority timing task on Core 1 // • BellEngine creates high-priority timing task on Core 1
// • Telemetry creates monitoring task for load tracking // • Telemetry creates monitoring task for load tracking
// • Player creates duration timer for playback control // • 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 // • Networking creates connection management timers
// ✅ Bell configuration automatically loaded by ConfigManager // ✅ Bell configuration automatically loaded by ConfigManager
// ✅ System ready for MQTT commands, WebSocket connections, and UDP discovery // ✅ 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 * • BellEngine: High-priority task on Core 1 for microsecond timing
* • Telemetry: Background monitoring task for system health * • Telemetry: Background monitoring task for system health
* • Player: Timer-based duration control for melody playback * • 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 * • Networking: Automatic connection management
* *
* The main loop only handles lightweight operations that don't require * The main loop only handles lightweight operations that don't require
@@ -371,26 +416,6 @@ void setup()
*/ */
void loop() 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 // Feed watchdog only during firmware validation
if (firmwareValidator.isInTestingMode()) { if (firmwareValidator.isInTestingMode()) {
esp_task_wdt_reset(); 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 // Keep the loop responsive but not busy
delay(100); // ⏱️ 100ms delay to prevent busy waiting delay(100); // ⏱️ 100ms delay to prevent busy waiting
} }