Fixed MQTT and WS Routing - w/o SSL
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
.vscode/
|
||||
docs/
|
||||
html/
|
||||
vesper/build/
|
||||
vesper/.vscode/
|
||||
vesper/docs/
|
||||
vesper/sd_settings/
|
||||
vesper/CLAUDE.md
|
||||
|
||||
vesper/flutter/
|
||||
vesper/docs_manual/
|
||||
Doxyfile
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
#include "../ConfigManager/ConfigManager.hpp" // Configuration and settings
|
||||
#include "../Telemetry/Telemetry.hpp" // System monitoring and analytics
|
||||
#include "../OutputManager/OutputManager.hpp" // Hardware abstraction layer
|
||||
#include "../Communication/Communication.hpp" // Communication system for notifications
|
||||
#include "../Communication/CommunicationRouter/CommunicationRouter.hpp" // Communication system for notifications
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// CONSTRUCTOR & DESTRUCTOR IMPLEMENTATION
|
||||
@@ -94,7 +94,7 @@ void BellEngine::begin() {
|
||||
/**
|
||||
* @brief Set Communication manager reference for bell notifications
|
||||
*/
|
||||
void BellEngine::setCommunicationManager(Communication* commManager) {
|
||||
void BellEngine::setCommunicationManager(CommunicationRouter* commManager) {
|
||||
_communicationManager = commManager;
|
||||
LOG_DEBUG("BellEngine: Communication manager %s",
|
||||
commManager ? "connected" : "disconnected");
|
||||
@@ -181,6 +181,9 @@ void BellEngine::engineLoop() {
|
||||
|
||||
playbackLoop();
|
||||
|
||||
// Check telemetry for overloads and send notifications if needed
|
||||
checkAndNotifyOverloads();
|
||||
|
||||
// Pause handling AFTER complete loop - never interrupt mid-melody!
|
||||
while (_player.isPaused && _player.isPlaying && !_player.hardStop) {
|
||||
LOG_DEBUG("⏸️ Pausing between melody loops");
|
||||
@@ -241,15 +244,15 @@ void BellEngine::activateNote(uint16_t note) {
|
||||
|
||||
// Iterate through each bit position (note index)
|
||||
for (uint8_t noteIndex = 0; noteIndex < 16; noteIndex++) {
|
||||
if (note & (1 << noteIndex)) {
|
||||
// Get bell mapping
|
||||
uint8_t bellIndex = _player.noteAssignments[noteIndex];
|
||||
if (note & (1 << noteIndex)) {
|
||||
// Get bell mapping (noteAssignments stored as 1-indexed)
|
||||
uint8_t bellConfig = _player.noteAssignments[noteIndex];
|
||||
|
||||
// Skip if no bell assigned
|
||||
if (bellIndex == 0) continue;
|
||||
// Skip if no bell assigned
|
||||
if (bellConfig == 0) continue;
|
||||
|
||||
// Convert to 0-based indexing
|
||||
bellIndex = bellIndex - 1;
|
||||
// Convert 1-indexed config to 0-indexed bellIndex
|
||||
uint8_t bellIndex = bellConfig - 1;
|
||||
|
||||
// Additional safety check to prevent underflow crashes
|
||||
if (bellIndex >= 255) {
|
||||
@@ -373,12 +376,6 @@ bool BellEngine::isHealthy() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we're not in emergency stop state
|
||||
if (_emergencyStop.load()) {
|
||||
LOG_DEBUG("BellEngine: Unhealthy - Emergency stop active");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if OutputManager is properly connected and healthy
|
||||
if (!_outputManager.isInitialized()) {
|
||||
LOG_DEBUG("BellEngine: Unhealthy - OutputManager not initialized");
|
||||
@@ -387,3 +384,48 @@ bool BellEngine::isHealthy() const {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BellEngine::checkAndNotifyOverloads() {
|
||||
if (!_communicationManager) {
|
||||
return; // No communication manager available
|
||||
}
|
||||
|
||||
// Collect overloaded bells from telemetry
|
||||
std::vector<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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class Player; // Melody playback controller
|
||||
class ConfigManager; // Configuration and settings management
|
||||
class Telemetry; // System monitoring and analytics
|
||||
class OutputManager; // Hardware abstraction layer
|
||||
class Communication; // Communication system for notifications
|
||||
class CommunicationRouter; // Communication system for notifications
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// ARCHITECTURE MIGRATION NOTE
|
||||
@@ -221,7 +221,7 @@ public:
|
||||
* @brief Set Communication manager reference for bell notifications
|
||||
* @param commManager Pointer to communication manager
|
||||
*/
|
||||
void setCommunicationManager(Communication* commManager);
|
||||
void setCommunicationManager(CommunicationRouter* commManager);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK METHOD
|
||||
@@ -239,7 +239,7 @@ private:
|
||||
ConfigManager& _configManager; // Configuration manager for bell settings
|
||||
Telemetry& _telemetry; // System monitoring and strike tracking
|
||||
OutputManager& _outputManager; // 🔥 Hardware abstraction layer for relay control
|
||||
Communication* _communicationManager; // Communication system for bell notifications
|
||||
CommunicationRouter* _communicationManager; // Communication system for bell notifications
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// ENGINE STATE (Atomic for thread safety)
|
||||
@@ -348,6 +348,15 @@ private:
|
||||
*/
|
||||
void emergencyShutdown();
|
||||
|
||||
/**
|
||||
* @brief Check telemetry and send overload notifications
|
||||
*
|
||||
* Monitors telemetry for bell overloads and sends notifications
|
||||
* through Communication manager when thresholds are exceeded.
|
||||
* Called after each playback loop.
|
||||
*/
|
||||
void checkAndNotifyOverloads();
|
||||
|
||||
/**
|
||||
* @brief Notify WebSocket clients of fired bells
|
||||
* @param bellIndices Vector of bell indices that were fired (1-indexed)
|
||||
|
||||
@@ -1,421 +1,55 @@
|
||||
#include "Communication.hpp"
|
||||
#include "../ConfigManager/ConfigManager.hpp"
|
||||
#include "../OTAManager/OTAManager.hpp"
|
||||
#include "../Networking/Networking.hpp"
|
||||
#include "../Logging/Logging.hpp"
|
||||
#include "../Player/Player.hpp"
|
||||
#include "../FileManager/FileManager.hpp"
|
||||
#include "../TimeKeeper/TimeKeeper.hpp"
|
||||
#include "../FirmwareValidator/FirmwareValidator.hpp"
|
||||
#include "../MqttSSL/MqttSSL.hpp"
|
||||
#include <WiFiClientSecure.h>
|
||||
/*
|
||||
* COMMANDHANDLER.CPP - Command Processing Implementation
|
||||
*/
|
||||
|
||||
Communication* Communication::_instance = nullptr;
|
||||
StaticJsonDocument<2048> Communication::_parseDocument;
|
||||
#include "CommandHandler.hpp"
|
||||
#include "../../ConfigManager/ConfigManager.hpp"
|
||||
#include "../../OTAManager/OTAManager.hpp"
|
||||
#include "../../Player/Player.hpp"
|
||||
#include "../../FileManager/FileManager.hpp"
|
||||
#include "../../TimeKeeper/TimeKeeper.hpp"
|
||||
#include "../../FirmwareValidator/FirmwareValidator.hpp"
|
||||
#include "../../ClientManager/ClientManager.hpp"
|
||||
#include "../../Logging/Logging.hpp"
|
||||
#include "../ResponseBuilder/ResponseBuilder.hpp"
|
||||
|
||||
static void connectToMqttWrapper(TimerHandle_t xTimer) {
|
||||
if (Communication::_instance) {
|
||||
Communication::_instance->connectToMqtt();
|
||||
}
|
||||
}
|
||||
|
||||
Communication::Communication(ConfigManager& configManager,
|
||||
OTAManager& otaManager,
|
||||
Networking& networking,
|
||||
AsyncMqttClient& mqttClient,
|
||||
AsyncWebServer& server,
|
||||
AsyncWebSocket& webSocket,
|
||||
AsyncUDP& udp)
|
||||
CommandHandler::CommandHandler(ConfigManager& configManager, OTAManager& otaManager)
|
||||
: _configManager(configManager)
|
||||
, _otaManager(otaManager)
|
||||
, _networking(networking)
|
||||
, _mqttClient(mqttClient)
|
||||
, _server(server)
|
||||
, _webSocket(webSocket)
|
||||
, _udp(udp)
|
||||
, _player(nullptr)
|
||||
, _fileManager(nullptr)
|
||||
, _timeKeeper(nullptr)
|
||||
, _firmwareValidator(nullptr)
|
||||
, _mqttReconnectTimer(nullptr) {
|
||||
, _clientManager(nullptr)
|
||||
, _responseCallback(nullptr) {}
|
||||
|
||||
_instance = this;
|
||||
CommandHandler::~CommandHandler() {}
|
||||
|
||||
void CommandHandler::setPlayerReference(Player* player) {
|
||||
_player = player;
|
||||
}
|
||||
|
||||
Communication::~Communication() {
|
||||
if (_mqttReconnectTimer != nullptr) {
|
||||
xTimerDelete(_mqttReconnectTimer, portMAX_DELAY);
|
||||
_mqttReconnectTimer = nullptr;
|
||||
}
|
||||
_instance = nullptr;
|
||||
void CommandHandler::setFileManagerReference(FileManager* fm) {
|
||||
_fileManager = fm;
|
||||
}
|
||||
|
||||
void Communication::begin() {
|
||||
LOG_INFO("Initializing Communication Manager v2.1");
|
||||
initMqtt();
|
||||
initWebSocket();
|
||||
LOG_INFO("Communication Manager initialized with multi-client support");
|
||||
void CommandHandler::setTimeKeeperReference(Timekeeper* tk) {
|
||||
_timeKeeper = tk;
|
||||
}
|
||||
|
||||
void Communication::initMqtt() {
|
||||
_mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE,
|
||||
(void*)0, connectToMqttWrapper);
|
||||
|
||||
_mqttClient.onConnect(onMqttConnect);
|
||||
_mqttClient.onDisconnect(onMqttDisconnect);
|
||||
_mqttClient.onSubscribe(onMqttSubscribe);
|
||||
_mqttClient.onUnsubscribe(onMqttUnsubscribe);
|
||||
_mqttClient.onMessage(onMqttMessage);
|
||||
_mqttClient.onPublish(onMqttPublish);
|
||||
|
||||
auto& mqttConfig = _configManager.getMqttConfig();
|
||||
|
||||
// Log SSL status - AsyncMqttClient SSL is compile-time configured
|
||||
MqttSSL::logSSLStatus(_mqttClient, mqttConfig.port);
|
||||
|
||||
// DEBUG: Log connection details
|
||||
LOG_INFO("MQTT Config: host=%s, port=%d, user=%s, pass=%s",
|
||||
mqttConfig.host.c_str(), mqttConfig.port,
|
||||
mqttConfig.user.c_str(), mqttConfig.password.c_str());
|
||||
|
||||
_mqttClient.setServer(mqttConfig.host.c_str(), mqttConfig.port);
|
||||
_mqttClient.setCredentials(mqttConfig.user.c_str(), mqttConfig.password.c_str());
|
||||
void CommandHandler::setFirmwareValidatorReference(FirmwareValidator* fv) {
|
||||
_firmwareValidator = fv;
|
||||
}
|
||||
|
||||
void Communication::initWebSocket() {
|
||||
_webSocket.onEvent(onWebSocketEvent);
|
||||
_server.addHandler(&_webSocket);
|
||||
void CommandHandler::setClientManagerReference(ClientManager* cm) {
|
||||
_clientManager = cm;
|
||||
}
|
||||
|
||||
void Communication::connectToMqtt() {
|
||||
if (_networking.isConnected()) {
|
||||
LOG_INFO("Connecting to MQTT...");
|
||||
_mqttClient.connect();
|
||||
} else {
|
||||
LOG_WARNING("Cannot connect to MQTT: No network connection");
|
||||
}
|
||||
void CommandHandler::setResponseCallback(ResponseCallback callback) {
|
||||
_responseCallback = callback;
|
||||
}
|
||||
|
||||
void Communication::subscribeMqtt() {
|
||||
char topic[64];
|
||||
snprintf(topic, sizeof(topic), "vesper/%s/control", _configManager.getDeviceUID().c_str());
|
||||
uint16_t topicId = _mqttClient.subscribe(topic, 2);
|
||||
LOG_INFO("Subscribing to Command topic, QoS 2, packetId: %d", topicId);
|
||||
}
|
||||
|
||||
void Communication::sendResponse(const String& response, const MessageContext& context) {
|
||||
if (context.source == MessageSource::MQTT) {
|
||||
publishToMqtt(response);
|
||||
} else if (context.source == MessageSource::WEBSOCKET) {
|
||||
_clientManager.sendToClient(context.clientId, response);
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::sendSuccessResponse(const String& type, const String& payload, const MessageContext& context) {
|
||||
String response = ResponseBuilder::success(type, payload);
|
||||
sendResponse(response, context);
|
||||
}
|
||||
|
||||
void Communication::sendErrorResponse(const String& type, const String& message, const MessageContext& context) {
|
||||
String response = ResponseBuilder::error(type, message);
|
||||
sendResponse(response, context);
|
||||
}
|
||||
|
||||
void Communication::broadcastStatus(const String& statusMessage) {
|
||||
publishToMqtt(statusMessage);
|
||||
broadcastToAllWebSocketClients(statusMessage);
|
||||
}
|
||||
|
||||
void Communication::broadcastStatus(const JsonDocument& statusJson) {
|
||||
String statusMessage;
|
||||
serializeJson(statusJson, statusMessage);
|
||||
broadcastStatus(statusMessage);
|
||||
}
|
||||
|
||||
void Communication::sendBellOverloadNotification(const std::vector<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) {
|
||||
void CommandHandler::processCommand(JsonDocument& command, const MessageContext& context) {
|
||||
String cmd = command["cmd"];
|
||||
JsonVariant contents = command["contents"];
|
||||
|
||||
@@ -444,12 +78,28 @@ void Communication::handleCommand(JsonDocument& command, const MessageContext& c
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handlePingCommand(const MessageContext& context) {
|
||||
void CommandHandler::sendResponse(const String& response, const MessageContext& context) {
|
||||
if (_responseCallback) {
|
||||
_responseCallback(response, context);
|
||||
}
|
||||
}
|
||||
|
||||
void CommandHandler::sendSuccessResponse(const String& type, const String& payload, const MessageContext& context) {
|
||||
String response = ResponseBuilder::success(type, payload);
|
||||
sendResponse(response, context);
|
||||
}
|
||||
|
||||
void CommandHandler::sendErrorResponse(const String& type, const String& message, const MessageContext& context) {
|
||||
String response = ResponseBuilder::error(type, message);
|
||||
sendResponse(response, context);
|
||||
}
|
||||
|
||||
void CommandHandler::handlePingCommand(const MessageContext& context) {
|
||||
String response = ResponseBuilder::pong();
|
||||
sendResponse(response, context);
|
||||
}
|
||||
|
||||
void Communication::handleStatusCommand(const MessageContext& context) {
|
||||
void CommandHandler::handleStatusCommand(const MessageContext& context) {
|
||||
PlayerStatus playerStatus = _player ? _player->getStatus() : PlayerStatus::STOPPED;
|
||||
uint32_t timeElapsedMs = 0;
|
||||
uint64_t projectedRunTime = 0;
|
||||
@@ -468,12 +118,19 @@ void Communication::handleStatusCommand(const MessageContext& context) {
|
||||
sendResponse(response, context);
|
||||
}
|
||||
|
||||
void Communication::handleIdentifyCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleIdentifyCommand(JsonVariant contents, const MessageContext& context) {
|
||||
if (context.source != MessageSource::WEBSOCKET) {
|
||||
sendErrorResponse("identify", "Identify command only available via WebSocket", context);
|
||||
return;
|
||||
}
|
||||
|
||||
// 🛡️ SAFETY CHECK: Ensure ClientManager reference is set
|
||||
if (!_clientManager) {
|
||||
LOG_ERROR("ClientManager reference not set in CommandHandler!");
|
||||
sendErrorResponse("identify", "Internal error: ClientManager not available", context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!contents.containsKey("device_type")) {
|
||||
sendErrorResponse("identify", "Missing device_type parameter", context);
|
||||
return;
|
||||
@@ -489,7 +146,7 @@ void Communication::handleIdentifyCommand(JsonVariant contents, const MessageCon
|
||||
}
|
||||
|
||||
if (deviceType != ClientManager::DeviceType::UNKNOWN) {
|
||||
_clientManager.updateClientType(context.clientId, deviceType);
|
||||
_clientManager->updateClientType(context.clientId, deviceType);
|
||||
sendSuccessResponse("identify", "Device identified as " + deviceTypeStr, context);
|
||||
LOG_INFO("Client #%u identified as %s device", context.clientId, deviceTypeStr.c_str());
|
||||
} else {
|
||||
@@ -497,7 +154,7 @@ void Communication::handleIdentifyCommand(JsonVariant contents, const MessageCon
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handlePlaybackCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handlePlaybackCommand(JsonVariant contents, const MessageContext& context) {
|
||||
if (_player) {
|
||||
bool success = _player->command(contents);
|
||||
|
||||
@@ -512,7 +169,7 @@ void Communication::handlePlaybackCommand(JsonVariant contents, const MessageCon
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleFileManagerCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleFileManagerCommand(JsonVariant contents, const MessageContext& context) {
|
||||
if (!contents.containsKey("action")) {
|
||||
sendErrorResponse("file_manager", "Missing action parameter", context);
|
||||
return;
|
||||
@@ -533,7 +190,7 @@ void Communication::handleFileManagerCommand(JsonVariant contents, const Message
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleRelaySetupCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleRelaySetupCommand(JsonVariant contents, const MessageContext& context) {
|
||||
if (!contents.containsKey("action")) {
|
||||
sendErrorResponse("relay_setup", "Missing action parameter", context);
|
||||
return;
|
||||
@@ -552,7 +209,7 @@ void Communication::handleRelaySetupCommand(JsonVariant contents, const MessageC
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleClockSetupCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleClockSetupCommand(JsonVariant contents, const MessageContext& context) {
|
||||
if (!contents.containsKey("action")) {
|
||||
sendErrorResponse("clock_setup", "Missing action parameter", context);
|
||||
return;
|
||||
@@ -585,7 +242,7 @@ void Communication::handleClockSetupCommand(JsonVariant contents, const MessageC
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleSystemInfoCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleSystemInfoCommand(JsonVariant contents, const MessageContext& context) {
|
||||
if (!contents.containsKey("action")) {
|
||||
sendErrorResponse("system_info", "Missing action parameter", context);
|
||||
return;
|
||||
@@ -606,13 +263,15 @@ void Communication::handleSystemInfoCommand(JsonVariant contents, const MessageC
|
||||
handleRollbackFirmwareCommand(context);
|
||||
} else if (action == "get_firmware_status") {
|
||||
handleGetFirmwareStatusCommand(context);
|
||||
} else if (action == "get_full_settings") {
|
||||
handleGetFullSettingsCommand(context);
|
||||
} else {
|
||||
LOG_WARNING("Unknown system info action: %s", action.c_str());
|
||||
sendErrorResponse("system_info", "Unknown action: " + action, context);
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleListMelodiesCommand(const MessageContext& context) {
|
||||
void CommandHandler::handleListMelodiesCommand(const MessageContext& context) {
|
||||
if (!_fileManager) {
|
||||
sendErrorResponse("list_melodies", "FileManager not available", context);
|
||||
return;
|
||||
@@ -639,7 +298,7 @@ void Communication::handleListMelodiesCommand(const MessageContext& context) {
|
||||
sendResponse(responseStr, context);
|
||||
}
|
||||
|
||||
void Communication::handleDownloadMelodyCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleDownloadMelodyCommand(JsonVariant contents, const MessageContext& context) {
|
||||
if (!_fileManager) {
|
||||
sendErrorResponse("download_melody", "FileManager not available", context);
|
||||
return;
|
||||
@@ -653,7 +312,7 @@ void Communication::handleDownloadMelodyCommand(JsonVariant contents, const Mess
|
||||
sendResponse(response, context);
|
||||
}
|
||||
|
||||
void Communication::handleDeleteMelodyCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleDeleteMelodyCommand(JsonVariant contents, const MessageContext& context) {
|
||||
if (!_fileManager) {
|
||||
sendErrorResponse("delete_melody", "FileManager not available", context);
|
||||
return;
|
||||
@@ -676,7 +335,7 @@ void Communication::handleDeleteMelodyCommand(JsonVariant contents, const Messag
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleSetRelayTimersCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleSetRelayTimersCommand(JsonVariant contents, const MessageContext& context) {
|
||||
try {
|
||||
_configManager.updateBellDurations(contents);
|
||||
// Save configuration to ensure persistence
|
||||
@@ -694,7 +353,7 @@ void Communication::handleSetRelayTimersCommand(JsonVariant contents, const Mess
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleSetRelayOutputsCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleSetRelayOutputsCommand(JsonVariant contents, const MessageContext& context) {
|
||||
try {
|
||||
_configManager.updateBellOutputs(contents);
|
||||
// Note: Bell outputs are typically not persisted to SD card as they're more of a mapping configuration
|
||||
@@ -706,7 +365,7 @@ void Communication::handleSetRelayOutputsCommand(JsonVariant contents, const Mes
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleSetClockOutputsCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleSetClockOutputsCommand(JsonVariant contents, const MessageContext& context) {
|
||||
try {
|
||||
_configManager.updateClockOutputs(contents);
|
||||
// Save configuration to ensure persistence
|
||||
@@ -724,7 +383,7 @@ void Communication::handleSetClockOutputsCommand(JsonVariant contents, const Mes
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleSetClockTimingsCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleSetClockTimingsCommand(JsonVariant contents, const MessageContext& context) {
|
||||
try {
|
||||
_configManager.updateClockOutputs(contents);
|
||||
// Save configuration to ensure persistence
|
||||
@@ -742,7 +401,7 @@ void Communication::handleSetClockTimingsCommand(JsonVariant contents, const Mes
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleSetClockAlertsCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleSetClockAlertsCommand(JsonVariant contents, const MessageContext& context) {
|
||||
try {
|
||||
_configManager.updateClockAlerts(contents);
|
||||
// Save configuration to ensure persistence
|
||||
@@ -760,7 +419,7 @@ void Communication::handleSetClockAlertsCommand(JsonVariant contents, const Mess
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleSetClockBacklightCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleSetClockBacklightCommand(JsonVariant contents, const MessageContext& context) {
|
||||
try {
|
||||
_configManager.updateClockBacklight(contents);
|
||||
// Save configuration to ensure persistence
|
||||
@@ -778,7 +437,7 @@ void Communication::handleSetClockBacklightCommand(JsonVariant contents, const M
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleSetClockSilenceCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleSetClockSilenceCommand(JsonVariant contents, const MessageContext& context) {
|
||||
try {
|
||||
_configManager.updateClockSilence(contents);
|
||||
// Save configuration to ensure persistence
|
||||
@@ -796,7 +455,7 @@ void Communication::handleSetClockSilenceCommand(JsonVariant contents, const Mes
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleSetRtcTimeCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleSetRtcTimeCommand(JsonVariant contents, const MessageContext& context) {
|
||||
if (!_timeKeeper) {
|
||||
sendErrorResponse("set_rtc_time", "TimeKeeper not available", context);
|
||||
return;
|
||||
@@ -866,7 +525,7 @@ void Communication::handleSetRtcTimeCommand(JsonVariant contents, const MessageC
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleSetPhysicalClockTimeCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleSetPhysicalClockTimeCommand(JsonVariant contents, const MessageContext& context) {
|
||||
if (!contents.containsKey("hour") || !contents.containsKey("minute")) {
|
||||
sendErrorResponse("set_physical_clock_time", "Missing hour or minute parameter", context);
|
||||
return;
|
||||
@@ -886,8 +545,12 @@ void Communication::handleSetPhysicalClockTimeCommand(JsonVariant contents, cons
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert 24-hour to 12-hour format for analog clock
|
||||
int clockHour = hour % 12;
|
||||
if (clockHour == 0) clockHour = 12; // Midnight/Noon = 12, not 0
|
||||
|
||||
// Set the physical clock time using ConfigManager
|
||||
_configManager.setPhysicalClockHour(hour);
|
||||
_configManager.setPhysicalClockHour(clockHour);
|
||||
_configManager.setPhysicalClockMinute(minute);
|
||||
_configManager.setLastSyncTime(millis() / 1000);
|
||||
|
||||
@@ -896,15 +559,15 @@ void Communication::handleSetPhysicalClockTimeCommand(JsonVariant contents, cons
|
||||
|
||||
if (saved) {
|
||||
sendSuccessResponse("set_physical_clock_time", "Physical clock time updated and saved successfully", context);
|
||||
LOG_INFO("Physical clock time set to %02d:%02d and saved to SD", hour, minute);
|
||||
LOG_INFO("Physical clock time set to %02d:%02d (12h: %02d:%02d) and saved to SD",
|
||||
hour, minute, clockHour, minute);
|
||||
} else {
|
||||
sendErrorResponse("set_physical_clock_time", "Physical clock time updated but failed to save to SD card", context);
|
||||
LOG_ERROR("Physical clock time set to %02d:%02d but failed to save to SD", hour, minute);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Communication::handlePauseClockUpdatesCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handlePauseClockUpdatesCommand(JsonVariant contents, const MessageContext& context) {
|
||||
if (!_timeKeeper) {
|
||||
return;
|
||||
}
|
||||
@@ -922,7 +585,7 @@ void Communication::handlePauseClockUpdatesCommand(JsonVariant contents, const M
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleSetClockEnabledCommand(JsonVariant contents, const MessageContext& context) {
|
||||
void CommandHandler::handleSetClockEnabledCommand(JsonVariant contents, const MessageContext& context) {
|
||||
if (!contents.containsKey("enabled")) {
|
||||
sendErrorResponse("set_clock_enabled", "Missing enabled parameter", context);
|
||||
return;
|
||||
@@ -948,29 +611,7 @@ void Communication::handleSetClockEnabledCommand(JsonVariant contents, const Mes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
String Communication::getPayloadContent(char* data, size_t len) {
|
||||
String content = "";
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
content.concat(data[i]);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
int Communication::extractBellNumber(const String& key) {
|
||||
if (key.length() >= 2) {
|
||||
char firstChar = key.charAt(0);
|
||||
if (firstChar == 'b' || firstChar == 'c') {
|
||||
String numberPart = key.substring(1);
|
||||
return numberPart.toInt();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Communication::handleGetDeviceTimeCommand(const MessageContext& context) {
|
||||
void CommandHandler::handleGetDeviceTimeCommand(const MessageContext& context) {
|
||||
StaticJsonDocument<256> response;
|
||||
response["status"] = "SUCCESS";
|
||||
response["type"] = "device_time";
|
||||
@@ -1003,7 +644,7 @@ void Communication::handleGetDeviceTimeCommand(const MessageContext& context) {
|
||||
LOG_DEBUG("Device time requested");
|
||||
}
|
||||
|
||||
void Communication::handleGetClockTimeCommand(const MessageContext& context) {
|
||||
void CommandHandler::handleGetClockTimeCommand(const MessageContext& context) {
|
||||
StaticJsonDocument<256> response;
|
||||
response["status"] = "SUCCESS";
|
||||
response["type"] = "clock_time";
|
||||
@@ -1025,80 +666,11 @@ void Communication::handleGetClockTimeCommand(const MessageContext& context) {
|
||||
_configManager.getLastSyncTime());
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK IMPLEMENTATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool Communication::isHealthy() const {
|
||||
// Check if required references are set
|
||||
if (!_player) {
|
||||
LOG_DEBUG("Communication: Unhealthy - Player reference not set");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_fileManager) {
|
||||
LOG_DEBUG("Communication: Unhealthy - FileManager reference not set");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_timeKeeper) {
|
||||
LOG_DEBUG("Communication: Unhealthy - TimeKeeper reference not set");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if WebSocket server is active
|
||||
if (!hasActiveWebSocketClients() && !isMqttConnected()) {
|
||||
LOG_DEBUG("Communication: Unhealthy - No active connections (WebSocket or MQTT)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if MQTT reconnection timer exists and is functioning
|
||||
if (_mqttReconnectTimer == nullptr) {
|
||||
LOG_DEBUG("Communication: Unhealthy - MQTT reconnection timer not created");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if networking dependency is healthy
|
||||
if (!_networking.isConnected()) {
|
||||
LOG_DEBUG("Communication: Unhealthy - No network connection");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// SYSTEM COMMAND IMPLEMENTATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void Communication::handleSystemCommand(JsonVariant contents, const MessageContext& context) {
|
||||
if (!contents.containsKey("action")) {
|
||||
sendErrorResponse("system", "Missing action parameter", context);
|
||||
return;
|
||||
}
|
||||
|
||||
String action = contents["action"];
|
||||
LOG_DEBUG("Processing system action: %s", action.c_str());
|
||||
|
||||
if (action == "commit_firmware") {
|
||||
handleCommitFirmwareCommand(context);
|
||||
} else if (action == "rollback_firmware") {
|
||||
handleRollbackFirmwareCommand(context);
|
||||
} else if (action == "get_firmware_status") {
|
||||
handleGetFirmwareStatusCommand(context);
|
||||
} else if (action == "set_network_config") {
|
||||
handleSetNetworkConfigCommand(contents, context);
|
||||
} else {
|
||||
LOG_WARNING("Unknown system action: %s", action.c_str());
|
||||
sendErrorResponse("system", "Unknown action: " + action, context);
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// FIRMWARE MANAGEMENT IMPLEMENTATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void Communication::handleCommitFirmwareCommand(const MessageContext& context) {
|
||||
void CommandHandler::handleCommitFirmwareCommand(const MessageContext& context) {
|
||||
if (!_firmwareValidator) {
|
||||
sendErrorResponse("commit_firmware", "FirmwareValidator not available", context);
|
||||
return;
|
||||
@@ -1123,7 +695,7 @@ void Communication::handleCommitFirmwareCommand(const MessageContext& context) {
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleRollbackFirmwareCommand(const MessageContext& context) {
|
||||
void CommandHandler::handleRollbackFirmwareCommand(const MessageContext& context) {
|
||||
if (!_firmwareValidator) {
|
||||
sendErrorResponse("rollback_firmware", "FirmwareValidator not available", context);
|
||||
return;
|
||||
@@ -1144,7 +716,7 @@ void Communication::handleRollbackFirmwareCommand(const MessageContext& context)
|
||||
}
|
||||
}
|
||||
|
||||
void Communication::handleGetFirmwareStatusCommand(const MessageContext& context) {
|
||||
void CommandHandler::handleGetFirmwareStatusCommand(const MessageContext& context) {
|
||||
if (!_firmwareValidator) {
|
||||
sendErrorResponse("get_firmware_status", "FirmwareValidator not available", context);
|
||||
return;
|
||||
@@ -1217,18 +789,44 @@ void Communication::handleGetFirmwareStatusCommand(const MessageContext& context
|
||||
LOG_DEBUG("Firmware status requested: %s", stateStr.c_str());
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// NETWORK CONFIGURATION IMPLEMENTATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
void CommandHandler::handleGetFullSettingsCommand(const MessageContext& context) {
|
||||
LOG_DEBUG("Full settings requested");
|
||||
|
||||
// Get all settings as JSON string from ConfigManager
|
||||
String settingsJson = _configManager.getAllSettingsAsJson();
|
||||
|
||||
// Parse it to embed in our response structure
|
||||
StaticJsonDocument<512> response;
|
||||
response["status"] = "SUCCESS";
|
||||
response["type"] = "full_settings";
|
||||
|
||||
// Parse the settings JSON and add as payload
|
||||
DynamicJsonDocument settingsDoc(4096);
|
||||
DeserializationError error = deserializeJson(settingsDoc, settingsJson);
|
||||
|
||||
if (error) {
|
||||
LOG_ERROR("Failed to parse settings JSON: %s", error.c_str());
|
||||
sendErrorResponse("get_full_settings", "Failed to serialize settings", context);
|
||||
return;
|
||||
}
|
||||
|
||||
response["payload"] = settingsDoc.as<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
|
||||
bool hasHostname = contents.containsKey("hostname");
|
||||
bool hasStaticIPConfig = contents.containsKey("useStaticIP");
|
||||
bool hasAPPass = contents.containsKey("apPass");
|
||||
bool hasDiscoveryPort = contents.containsKey("discoveryPort");
|
||||
|
||||
if (!hasHostname && !hasStaticIPConfig && !hasAPPass && !hasDiscoveryPort) {
|
||||
if (!hasHostname && !hasStaticIPConfig) {
|
||||
sendErrorResponse("set_network_config", "No network parameters provided", context);
|
||||
return;
|
||||
}
|
||||
@@ -1248,8 +846,6 @@ void Communication::handleSetNetworkConfigCommand(JsonVariant contents, const Me
|
||||
IPAddress dns1 = currentConfig.dns1;
|
||||
IPAddress dns2 = currentConfig.dns2;
|
||||
String hostname = currentConfig.hostname;
|
||||
String apPass = currentConfig.apPass;
|
||||
uint16_t discoveryPort = currentConfig.discoveryPort;
|
||||
|
||||
// Update hostname if provided
|
||||
if (hasHostname) {
|
||||
@@ -1310,16 +906,10 @@ void Communication::handleSetNetworkConfigCommand(JsonVariant contents, const Me
|
||||
|
||||
// If anything changed, update and save configuration
|
||||
if (configChanged) {
|
||||
// Update network config using existing method
|
||||
_configManager.updateNetworkConfig(useStaticIP, ip, gateway, subnet, dns1, dns2);
|
||||
// Update network config (saves to SD internally)
|
||||
_configManager.updateNetworkConfig(hostname, useStaticIP, ip, gateway, subnet, dns1, dns2);
|
||||
|
||||
// Manually update fields not handled by updateNetworkConfig
|
||||
// Note: This is a workaround since NetworkConfig doesn't have setters for all fields
|
||||
auto& writableConfig = const_cast<ConfigManager::NetworkConfig&>(_configManager.getNetworkConfig());
|
||||
writableConfig.hostname = hostname;
|
||||
|
||||
// Save to SD card
|
||||
bool saved = _configManager.saveNetworkConfig();
|
||||
bool saved = true; // saveNetworkConfig() already called in updateNetworkConfig()
|
||||
|
||||
if (saved) {
|
||||
String responseMsg = "Network configuration updated successfully";
|
||||
@@ -1344,3 +934,63 @@ void Communication::handleSetNetworkConfigCommand(JsonVariant contents, const Me
|
||||
LOG_ERROR("Unknown exception in handleSetNetworkConfigCommand");
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// RESET DEFAULTS COMMAND
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void CommandHandler::handleResetDefaultsCommand(const MessageContext& context) {
|
||||
|
||||
LOG_WARNING("⚠️ Factory reset requested. Proceeding...");
|
||||
|
||||
try {
|
||||
// Reset all configurations to defaults
|
||||
bool resetComplete = _configManager.resetAllToDefaults();
|
||||
|
||||
if (resetComplete) {
|
||||
sendSuccessResponse("reset_defaults", "Reset to Defaults completed. Device will Restart to apply changes.", context);
|
||||
LOG_WARNING("✅ Factory reset completed and all configurations saved to SD card");
|
||||
} else {
|
||||
sendErrorResponse("reset_defaults", "Reset to Defaults applied but failed to save some configurations to SD card", context);
|
||||
LOG_ERROR("❌ Reset to Defaults applied but failed to save some configurations to SD card");
|
||||
}
|
||||
} catch (...) {
|
||||
sendErrorResponse("reset_defaults", "Failed to perform Reset to Defaults", context);
|
||||
LOG_ERROR("❌ Exception occurred during Resetting to Defaults");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// SYSTEM COMMAND IMPLEMENTATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void CommandHandler::handleSystemCommand(JsonVariant contents, const MessageContext& context) {
|
||||
if (!contents.containsKey("action")) {
|
||||
sendErrorResponse("system", "Missing action parameter", context);
|
||||
return;
|
||||
}
|
||||
|
||||
String action = contents["action"];
|
||||
LOG_DEBUG("Processing system action: %s", action.c_str());
|
||||
|
||||
if (action == "status") {
|
||||
handleStatusCommand(context);
|
||||
} else if (action == "reset_defaults") {
|
||||
handleResetDefaultsCommand(context);
|
||||
} else if (action == "commit_firmware") {
|
||||
handleCommitFirmwareCommand(context);
|
||||
} else if (action == "rollback_firmware") {
|
||||
handleRollbackFirmwareCommand(context);
|
||||
} else if (action == "get_firmware_status") {
|
||||
handleGetFirmwareStatusCommand(context);
|
||||
} else if (action == "set_network_config") {
|
||||
handleSetNetworkConfigCommand(contents, context);
|
||||
} else {
|
||||
LOG_WARNING("Unknown system action: %s", action.c_str());
|
||||
sendErrorResponse("system", "Unknown action: " + action, context);
|
||||
}
|
||||
}
|
||||
|
||||
141
vesper/src/Communication/CommandHandler/CommandHandler.hpp
Normal file
141
vesper/src/Communication/CommandHandler/CommandHandler.hpp
Normal 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);
|
||||
};
|
||||
@@ -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.
|
||||
};
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
240
vesper/src/Communication/MQTTAsyncClient/MQTTAsyncClient.cpp
Normal file
240
vesper/src/Communication/MQTTAsyncClient/MQTTAsyncClient.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
116
vesper/src/Communication/MQTTAsyncClient/MQTTAsyncClient.hpp
Normal file
116
vesper/src/Communication/MQTTAsyncClient/MQTTAsyncClient.hpp
Normal 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);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "ResponseBuilder.hpp"
|
||||
#include "../Logging/Logging.hpp"
|
||||
#include "../../Logging/Logging.hpp"
|
||||
|
||||
// Static member initialization
|
||||
StaticJsonDocument<512> ResponseBuilder::_responseDoc;
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "../Player/Player.hpp" // For PlayerStatus enum
|
||||
#include "../../Player/Player.hpp" // For PlayerStatus enum
|
||||
|
||||
class ResponseBuilder {
|
||||
public:
|
||||
157
vesper/src/Communication/WebSocketServer/WebSocketServer.cpp
Normal file
157
vesper/src/Communication/WebSocketServer/WebSocketServer.cpp
Normal 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());
|
||||
}
|
||||
}
|
||||
103
vesper/src/Communication/WebSocketServer/WebSocketServer.hpp
Normal file
103
vesper/src/Communication/WebSocketServer/WebSocketServer.hpp
Normal 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;
|
||||
};
|
||||
@@ -44,7 +44,7 @@ void ConfigManager::createDefaultBellConfig() {
|
||||
// Initialize default durations (90ms for all bells)
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
bellConfig.durations[i] = 90;
|
||||
bellConfig.outputs[i] = i; // Direct mapping by default
|
||||
bellConfig.outputs[i] = i + 1; // 1-indexed mapping by default
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,8 @@ bool ConfigManager::begin() {
|
||||
|
||||
// Step 4: Load device configuration from SD card (firmware version only)
|
||||
if (!loadDeviceConfig()) {
|
||||
LOG_WARNING("ConfigManager: Could not load device config from SD card - using defaults");
|
||||
LOG_INFO("ConfigManager: Creating default device config file");
|
||||
saveDeviceConfig();
|
||||
}
|
||||
|
||||
// Step 5: Load update servers list
|
||||
@@ -78,12 +79,32 @@ bool ConfigManager::begin() {
|
||||
LOG_WARNING("ConfigManager: Could not load update servers - using fallback only");
|
||||
}
|
||||
|
||||
// Step 6: Load user-configurable settings from SD
|
||||
// Step 6: Load user-configurable settings from SD (and create if missing)
|
||||
loadFromSD();
|
||||
loadNetworkConfig(); // Load network configuration (hostname, static IP settings)
|
||||
loadBellDurations();
|
||||
loadClockConfig(); // Load clock configuration (C1/C2 outputs, pulse durations)
|
||||
loadClockState(); // Load physical clock state (hour, minute, position)
|
||||
|
||||
// Load network config, save defaults if not found
|
||||
if (!loadNetworkConfig()) {
|
||||
LOG_INFO("ConfigManager: Creating default network config file");
|
||||
saveNetworkConfig();
|
||||
}
|
||||
|
||||
// Load bell durations, save defaults if not found
|
||||
if (!loadBellDurations()) {
|
||||
LOG_INFO("ConfigManager: Creating default bell durations file");
|
||||
saveBellDurations();
|
||||
}
|
||||
|
||||
// Load clock config, save defaults if not found
|
||||
if (!loadClockConfig()) {
|
||||
LOG_INFO("ConfigManager: Creating default clock config file");
|
||||
saveClockConfig();
|
||||
}
|
||||
|
||||
// Load clock state, save defaults if not found
|
||||
if (!loadClockState()) {
|
||||
LOG_INFO("ConfigManager: Creating default clock state file");
|
||||
saveClockState();
|
||||
}
|
||||
|
||||
LOG_INFO("ConfigManager: Initialization complete - UID: %s, Hostname: %s",
|
||||
deviceConfig.deviceUID.c_str(), networkConfig.hostname.c_str());
|
||||
@@ -123,42 +144,35 @@ bool ConfigManager::loadDeviceIdentityFromNVS() {
|
||||
return false;
|
||||
}
|
||||
|
||||
deviceConfig.deviceUID = readNVSString(NVS_DEVICE_UID_KEY, "PV000000000000");
|
||||
deviceConfig.hwType = readNVSString(NVS_HW_TYPE_KEY, "BellSystems");
|
||||
deviceConfig.hwVersion = readNVSString(NVS_HW_VERSION_KEY, "0");
|
||||
// Read factory-set device identity from NVS (READ-ONLY)
|
||||
deviceConfig.deviceUID = readNVSString(NVS_DEVICE_UID_KEY, "");
|
||||
deviceConfig.hwType = readNVSString(NVS_HW_TYPE_KEY, "");
|
||||
deviceConfig.hwVersion = readNVSString(NVS_HW_VERSION_KEY, "");
|
||||
|
||||
LOG_INFO("ConfigManager: Device identity loaded from NVS - UID: %s, Type: %s, Version: %s",
|
||||
deviceConfig.deviceUID.c_str(),
|
||||
deviceConfig.hwType.c_str(),
|
||||
deviceConfig.hwVersion.c_str());
|
||||
// Validate that factory identity exists
|
||||
if (deviceConfig.deviceUID.isEmpty() || deviceConfig.hwType.isEmpty() || deviceConfig.hwVersion.isEmpty()) {
|
||||
LOG_ERROR("═══════════════════════════════════════════════════════════════════════════");
|
||||
LOG_ERROR(" ⚠️ CRITICAL: DEVICE IDENTITY NOT FOUND IN NVS");
|
||||
LOG_ERROR(" ⚠️ This device has NOT been factory-programmed!");
|
||||
LOG_ERROR(" ⚠️ Please flash factory firmware to set device identity");
|
||||
LOG_ERROR("═══════════════════════════════════════════════════════════════════════════");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("═══════════════════════════════════════════════════════════════════════════");
|
||||
LOG_INFO(" 🏭 FACTORY DEVICE IDENTITY LOADED FROM NVS (READ-ONLY)");
|
||||
LOG_INFO(" 🆔 Device UID: %s", deviceConfig.deviceUID.c_str());
|
||||
LOG_INFO(" 🔧 Hardware Type: %s", deviceConfig.hwType.c_str());
|
||||
LOG_INFO(" 📐 Hardware Version: %s", deviceConfig.hwVersion.c_str());
|
||||
LOG_INFO(" 🔒 These values are PERMANENT and cannot be changed by production firmware");
|
||||
LOG_INFO("═══════════════════════════════════════════════════════════════════════════");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::saveDeviceIdentityToNVS() {
|
||||
if (nvsHandle == 0) {
|
||||
LOG_ERROR("ConfigManager: NVS not initialized, cannot save device identity");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
success &= writeNVSString(NVS_DEVICE_UID_KEY, deviceConfig.deviceUID);
|
||||
success &= writeNVSString(NVS_HW_TYPE_KEY, deviceConfig.hwType);
|
||||
success &= writeNVSString(NVS_HW_VERSION_KEY, deviceConfig.hwVersion);
|
||||
|
||||
if (success) {
|
||||
esp_err_t err = nvs_commit(nvsHandle);
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("ConfigManager: Failed to commit NVS changes: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("ConfigManager: Device identity saved to NVS");
|
||||
} else {
|
||||
LOG_ERROR("ConfigManager: Failed to save device identity to NVS");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
// REMOVED: saveDeviceIdentityToNVS() - Production firmware MUST NOT write device identity
|
||||
// Device identity (UID, hwType, hwVersion) is factory-set ONLY and stored in NVS by factory firmware
|
||||
// Production firmware reads these values once at boot and keeps them in RAM
|
||||
|
||||
String ConfigManager::readNVSString(const char* key, const String& defaultValue) {
|
||||
if (nvsHandle == 0) {
|
||||
@@ -195,21 +209,8 @@ String ConfigManager::readNVSString(const char* key, const String& defaultValue)
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ConfigManager::writeNVSString(const char* key, const String& value) {
|
||||
if (nvsHandle == 0) {
|
||||
LOG_ERROR("ConfigManager: NVS not initialized, cannot write key: %s", key);
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_err_t err = nvs_set_str(nvsHandle, key, value.c_str());
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("ConfigManager: Failed to write NVS key '%s': %s", key, esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DEBUG("ConfigManager: Written NVS key '%s': %s", key, value.c_str());
|
||||
return true;
|
||||
}
|
||||
// REMOVED: writeNVSString() - Production firmware MUST NOT write to NVS
|
||||
// All device identity is factory-set and read-only in production firmware
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// STANDARD SD CARD FUNCTIONALITY
|
||||
@@ -327,7 +328,7 @@ bool ConfigManager::loadBellDurations() {
|
||||
|
||||
File file = SD.open("/settings/relayTimings.json", FILE_READ);
|
||||
if (!file) {
|
||||
LOG_ERROR("ConfigManager: Settings file not found on SD. Using default bell durations.");
|
||||
LOG_WARNING("ConfigManager: Settings file not found on SD. Using default bell durations.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -681,8 +682,9 @@ void ConfigManager::updateTimeConfig(long gmtOffsetSec, int daylightOffsetSec) {
|
||||
gmtOffsetSec, daylightOffsetSec);
|
||||
}
|
||||
|
||||
void ConfigManager::updateNetworkConfig(bool useStaticIP, IPAddress ip, IPAddress gateway,
|
||||
void ConfigManager::updateNetworkConfig(const String& hostname, bool useStaticIP, IPAddress ip, IPAddress gateway,
|
||||
IPAddress subnet, IPAddress dns1, IPAddress dns2) {
|
||||
networkConfig.hostname = hostname;
|
||||
networkConfig.useStaticIP = useStaticIP;
|
||||
networkConfig.ip = ip;
|
||||
networkConfig.gateway = gateway;
|
||||
@@ -690,8 +692,8 @@ void ConfigManager::updateNetworkConfig(bool useStaticIP, IPAddress ip, IPAddres
|
||||
networkConfig.dns1 = dns1;
|
||||
networkConfig.dns2 = dns2;
|
||||
saveNetworkConfig(); // Save immediately to SD
|
||||
LOG_INFO("ConfigManager: NetworkConfig updated - Static IP: %s, IP: %s",
|
||||
useStaticIP ? "enabled" : "disabled", ip.toString().c_str());
|
||||
LOG_INFO("ConfigManager: NetworkConfig updated - Hostname: %s, Static IP: %s, IP: %s",
|
||||
hostname.c_str(), useStaticIP ? "enabled" : "disabled", ip.toString().c_str());
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -802,96 +804,28 @@ bool ConfigManager::saveNetworkConfig() {
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// FACTORY RESET IMPLEMENTATION
|
||||
// SETTINGS RESET IMPLEMENTATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool ConfigManager::factoryReset() {
|
||||
LOG_WARNING("═══════════════════════════════════════════════════════════════════════════");
|
||||
LOG_WARNING("🏭 FACTORY RESET INITIATED");
|
||||
LOG_WARNING("═══════════════════════════════════════════════════════════════════════════");
|
||||
|
||||
if (!ensureSDCard()) {
|
||||
LOG_ERROR("❌ ConfigManager: Cannot perform factory reset - SD card not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 1: Delete all configuration files
|
||||
LOG_INFO("🗑️ Step 1: Deleting all configuration files from SD card...");
|
||||
bool deleteSuccess = clearAllSettings();
|
||||
|
||||
if (!deleteSuccess) {
|
||||
LOG_ERROR("❌ ConfigManager: Factory reset failed - could not delete all settings");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 2: Reset in-memory configuration to defaults
|
||||
LOG_INFO("🔄 Step 2: Resetting in-memory configuration to defaults...");
|
||||
|
||||
// Reset network config (keep device-generated values)
|
||||
networkConfig.useStaticIP = false;
|
||||
networkConfig.ip = IPAddress(0, 0, 0, 0);
|
||||
networkConfig.gateway = IPAddress(0, 0, 0, 0);
|
||||
networkConfig.subnet = IPAddress(0, 0, 0, 0);
|
||||
networkConfig.dns1 = IPAddress(0, 0, 0, 0);
|
||||
networkConfig.dns2 = IPAddress(0, 0, 0, 0);
|
||||
// hostname and apSsid are auto-generated from deviceUID, keep them
|
||||
|
||||
// Reset time config
|
||||
timeConfig.gmtOffsetSec = 0;
|
||||
timeConfig.daylightOffsetSec = 0;
|
||||
|
||||
// Reset bell config
|
||||
createDefaultBellConfig();
|
||||
|
||||
// Reset clock config to defaults
|
||||
clockConfig.c1output = 255;
|
||||
clockConfig.c2output = 255;
|
||||
clockConfig.pulseDuration = 5000;
|
||||
clockConfig.pauseDuration = 2000;
|
||||
clockConfig.physicalHour = 0;
|
||||
clockConfig.physicalMinute = 0;
|
||||
clockConfig.nextOutputIsC1 = true;
|
||||
clockConfig.lastSyncTime = 0;
|
||||
clockConfig.alertType = "OFF";
|
||||
clockConfig.alertRingInterval = 1200;
|
||||
clockConfig.hourBell = 255;
|
||||
clockConfig.halfBell = 255;
|
||||
clockConfig.quarterBell = 255;
|
||||
clockConfig.backlight = false;
|
||||
clockConfig.backlightOutput = 255;
|
||||
clockConfig.backlightOnTime = "18:00";
|
||||
clockConfig.backlightOffTime = "06:00";
|
||||
clockConfig.daytimeSilenceEnabled = false;
|
||||
clockConfig.daytimeSilenceOnTime = "14:00";
|
||||
clockConfig.daytimeSilenceOffTime = "17:00";
|
||||
clockConfig.nighttimeSilenceEnabled = false;
|
||||
clockConfig.nighttimeSilenceOnTime = "22:00";
|
||||
clockConfig.nighttimeSilenceOffTime = "07:00";
|
||||
|
||||
// Note: Device identity (deviceUID, hwType, hwVersion) in NVS is NOT reset
|
||||
// Note: WiFi credentials are handled by WiFiManager, not reset here
|
||||
|
||||
LOG_INFO("✅ Step 2: In-memory configuration reset to defaults");
|
||||
|
||||
LOG_WARNING("✅ FACTORY RESET COMPLETE");
|
||||
LOG_WARNING("🔄 Device will boot with default settings on next restart");
|
||||
LOG_WARNING("🆔 Device identity (UID) preserved in NVS");
|
||||
LOG_INFO("WiFi credentials should be cleared separately using WiFiManager");
|
||||
|
||||
return true;
|
||||
}
|
||||
bool ConfigManager::resetAllToDefaults() {
|
||||
|
||||
LOG_WARNING("═══════════════════════════════════════════════════════════════════════════");
|
||||
LOG_WARNING(" 🏭 RESET SETTINGS TO DEFAULTS INITIATED");
|
||||
LOG_WARNING("═══════════════════════════════════════════════════════════════════════════");
|
||||
|
||||
bool ConfigManager::clearAllSettings() {
|
||||
if (!ensureSDCard()) {
|
||||
LOG_ERROR("ConfigManager: SD card not available for clearing settings");
|
||||
LOG_ERROR("❌ ConfigManager: Cannot perform reset - SD card not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool allDeleted = true;
|
||||
int filesDeleted = 0;
|
||||
int filesFailed = 0;
|
||||
|
||||
// List of all configuration files to delete
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// STEP 1: Delete all configuration files
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const char* settingsFiles[] = {
|
||||
"/settings/deviceConfig.json",
|
||||
"/settings/networkConfig.json",
|
||||
@@ -903,15 +837,14 @@ bool ConfigManager::clearAllSettings() {
|
||||
|
||||
int numFiles = sizeof(settingsFiles) / sizeof(settingsFiles[0]);
|
||||
|
||||
LOG_INFO("ConfigManager: Attempting to delete %d configuration files...", numFiles);
|
||||
LOG_WARNING("🗑️ Step 1: Deleting %d configuration files...", numFiles);
|
||||
|
||||
// Delete each configuration file
|
||||
for (int i = 0; i < numFiles; i++) {
|
||||
const char* filepath = settingsFiles[i];
|
||||
|
||||
if (SD.exists(filepath)) {
|
||||
if (SD.remove(filepath)) {
|
||||
LOG_INFO("✅ Deleted: %s", filepath);
|
||||
LOG_DEBUG("✅ Deleted: %s", filepath);
|
||||
filesDeleted++;
|
||||
} else {
|
||||
LOG_ERROR("❌ Failed to delete: %s", filepath);
|
||||
@@ -923,22 +856,153 @@ bool ConfigManager::clearAllSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// Also delete the /melodies directory if you want a complete reset
|
||||
// Uncomment if you want to delete melodies too:
|
||||
/*
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// STEP 2: Delete all melodies recursively
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (SD.exists("/melodies")) {
|
||||
LOG_INFO("Deleting /melodies directory...");
|
||||
// Note: SD library doesn't have rmdir for non-empty dirs
|
||||
// You'd need to implement recursive delete or just leave melodies
|
||||
LOG_WARNING("🗑️ Step 2: Deleting melody files...");
|
||||
|
||||
File melodiesDir = SD.open("/melodies");
|
||||
if (melodiesDir && melodiesDir.isDirectory()) {
|
||||
int melodiesDeleted = 0;
|
||||
int melodiesFailed = 0;
|
||||
|
||||
File entry = melodiesDir.openNextFile();
|
||||
while (entry) {
|
||||
String entryPath = String("/melodies/") + entry.name();
|
||||
|
||||
if (!entry.isDirectory()) {
|
||||
if (SD.remove(entryPath.c_str())) {
|
||||
LOG_DEBUG("✅ Deleted melody: %s", entryPath.c_str());
|
||||
melodiesDeleted++;
|
||||
} else {
|
||||
LOG_ERROR("❌ Failed to delete melody: %s", entryPath.c_str());
|
||||
melodiesFailed++;
|
||||
allDeleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
entry.close();
|
||||
entry = melodiesDir.openNextFile();
|
||||
}
|
||||
|
||||
melodiesDir.close();
|
||||
|
||||
// Try to remove the empty directory
|
||||
if (SD.rmdir("/melodies")) {
|
||||
LOG_DEBUG("✅ Deleted /melodies directory");
|
||||
} else {
|
||||
LOG_WARNING("⚠️ Could not delete /melodies directory (may not be empty)");
|
||||
}
|
||||
|
||||
LOG_WARNING(" 🎵 Melodies deleted: %d, failed: %d", melodiesDeleted, melodiesFailed);
|
||||
filesDeleted += melodiesDeleted;
|
||||
filesFailed += melodiesFailed;
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("⏩ /melodies directory not found");
|
||||
}
|
||||
*/
|
||||
|
||||
LOG_INFO("═══════════════════════════════════════════════════════════════════════════");
|
||||
LOG_INFO("📄 Settings cleanup summary:");
|
||||
LOG_INFO(" ✅ Files deleted: %d", filesDeleted);
|
||||
LOG_INFO(" ❌ Files failed: %d", filesFailed);
|
||||
LOG_INFO(" 🔄 Total processed: %d / %d", filesDeleted + filesFailed, numFiles);
|
||||
LOG_INFO("═══════════════════════════════════════════════════════════════════════════");
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// SUMMARY
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
LOG_WARNING("═══════════════════════════════════════════════════════════════════════════");
|
||||
LOG_WARNING("📄 Full reset summary:");
|
||||
LOG_WARNING(" ✅ Files deleted: %d", filesDeleted);
|
||||
LOG_WARNING(" ❌ Files failed: %d", filesFailed);
|
||||
LOG_WARNING(" 🔄 Total processed: %d", filesDeleted + filesFailed);
|
||||
LOG_WARNING("═══════════════════════════════════════════════════════════════════════════");
|
||||
|
||||
LOG_WARNING("✅ RESET TO DEFAULT COMPLETE");
|
||||
LOG_WARNING("🔄 Device will boot with default settings on next restart");
|
||||
LOG_WARNING("🆔 Device identity (UID) preserved");
|
||||
|
||||
return allDeleted;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// GET ALL SETTINGS AS JSON
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
String ConfigManager::getAllSettingsAsJson() const {
|
||||
// Use a large document to hold everything
|
||||
DynamicJsonDocument doc(4096);
|
||||
|
||||
// Device info
|
||||
JsonObject device = doc.createNestedObject("device");
|
||||
device["uid"] = deviceConfig.deviceUID;
|
||||
device["hwType"] = deviceConfig.hwType;
|
||||
device["hwVersion"] = deviceConfig.hwVersion;
|
||||
device["fwVersion"] = deviceConfig.fwVersion;
|
||||
|
||||
// Network config
|
||||
JsonObject network = doc.createNestedObject("network");
|
||||
network["hostname"] = networkConfig.hostname;
|
||||
network["useStaticIP"] = networkConfig.useStaticIP;
|
||||
network["ip"] = networkConfig.ip.toString();
|
||||
network["gateway"] = networkConfig.gateway.toString();
|
||||
network["subnet"] = networkConfig.subnet.toString();
|
||||
network["dns1"] = networkConfig.dns1.toString();
|
||||
network["dns2"] = networkConfig.dns2.toString();
|
||||
|
||||
// Time config
|
||||
JsonObject time = doc.createNestedObject("time");
|
||||
time["ntpServer"] = timeConfig.ntpServer;
|
||||
time["gmtOffsetSec"] = timeConfig.gmtOffsetSec;
|
||||
time["daylightOffsetSec"] = timeConfig.daylightOffsetSec;
|
||||
|
||||
// Bell durations (relay timings)
|
||||
JsonObject bells = doc.createNestedObject("bells");
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
String key = String("b") + (i + 1);
|
||||
bells[key] = bellConfig.durations[i];
|
||||
}
|
||||
|
||||
// Clock configuration
|
||||
JsonObject clock = doc.createNestedObject("clock");
|
||||
clock["enabled"] = clockConfig.enabled;
|
||||
clock["c1output"] = clockConfig.c1output;
|
||||
clock["c2output"] = clockConfig.c2output;
|
||||
clock["pulseDuration"] = clockConfig.pulseDuration;
|
||||
clock["pauseDuration"] = clockConfig.pauseDuration;
|
||||
|
||||
// Clock state
|
||||
JsonObject clockState = doc.createNestedObject("clockState");
|
||||
clockState["physicalHour"] = clockConfig.physicalHour;
|
||||
clockState["physicalMinute"] = clockConfig.physicalMinute;
|
||||
clockState["nextOutputIsC1"] = clockConfig.nextOutputIsC1;
|
||||
clockState["lastSyncTime"] = clockConfig.lastSyncTime;
|
||||
|
||||
// Clock alerts
|
||||
JsonObject alerts = doc.createNestedObject("alerts");
|
||||
alerts["alertType"] = clockConfig.alertType;
|
||||
alerts["alertRingInterval"] = clockConfig.alertRingInterval;
|
||||
alerts["hourBell"] = clockConfig.hourBell;
|
||||
alerts["halfBell"] = clockConfig.halfBell;
|
||||
alerts["quarterBell"] = clockConfig.quarterBell;
|
||||
|
||||
// Clock backlight
|
||||
JsonObject backlight = doc.createNestedObject("backlight");
|
||||
backlight["enabled"] = clockConfig.backlight;
|
||||
backlight["output"] = clockConfig.backlightOutput;
|
||||
backlight["onTime"] = clockConfig.backlightOnTime;
|
||||
backlight["offTime"] = clockConfig.backlightOffTime;
|
||||
|
||||
// Silence periods
|
||||
JsonObject silence = doc.createNestedObject("silence");
|
||||
JsonObject daytime = silence.createNestedObject("daytime");
|
||||
daytime["enabled"] = clockConfig.daytimeSilenceEnabled;
|
||||
daytime["onTime"] = clockConfig.daytimeSilenceOnTime;
|
||||
daytime["offTime"] = clockConfig.daytimeSilenceOffTime;
|
||||
JsonObject nighttime = silence.createNestedObject("nighttime");
|
||||
nighttime["enabled"] = clockConfig.nighttimeSilenceEnabled;
|
||||
nighttime["onTime"] = clockConfig.nighttimeSilenceOnTime;
|
||||
nighttime["offTime"] = clockConfig.nighttimeSilenceOffTime;
|
||||
|
||||
// Serialize to string
|
||||
String output;
|
||||
serializeJson(doc, output);
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -75,14 +75,15 @@ public:
|
||||
* @struct MqttConfig
|
||||
* @brief MQTT broker connection settings
|
||||
*
|
||||
* Cloud broker as default, can be overridden via SD card.
|
||||
* Default configured for local Mosquitto broker.
|
||||
* Username defaults to deviceUID for unique identification.
|
||||
*/
|
||||
struct MqttConfig {
|
||||
String host = "j2f24f16.ala.eu-central-1.emqxsl.com"; // 📡 Cloud MQTT broker (default)
|
||||
int port = 1883; // 🔌 Standard MQTT port (default)
|
||||
IPAddress host = IPAddress(145, 223, 96, 251); // 📡 Local Mosquitto broker
|
||||
int port = 1883; // 🔌 Standard MQTT port (non-SSL)
|
||||
String user; // 👤 Auto-set to deviceUID
|
||||
String password = "vesper"; // 🔑 Default password - OK as is
|
||||
String password = "vesper"; // 🔑 Default password
|
||||
bool useSSL = false; // 🔒 SSL disabled for local broker
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -249,12 +250,11 @@ private:
|
||||
bool ensureSDCard();
|
||||
void createDefaultBellConfig();
|
||||
|
||||
// NVS management (for factory-set device identity)
|
||||
// NVS management (READ-ONLY for factory-set device identity)
|
||||
bool initializeNVS();
|
||||
bool loadDeviceIdentityFromNVS();
|
||||
bool saveDeviceIdentityToNVS();
|
||||
String readNVSString(const char* key, const String& defaultValue);
|
||||
bool writeNVSString(const char* key, const String& value);
|
||||
// REMOVED: saveDeviceIdentityToNVS() and writeNVSString() - Production firmware is READ-ONLY
|
||||
|
||||
public:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -287,37 +287,20 @@ public:
|
||||
const BellConfig& getBellConfig() const { return bellConfig; }
|
||||
const ClockConfig& getClockConfig() const { return clockConfig; }
|
||||
|
||||
// Device identity methods (read-only - factory set via separate firmware)
|
||||
// Device identity methods (READ-ONLY - factory set via separate factory firmware)
|
||||
// These values are loaded ONCE at boot from NVS and kept in RAM
|
||||
// Production firmware CANNOT modify device identity
|
||||
String getDeviceUID() const { return deviceConfig.deviceUID; }
|
||||
String getHwType() const { return deviceConfig.hwType; }
|
||||
String getHwVersion() const { return deviceConfig.hwVersion; }
|
||||
String getFwVersion() const { return deviceConfig.fwVersion; }
|
||||
|
||||
/** @brief Set device UID (factory programming only) */
|
||||
void setDeviceUID(const String& uid) {
|
||||
deviceConfig.deviceUID = uid;
|
||||
saveDeviceIdentityToNVS();
|
||||
generateNetworkIdentifiers();
|
||||
}
|
||||
|
||||
/** @brief Set hardware type (factory programming only) */
|
||||
void setHwType(const String& type) {
|
||||
deviceConfig.hwType = type;
|
||||
saveDeviceIdentityToNVS();
|
||||
}
|
||||
|
||||
/** @brief Set hardware version (factory programming only) */
|
||||
void setHwVersion(const String& version) {
|
||||
deviceConfig.hwVersion = version;
|
||||
saveDeviceIdentityToNVS();
|
||||
}
|
||||
|
||||
/** @brief Set firmware version (auto-updated after OTA) */
|
||||
void setFwVersion(const String& version) { deviceConfig.fwVersion = version; }
|
||||
|
||||
// Configuration update methods for app commands
|
||||
void updateTimeConfig(long gmtOffsetSec, int daylightOffsetSec);
|
||||
void updateNetworkConfig(bool useStaticIP, IPAddress ip, IPAddress gateway,
|
||||
void updateNetworkConfig(const String& hostname, bool useStaticIP, IPAddress ip, IPAddress gateway,
|
||||
IPAddress subnet, IPAddress dns1, IPAddress dns2);
|
||||
|
||||
// Network configuration persistence
|
||||
@@ -352,7 +335,7 @@ public:
|
||||
uint8_t getPhysicalClockMinute() const { return clockConfig.physicalMinute; }
|
||||
bool getNextOutputIsC1() const { return clockConfig.nextOutputIsC1; }
|
||||
uint32_t getLastSyncTime() const { return clockConfig.lastSyncTime; }
|
||||
void setPhysicalClockHour(uint8_t hour) { clockConfig.physicalHour = hour % 12; }
|
||||
void setPhysicalClockHour(uint8_t hour) { clockConfig.physicalHour = (hour % 12 == 0) ? 12 : hour % 12; }
|
||||
void setPhysicalClockMinute(uint8_t minute) { clockConfig.physicalMinute = minute % 60; }
|
||||
void setNextOutputIsC1(bool isC1) { clockConfig.nextOutputIsC1 = isC1; }
|
||||
void setLastSyncTime(uint32_t timestamp) { clockConfig.lastSyncTime = timestamp; }
|
||||
@@ -412,33 +395,34 @@ public:
|
||||
String getAPSSID() const { return networkConfig.apSsid; }
|
||||
bool isHealthy() const;
|
||||
|
||||
/**
|
||||
* @brief Get all configuration settings as a JSON string
|
||||
* @return JSON string containing all current settings
|
||||
*/
|
||||
String getAllSettingsAsJson() const;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// FACTORY RESET
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Perform complete factory reset
|
||||
* @brief Perform a full Reset of the Settings to Factory Defaults
|
||||
*
|
||||
* This method:
|
||||
* 1. Deletes all configuration files from SD card
|
||||
* 2. Does NOT touch NVS (device identity remains)
|
||||
* 3. On next boot, all settings will be recreated with defaults
|
||||
* 1. Clears all user Settings on the SD card
|
||||
* 2. Deletes all Melodies on the SD card
|
||||
*
|
||||
* @return true if factory reset successful
|
||||
* @return true if reset successful, false otherwise
|
||||
*/
|
||||
bool factoryReset();
|
||||
bool resetAllToDefaults();
|
||||
|
||||
/**
|
||||
* @brief Delete all settings files from SD card
|
||||
* @return true if all files deleted successfully
|
||||
*/
|
||||
bool clearAllSettings();
|
||||
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
// DEPLOYMENT NOTES:
|
||||
// 2. USER SETTINGS: All loaded from SD card, configured via app
|
||||
// 3. NETWORK: WiFiManager handles credentials, no hardcoded SSIDs/passwords
|
||||
// 2. USER SETTINGS: All loaded from SD card, configurable via app
|
||||
// 3. NETWORK: WiFiManager handles SSIDs/Passwords. IP Settigns loaded from SD, configurable via app
|
||||
// 4. IDENTIFIERS: Auto-generated from deviceUID for consistency
|
||||
// 5. DEFAULTS: Clean minimal defaults, everything configurable
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#include "HealthMonitor.hpp"
|
||||
#include "../BellEngine/BellEngine.hpp"
|
||||
#include "../OutputManager/OutputManager.hpp"
|
||||
#include "../Communication/Communication.hpp"
|
||||
#include "../Communication/CommunicationRouter/CommunicationRouter.hpp"
|
||||
#include "../Player/Player.hpp"
|
||||
#include "../TimeKeeper/TimeKeeper.hpp"
|
||||
#include "../Telemetry/Telemetry.hpp"
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
// Forward declarations for all monitored subsystems
|
||||
class BellEngine;
|
||||
class OutputManager;
|
||||
class Communication;
|
||||
class CommunicationRouter;
|
||||
class Player;
|
||||
class Timekeeper;
|
||||
class Telemetry;
|
||||
@@ -137,7 +137,7 @@ public:
|
||||
void setOutputManager(OutputManager* outputManager) { _outputManager = outputManager; }
|
||||
|
||||
/** @brief Register Communication for monitoring */
|
||||
void setCommunication(Communication* communication) { _communication = communication; }
|
||||
void setCommunication(CommunicationRouter* communication) { _communication = communication; }
|
||||
|
||||
/** @brief Register Player for monitoring */
|
||||
void setPlayer(Player* player) { _player = player; }
|
||||
@@ -256,7 +256,7 @@ private:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
BellEngine* _bellEngine = nullptr;
|
||||
OutputManager* _outputManager = nullptr;
|
||||
Communication* _communication = nullptr;
|
||||
CommunicationRouter* _communication = nullptr;
|
||||
Player* _player = nullptr;
|
||||
Timekeeper* _timeKeeper = nullptr;
|
||||
Telemetry* _telemetry = nullptr;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -12,9 +12,9 @@ Networking::Networking(ConfigManager& configManager)
|
||||
, _state(NetworkState::DISCONNECTED)
|
||||
, _activeConnection(ConnectionType::NONE)
|
||||
, _lastConnectionAttempt(0)
|
||||
, _bootStartTime(0)
|
||||
, _bootSequenceComplete(false)
|
||||
, _ethernetCableConnected(false)
|
||||
, _wifiConnectionFailures(0)
|
||||
, _wifiManager(nullptr)
|
||||
, _reconnectionTimer(nullptr) {
|
||||
|
||||
@@ -46,11 +46,10 @@ Networking::~Networking() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Networking::begin() {
|
||||
LOG_INFO("Initializing Networking System");
|
||||
|
||||
_bootStartTime = millis();
|
||||
|
||||
// Create reconnection timer
|
||||
_reconnectionTimer = xTimerCreate("reconnectionTimer", pdMS_TO_TICKS(RECONNECTION_INTERVAL),
|
||||
pdTRUE, (void*)0, reconnectionTimerCallback);
|
||||
@@ -60,7 +59,10 @@ void Networking::begin() {
|
||||
|
||||
// Configure WiFiManager
|
||||
_wifiManager->setDebugOutput(false);
|
||||
_wifiManager->setConfigPortalTimeout(180); // 3 minutes
|
||||
_wifiManager->setConfigPortalTimeout(300); // 5 minutes
|
||||
|
||||
// Clear Previous Settings, USE once to test.
|
||||
//_wifiManager->resetSettings();
|
||||
|
||||
// Start Ethernet hardware
|
||||
auto& hwConfig = _configManager.getHardwareConfig();
|
||||
@@ -98,13 +100,16 @@ void Networking::startWiFiConnection() {
|
||||
|
||||
if (!hasValidWiFiCredentials()) {
|
||||
LOG_WARNING("No valid WiFi credentials found");
|
||||
if (shouldStartPortal()) {
|
||||
if (!_bootSequenceComplete) {
|
||||
// No credentials during boot - start portal
|
||||
startWiFiPortal();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Using WiFiManager saved credentials");
|
||||
// Get and log saved credentials (for debugging)
|
||||
String savedSSID = _wifiManager->getWiFiSSID(true);
|
||||
LOG_INFO("Using WiFiManager saved credentials - SSID: %s", savedSSID.c_str());
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
applyNetworkConfig(false); // false = WiFi config
|
||||
@@ -158,12 +163,36 @@ void Networking::handleReconnection() {
|
||||
return; // Still waiting for Ethernet
|
||||
}
|
||||
|
||||
// Check for WiFi timeout (try again)
|
||||
// Check for WiFi timeout
|
||||
if (_state == NetworkState::CONNECTING_WIFI) {
|
||||
unsigned long now = millis();
|
||||
if (now - _lastConnectionAttempt > 10000) { // 10 second timeout
|
||||
LOG_INFO("WiFi connection timeout - retrying");
|
||||
startWiFiConnection(); // Retry WiFi
|
||||
_wifiConnectionFailures++;
|
||||
LOG_WARNING("WiFi connection timeout (failure #%d)", _wifiConnectionFailures);
|
||||
|
||||
// After 3 failed attempts during boot, start portal
|
||||
if (_wifiConnectionFailures >= MAX_WIFI_FAILURES) {
|
||||
LOG_ERROR("Multiple WiFi connection failures - credentials may be invalid");
|
||||
|
||||
if (!_bootSequenceComplete) {
|
||||
// Boot not complete yet - open portal
|
||||
LOG_INFO("Opening WiFi portal for reconfiguration");
|
||||
_wifiConnectionFailures = 0; // Reset counter
|
||||
startWiFiPortal();
|
||||
} else {
|
||||
// Boot already complete - just keep retrying
|
||||
LOG_WARNING("WiFi connection lost - continuing retry attempts");
|
||||
// Reset counter after extended failure to prevent overflow
|
||||
if (_wifiConnectionFailures > 10) {
|
||||
_wifiConnectionFailures = 3;
|
||||
}
|
||||
_lastConnectionAttempt = now;
|
||||
startWiFiConnection();
|
||||
}
|
||||
} else {
|
||||
// Retry WiFi connection
|
||||
startWiFiConnection();
|
||||
}
|
||||
}
|
||||
return; // Still waiting for WiFi
|
||||
}
|
||||
@@ -176,7 +205,8 @@ void Networking::handleReconnection() {
|
||||
LOG_INFO("No Ethernet - trying WiFi");
|
||||
if (hasValidWiFiCredentials()) {
|
||||
startWiFiConnection();
|
||||
} else if (shouldStartPortal()) {
|
||||
} else if (!_bootSequenceComplete) {
|
||||
// No credentials during boot - start portal
|
||||
startWiFiPortal();
|
||||
} else {
|
||||
LOG_WARNING("No WiFi credentials and boot sequence complete - waiting");
|
||||
@@ -292,6 +322,9 @@ void Networking::onWiFiConnected() {
|
||||
setState(NetworkState::CONNECTED_WIFI);
|
||||
setActiveConnection(ConnectionType::WIFI);
|
||||
|
||||
// Reset failure counter on successful connection
|
||||
_wifiConnectionFailures = 0;
|
||||
|
||||
// Stop reconnection timer
|
||||
xTimerStop(_reconnectionTimer, 0);
|
||||
|
||||
@@ -347,18 +380,13 @@ void Networking::applyNetworkConfig(bool ethernet) {
|
||||
}
|
||||
|
||||
bool Networking::hasValidWiFiCredentials() {
|
||||
// Check if WiFiManager has saved credentials
|
||||
return WiFi.SSID().length() > 0;
|
||||
// Use WiFiManager's method to check if credentials are saved
|
||||
return _wifiManager->getWiFiIsSaved();
|
||||
}
|
||||
|
||||
bool Networking::shouldStartPortal() {
|
||||
// Only start portal during boot sequence and if we're truly disconnected
|
||||
return !_bootSequenceComplete &&
|
||||
(millis() - _bootStartTime < BOOT_TIMEOUT) &&
|
||||
_activeConnection == ConnectionType::NONE;
|
||||
}
|
||||
|
||||
// Status methods
|
||||
|
||||
// Returns if Networking is connected
|
||||
bool Networking::isConnected() const {
|
||||
return _activeConnection != ConnectionType::NONE;
|
||||
}
|
||||
@@ -445,12 +473,5 @@ void Networking::networkEventHandler(arduino_event_id_t event, arduino_event_inf
|
||||
void Networking::reconnectionTimerCallback(TimerHandle_t xTimer) {
|
||||
if (_instance) {
|
||||
_instance->handleReconnection();
|
||||
|
||||
// Check if boot sequence should be marked complete
|
||||
if (!_instance->_bootSequenceComplete &&
|
||||
(millis() - _instance->_bootStartTime > BOOT_TIMEOUT)) {
|
||||
_instance->_bootSequenceComplete = true;
|
||||
LOG_INFO("Boot sequence timeout - no more portal attempts");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ public:
|
||||
|
||||
void begin();
|
||||
|
||||
// Status methods
|
||||
// Returns whether the network is currently connected
|
||||
bool isConnected() const;
|
||||
String getLocalIP() const;
|
||||
ConnectionType getActiveConnection() const { return _activeConnection; }
|
||||
@@ -119,9 +119,9 @@ private:
|
||||
NetworkState _state;
|
||||
ConnectionType _activeConnection;
|
||||
unsigned long _lastConnectionAttempt;
|
||||
unsigned long _bootStartTime;
|
||||
bool _bootSequenceComplete;
|
||||
bool _ethernetCableConnected;
|
||||
int _wifiConnectionFailures; // Track consecutive WiFi failures
|
||||
|
||||
// Callbacks
|
||||
std::function<void()> _onNetworkConnected;
|
||||
@@ -151,12 +151,11 @@ private:
|
||||
// Utility methods
|
||||
void applyNetworkConfig(bool ethernet = false);
|
||||
bool hasValidWiFiCredentials();
|
||||
bool shouldStartPortal();
|
||||
|
||||
// Timer callback
|
||||
static void reconnectionTimerCallback(TimerHandle_t xTimer);
|
||||
|
||||
// Constants
|
||||
static const unsigned long RECONNECTION_INTERVAL = 5000; // 5 seconds
|
||||
static const unsigned long BOOT_TIMEOUT = 30000; // 30 seconds for boot sequence
|
||||
static const int MAX_WIFI_FAILURES = 3; // Portal after 3 failures
|
||||
};
|
||||
|
||||
@@ -1,33 +1,109 @@
|
||||
#include "OTAManager.hpp"
|
||||
#include "../ConfigManager/ConfigManager.hpp"
|
||||
#include "../Logging/Logging.hpp"
|
||||
#include "../Player/Player.hpp"
|
||||
#include <nvs_flash.h>
|
||||
#include <nvs.h>
|
||||
|
||||
OTAManager::OTAManager(ConfigManager& configManager)
|
||||
: _configManager(configManager)
|
||||
, _fileManager(nullptr)
|
||||
, _player(nullptr)
|
||||
, _status(Status::IDLE)
|
||||
, _lastError(ErrorCode::NONE)
|
||||
, _availableVersion(0.0f)
|
||||
, _minVersion(0.0f)
|
||||
, _expectedFileSize(0)
|
||||
, _updateAvailable(false)
|
||||
, _availableChecksum("")
|
||||
, _updateChannel("stable")
|
||||
, _isMandatory(false)
|
||||
, _isEmergency(false)
|
||||
, _progressCallback(nullptr)
|
||||
, _statusCallback(nullptr) {
|
||||
, _statusCallback(nullptr)
|
||||
, _scheduledCheckTimer(NULL) {
|
||||
}
|
||||
|
||||
OTAManager::~OTAManager() {
|
||||
if (_scheduledCheckTimer != NULL) {
|
||||
xTimerStop(_scheduledCheckTimer, 0);
|
||||
xTimerDelete(_scheduledCheckTimer, portMAX_DELAY);
|
||||
_scheduledCheckTimer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void OTAManager::begin() {
|
||||
LOG_INFO("OTA Manager initialized");
|
||||
setStatus(Status::IDLE);
|
||||
|
||||
// Create timer for scheduled checks (checks every minute if it's 3:00 AM)
|
||||
_scheduledCheckTimer = xTimerCreate(
|
||||
"OTA_Schedule",
|
||||
pdMS_TO_TICKS(60000), // Check every minute
|
||||
pdTRUE, // Auto-reload (periodic)
|
||||
this, // Timer ID (pass OTAManager instance)
|
||||
scheduledCheckCallback
|
||||
);
|
||||
|
||||
if (_scheduledCheckTimer != NULL) {
|
||||
xTimerStart(_scheduledCheckTimer, 0);
|
||||
LOG_INFO("OTA scheduled check timer started (will check at 3:00 AM)");
|
||||
} else {
|
||||
LOG_ERROR("Failed to create OTA scheduled check timer!");
|
||||
}
|
||||
}
|
||||
|
||||
void OTAManager::setFileManager(FileManager* fm) {
|
||||
_fileManager = fm;
|
||||
}
|
||||
|
||||
void OTAManager::setPlayer(Player* player) {
|
||||
_player = player;
|
||||
}
|
||||
|
||||
// ✅ NEW: Static timer callback for scheduled checks
|
||||
void OTAManager::scheduledCheckCallback(TimerHandle_t xTimer) {
|
||||
OTAManager* ota = static_cast<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() {
|
||||
// Boot-time check: only check stable channel for emergency/mandatory updates
|
||||
checkForUpdates("stable");
|
||||
@@ -39,6 +115,12 @@ void OTAManager::checkForUpdates(const String& channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 CRITICAL: Check network connectivity before attempting HTTP requests
|
||||
if (WiFi.status() != WL_CONNECTED && !ETH.linkUp()) {
|
||||
LOG_WARNING("OTA check skipped - no network connectivity");
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(Status::CHECKING_VERSION);
|
||||
LOG_INFO("Checking for firmware updates in %s channel for %s...",
|
||||
channel.c_str(), _configManager.getHardwareVariant().c_str());
|
||||
@@ -118,7 +200,7 @@ void OTAManager::notifyProgress(size_t current, size_t total) {
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced version checking with channel support and multiple servers
|
||||
// ✅ ENHANCED: Version checking with full validation
|
||||
bool OTAManager::checkVersion(const String& channel) {
|
||||
std::vector<String> servers = _configManager.getUpdateServers();
|
||||
auto& updateConfig = _configManager.getUpdateConfig();
|
||||
@@ -167,6 +249,16 @@ bool OTAManager::checkVersion(const String& channel) {
|
||||
_updateChannel = doc["channel"].as<String>();
|
||||
_isMandatory = doc["mandatory"].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
|
||||
String hwVariant = doc["hardwareVariant"].as<String>();
|
||||
@@ -177,6 +269,16 @@ bool OTAManager::checkVersion(const String& channel) {
|
||||
continue; // Try next server
|
||||
}
|
||||
|
||||
// ✅ NEW: Check minVersion compatibility
|
||||
float currentVersion = getCurrentVersion();
|
||||
if (_minVersion > 0.0f && currentVersion < _minVersion) {
|
||||
LOG_ERROR("OTA: Current version %.1f is below minimum required %.1f",
|
||||
currentVersion, _minVersion);
|
||||
LOG_ERROR("OTA: Intermediate update required first - cannot proceed");
|
||||
_lastError = ErrorCode::VERSION_TOO_LOW;
|
||||
continue; // Try next server
|
||||
}
|
||||
|
||||
if (_availableVersion == 0.0f) {
|
||||
LOG_ERROR("OTA: Invalid version in metadata from %s", baseUrl.c_str());
|
||||
continue; // Try next server
|
||||
@@ -188,6 +290,8 @@ bool OTAManager::checkVersion(const String& channel) {
|
||||
}
|
||||
|
||||
LOG_INFO("OTA: Successfully got metadata from %s", baseUrl.c_str());
|
||||
LOG_INFO("OTA: Expected file size: %u bytes, Min version: %.1f",
|
||||
_expectedFileSize, _minVersion);
|
||||
return true; // Success!
|
||||
} else {
|
||||
LOG_ERROR("OTA: Server %s failed after %d retries. HTTP error: %d",
|
||||
@@ -202,7 +306,7 @@ bool OTAManager::checkVersion(const String& channel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enhanced download and install with channel support and multiple servers
|
||||
// ✅ ENHANCED: Download and install with size validation
|
||||
bool OTAManager::downloadAndInstall(const String& channel) {
|
||||
std::vector<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",
|
||||
serverIndex + 1, servers.size(), baseUrl.c_str());
|
||||
|
||||
if (downloadToSD(firmwareUrl, _availableChecksum)) {
|
||||
if (downloadToSD(firmwareUrl, _availableChecksum, _expectedFileSize)) {
|
||||
// Success! Now install from SD
|
||||
return installFromSD("/firmware/staged_update.bin");
|
||||
} else {
|
||||
@@ -226,9 +330,7 @@ bool OTAManager::downloadAndInstall(const String& channel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum) {
|
||||
// This method now receives the exact firmware URL from downloadAndInstall
|
||||
// The server selection logic is handled there
|
||||
bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum, size_t expectedSize) {
|
||||
if (!_fileManager) {
|
||||
LOG_ERROR("FileManager not set!");
|
||||
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||
@@ -260,6 +362,24 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum)
|
||||
return false;
|
||||
}
|
||||
|
||||
// ✅ NEW: Validate file size against metadata
|
||||
if (expectedSize > 0 && (size_t)contentLength != expectedSize) {
|
||||
LOG_ERROR("OTA: File size mismatch! Expected: %u, Got: %d", expectedSize, contentLength);
|
||||
setStatus(Status::FAILED, ErrorCode::SIZE_MISMATCH);
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// ✅ NEW: Check available SD card space
|
||||
if (!checkAvailableSpace(contentLength)) {
|
||||
LOG_ERROR("OTA: Insufficient SD card space for update");
|
||||
setStatus(Status::FAILED, ErrorCode::INSUFFICIENT_SPACE);
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("OTA: Starting download of %d bytes...", contentLength);
|
||||
|
||||
// Open file for writing
|
||||
File file = SD.open(tempPath.c_str(), FILE_WRITE);
|
||||
if (!file) {
|
||||
@@ -272,8 +392,9 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum)
|
||||
WiFiClient* stream = http.getStreamPtr();
|
||||
uint8_t buffer[1024];
|
||||
size_t written = 0;
|
||||
size_t lastLoggedPercent = 0;
|
||||
|
||||
while (http.connected() && written < contentLength) {
|
||||
while (http.connected() && written < (size_t)contentLength) {
|
||||
size_t available = stream->available();
|
||||
if (available) {
|
||||
size_t toRead = min(available, sizeof(buffer));
|
||||
@@ -289,7 +410,17 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum)
|
||||
return false;
|
||||
}
|
||||
written += bytesWritten;
|
||||
|
||||
// ✅ IMPROVED: Progress reporting with percentage
|
||||
notifyProgress(written, contentLength);
|
||||
|
||||
// Log progress every 10%
|
||||
size_t currentPercent = (written * 100) / contentLength;
|
||||
if (currentPercent >= lastLoggedPercent + 10) {
|
||||
LOG_INFO("OTA: Download progress: %u%% (%u/%u bytes)",
|
||||
currentPercent, written, contentLength);
|
||||
lastLoggedPercent = currentPercent;
|
||||
}
|
||||
}
|
||||
}
|
||||
yield();
|
||||
@@ -298,13 +429,13 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum)
|
||||
file.close();
|
||||
http.end();
|
||||
|
||||
if (written != contentLength) {
|
||||
LOG_ERROR("Download incomplete: %d/%d bytes", written, contentLength);
|
||||
if (written != (size_t)contentLength) {
|
||||
LOG_ERROR("Download incomplete: %u/%d bytes", written, contentLength);
|
||||
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Download complete (%d bytes)", written);
|
||||
LOG_INFO("Download complete (%u bytes)", written);
|
||||
|
||||
// Verify checksum
|
||||
if (!verifyChecksum(tempPath, expectedChecksum)) {
|
||||
@@ -522,7 +653,7 @@ bool OTAManager::performManualUpdate(const String& channel) {
|
||||
String firmwareUrl = buildFirmwareUrl(channel);
|
||||
|
||||
// Download to SD first
|
||||
if (!downloadToSD(firmwareUrl, _availableChecksum)) {
|
||||
if (!downloadToSD(firmwareUrl, _availableChecksum, _expectedFileSize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -537,7 +668,6 @@ String OTAManager::getHardwareVariant() const {
|
||||
|
||||
void OTAManager::setHardwareVariant(const String& variant) {
|
||||
LOG_WARNING("OTAManager::setHardwareVariant is deprecated. Use ConfigManager::setHardwareVariant instead");
|
||||
// For backward compatibility, we could call configManager, but it's better to use ConfigManager directly
|
||||
}
|
||||
|
||||
// URL builders for multi-channel architecture
|
||||
@@ -556,6 +686,47 @@ String OTAManager::buildFirmwareUrl(const String& channel) const {
|
||||
return buildChannelUrl(channel) + "firmware.bin";
|
||||
}
|
||||
|
||||
// ✅ NEW: Check if player is currently active
|
||||
bool OTAManager::isPlayerActive() const {
|
||||
if (!_player) {
|
||||
// If player reference not set, assume it's safe to update
|
||||
return false;
|
||||
}
|
||||
|
||||
// Player is active if it's playing or paused (not stopped)
|
||||
return _player->isCurrentlyPlaying() || _player->isCurrentlyPaused();
|
||||
}
|
||||
|
||||
// ✅ NEW: Check if SD card has enough free space
|
||||
bool OTAManager::checkAvailableSpace(size_t requiredBytes) const {
|
||||
if (!_fileManager) {
|
||||
LOG_WARNING("OTA: FileManager not set, cannot check available space");
|
||||
return true; // Assume it's okay if we can't check
|
||||
}
|
||||
|
||||
// Add 10% safety margin
|
||||
size_t requiredWithMargin = requiredBytes + (requiredBytes / 10);
|
||||
|
||||
// Get SD card info
|
||||
uint64_t totalBytes = SD.totalBytes();
|
||||
uint64_t usedBytes = SD.usedBytes();
|
||||
uint64_t freeBytes = totalBytes - usedBytes;
|
||||
|
||||
LOG_INFO("OTA: SD card space - Total: %llu MB, Used: %llu MB, Free: %llu MB",
|
||||
totalBytes / (1024 * 1024),
|
||||
usedBytes / (1024 * 1024),
|
||||
freeBytes / (1024 * 1024));
|
||||
|
||||
if (freeBytes < requiredWithMargin) {
|
||||
LOG_ERROR("OTA: Insufficient space! Required: %u bytes (+10%% margin), Available: %llu bytes",
|
||||
requiredWithMargin, freeBytes);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("OTA: Sufficient space available for update");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK IMPLEMENTATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -599,5 +770,11 @@ bool OTAManager::isHealthy() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if scheduled timer is running
|
||||
if (_scheduledCheckTimer == NULL || xTimerIsTimerActive(_scheduledCheckTimer) == pdFALSE) {
|
||||
LOG_DEBUG("OTAManager: Unhealthy - Scheduled check timer not running");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* This class manages over-the-air firmware updates with safe, reliable
|
||||
* update mechanisms, version checking, and comprehensive error handling.
|
||||
*
|
||||
* 📋 VERSION: 2.0 (Enhanced OTA management)
|
||||
* 📋 VERSION: 2.1 (Enhanced with scheduled checks and full validation)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
@@ -24,9 +24,11 @@
|
||||
#include <mbedtls/md.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <functional>
|
||||
#include <time.h>
|
||||
#include "../FileManager/FileManager.hpp"
|
||||
|
||||
class ConfigManager; // Forward declaration
|
||||
class Player; // Forward declaration for idle check
|
||||
|
||||
class OTAManager {
|
||||
public:
|
||||
@@ -48,7 +50,11 @@ public:
|
||||
WRITE_FAILED,
|
||||
VERIFICATION_FAILED,
|
||||
CHECKSUM_MISMATCH,
|
||||
METADATA_PARSE_FAILED
|
||||
METADATA_PARSE_FAILED,
|
||||
SIZE_MISMATCH,
|
||||
VERSION_TOO_LOW,
|
||||
CHANNEL_MISMATCH,
|
||||
PLAYER_ACTIVE
|
||||
};
|
||||
|
||||
// Callback types
|
||||
@@ -56,11 +62,16 @@ public:
|
||||
using StatusCallback = std::function<void(Status status, ErrorCode error)>;
|
||||
|
||||
explicit OTAManager(ConfigManager& configManager);
|
||||
~OTAManager();
|
||||
|
||||
void begin();
|
||||
void setFileManager(FileManager* fm);
|
||||
void setPlayer(Player* player); // NEW: Set player reference for idle check
|
||||
|
||||
void checkForUpdates();
|
||||
void checkForUpdates(const String& channel); // Check specific channel
|
||||
void checkForEmergencyUpdates(); // NEW: Scheduled emergency-only check
|
||||
|
||||
void update();
|
||||
void update(const String& channel); // Update from specific channel
|
||||
void checkFirmwareUpdateFromSD(); // Check SD for firmware update
|
||||
@@ -92,9 +103,12 @@ public:
|
||||
private:
|
||||
ConfigManager& _configManager;
|
||||
FileManager* _fileManager;
|
||||
Player* _player; // NEW: Player reference for idle check
|
||||
Status _status;
|
||||
ErrorCode _lastError;
|
||||
float _availableVersion;
|
||||
float _minVersion; // NEW: Minimum required version
|
||||
size_t _expectedFileSize; // NEW: Expected firmware file size
|
||||
bool _updateAvailable;
|
||||
String _availableChecksum;
|
||||
String _updateChannel;
|
||||
@@ -104,6 +118,10 @@ private:
|
||||
ProgressCallback _progressCallback;
|
||||
StatusCallback _statusCallback;
|
||||
|
||||
// NEW: Scheduled check timer
|
||||
TimerHandle_t _scheduledCheckTimer;
|
||||
static void scheduledCheckCallback(TimerHandle_t xTimer);
|
||||
|
||||
void setStatus(Status status, ErrorCode error = ErrorCode::NONE);
|
||||
void notifyProgress(size_t current, size_t total);
|
||||
bool checkVersion();
|
||||
@@ -111,11 +129,15 @@ private:
|
||||
bool checkChannelsMetadata();
|
||||
bool downloadAndInstall();
|
||||
bool downloadAndInstall(const String& channel);
|
||||
bool downloadToSD(const String& url, const String& expectedChecksum);
|
||||
bool downloadToSD(const String& url, const String& expectedChecksum, size_t expectedSize); // NEW: Added size param
|
||||
bool verifyChecksum(const String& filePath, const String& expectedChecksum);
|
||||
String calculateSHA256(const String& filePath);
|
||||
bool installFromSD(const String& filePath);
|
||||
String buildChannelUrl(const String& channel) const;
|
||||
String buildMetadataUrl(const String& channel) const;
|
||||
String buildFirmwareUrl(const String& channel) const;
|
||||
|
||||
// NEW: Helper methods
|
||||
bool isPlayerActive() const;
|
||||
bool checkAvailableSpace(size_t requiredBytes) const;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "Player.hpp"
|
||||
#include "../Communication/Communication.hpp"
|
||||
#include "../Communication/CommunicationRouter/CommunicationRouter.hpp"
|
||||
#include "../BellEngine/BellEngine.hpp"
|
||||
|
||||
// Note: Removed global melody_steps dependency for cleaner architecture
|
||||
|
||||
// Constructor with dependencies
|
||||
Player::Player(Communication* comm, FileManager* fm)
|
||||
Player::Player(CommunicationRouter* comm, FileManager* fm)
|
||||
: id(0)
|
||||
, name("melody1")
|
||||
, uid("x")
|
||||
@@ -58,7 +58,7 @@ Player::Player()
|
||||
, _durationTimerHandle(NULL) {
|
||||
}
|
||||
|
||||
void Player::setDependencies(Communication* comm, FileManager* fm) {
|
||||
void Player::setDependencies(CommunicationRouter* comm, FileManager* fm) {
|
||||
_commManager = comm;
|
||||
_fileManager = fm;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// FORWARD DECLARATIONS - Dependencies injected at runtime
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
class Communication; // Command handling and communication
|
||||
class CommunicationRouter; // Command handling and communication
|
||||
class BellEngine; // High-precision timing engine
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
@@ -104,7 +104,7 @@ public:
|
||||
* @param comm Pointer to communication manager
|
||||
* @param fm Pointer to file manager
|
||||
*/
|
||||
Player(Communication* comm, FileManager* fm);
|
||||
Player(CommunicationRouter* comm, FileManager* fm);
|
||||
|
||||
/**
|
||||
* @brief Default constructor for backward compatibility
|
||||
@@ -118,7 +118,7 @@ public:
|
||||
* @param comm Pointer to communication manager
|
||||
* @param fm Pointer to file manager
|
||||
*/
|
||||
void setDependencies(Communication* comm, FileManager* fm);
|
||||
void setDependencies(CommunicationRouter* comm, FileManager* fm);
|
||||
|
||||
/**
|
||||
* @brief Set BellEngine reference for precision timing
|
||||
@@ -238,7 +238,7 @@ private:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PRIVATE DEPENDENCIES AND DATA
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
Communication* _commManager; // 📡 Communication system reference
|
||||
CommunicationRouter* _commManager; // 📡 Communication system reference
|
||||
FileManager* _fileManager; // 📁 File operations reference
|
||||
BellEngine* _bellEngine; // 🔥 High-precision timing engine reference
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "Telemetry.hpp"
|
||||
#include "../Communication/Communication.hpp"
|
||||
|
||||
void Telemetry::begin() {
|
||||
// Initialize arrays
|
||||
@@ -149,17 +148,8 @@ void Telemetry::checkBellLoads() {
|
||||
}
|
||||
}
|
||||
|
||||
// Send batch notifications if any bells are overloaded
|
||||
if (!criticalBells.empty()) {
|
||||
String severity = anyOverload ? "critical" : "warning";
|
||||
if (Communication::_instance) {
|
||||
Communication::_instance->sendBellOverloadNotification(criticalBells, criticalLoads, severity);
|
||||
}
|
||||
} else if (!warningBells.empty()) {
|
||||
if (Communication::_instance) {
|
||||
Communication::_instance->sendBellOverloadNotification(warningBells, warningLoads, "warning");
|
||||
}
|
||||
}
|
||||
// Note: Notifications now handled by BellEngine which has Communication reference
|
||||
// BellEngine monitors telemetry and sends notifications when overloads detected
|
||||
|
||||
// Trigger force stop if any bell is actually overloaded
|
||||
if (anyOverload && forceStopCallback != nullptr) {
|
||||
|
||||
@@ -267,9 +267,13 @@ void Timekeeper::checkAndSyncPhysicalClock() {
|
||||
// Calculate time difference (your exact logic!)
|
||||
int16_t timeDifference = (realHour * 60 + realMinute) - (physicalHour * 60 + physicalMinute);
|
||||
|
||||
LOG_VERBOSE("⏰ CHECK: Real time %02d:%02d vs Physical %02d:%02d - DIFF: %d mins",
|
||||
realHour, realMinute, physicalHour, physicalMinute, timeDifference);
|
||||
|
||||
// Handle 12-hour rollover (if negative, add 12 hours)
|
||||
if (timeDifference < 0) {
|
||||
timeDifference += 12 * 60; // Add 12 hours to handle rollover
|
||||
LOG_VERBOSE("⏰ DIFF: Adjusted for rollover, new difference %d minutes", timeDifference);
|
||||
}
|
||||
|
||||
// If there's a difference, advance the clock by one minute (your runMotor equivalent)
|
||||
@@ -310,7 +314,7 @@ void Timekeeper::updatePhysicalClockTime() {
|
||||
currentMinute = 0;
|
||||
currentHour++;
|
||||
if (currentHour > 12) { // 12-hour clock (your code used 24, but clock face is 12)
|
||||
currentHour = 0;
|
||||
currentHour = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
* ✅ Comprehensive logging system
|
||||
*
|
||||
* 📡 COMMUNICATION PROTOCOLS:
|
||||
* • MQTT (Primary control interface)
|
||||
* • MQTT (SSL/TLS via PubSubClient on Core 0)
|
||||
* • WebSocket (Real-time web interface)
|
||||
* • UDP Discovery (Auto-discovery service)
|
||||
* • HTTP/HTTPS (OTA updates)
|
||||
@@ -48,12 +48,39 @@
|
||||
* High-priority FreeRTOS tasks ensure microsecond timing precision.
|
||||
* Core 1 dedicated to BellEngine for maximum performance.
|
||||
*
|
||||
* 📋 VERSION: 1.1
|
||||
* 📅 DATE: 2025-09-08
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
* 📋 VERSION CONFIGURATION
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
* 📅 DATE: 2025-10-10
|
||||
* 👨💻 AUTHOR: BellSystems bonamin
|
||||
*/
|
||||
|
||||
#define FW_VERSION "0.1"
|
||||
|
||||
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
* 📅 VERSION HISTORY:
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
* v0.1 - Vesper Launch Beta
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
// SYSTEM LIBRARIES - Core ESP32 and Arduino functionality
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
@@ -71,7 +98,6 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
// NETWORKING LIBRARIES - Advanced networking and communication
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
#include <AsyncMqttClient.h> // High-performance async MQTT client
|
||||
#include <WiFiManager.h> // WiFi configuration portal
|
||||
#include <ESPAsyncWebServer.h> // Async web server for WebSocket support
|
||||
#include <AsyncUDP.h> // UDP for discovery service
|
||||
@@ -98,16 +124,15 @@
|
||||
#include "src/Telemetry/Telemetry.hpp"
|
||||
#include "src/OTAManager/OTAManager.hpp"
|
||||
#include "src/Networking/Networking.hpp"
|
||||
#include "src/Communication/Communication.hpp"
|
||||
#include "src/Communication/CommunicationRouter/CommunicationRouter.hpp"
|
||||
#include "src/ClientManager/ClientManager.hpp"
|
||||
#include "src/Communication/ResponseBuilder.hpp"
|
||||
#include "src/Communication/ResponseBuilder/ResponseBuilder.hpp"
|
||||
#include "src/Player/Player.hpp"
|
||||
#include "src/BellEngine/BellEngine.hpp"
|
||||
#include "src/OutputManager/OutputManager.hpp"
|
||||
#include "src/HealthMonitor/HealthMonitor.hpp"
|
||||
#include "src/FirmwareValidator/FirmwareValidator.hpp"
|
||||
#include "src/InputManager/InputManager.hpp"
|
||||
#include "src/MqttSSL/MqttSSL.hpp"
|
||||
|
||||
// Class Constructors
|
||||
ConfigManager configManager;
|
||||
@@ -115,13 +140,12 @@ FileManager fileManager(&configManager);
|
||||
Timekeeper timekeeper;
|
||||
Telemetry telemetry;
|
||||
OTAManager otaManager(configManager);
|
||||
AsyncMqttClient mqttClient;
|
||||
Player player;
|
||||
AsyncWebServer server(80);
|
||||
AsyncWebSocket ws("/ws");
|
||||
AsyncUDP udp;
|
||||
Networking networking(configManager);
|
||||
Communication communication(configManager, otaManager, networking, mqttClient, server, ws, udp);
|
||||
CommunicationRouter communication(configManager, otaManager, networking, server, ws, udp);
|
||||
HealthMonitor healthMonitor;
|
||||
FirmwareValidator firmwareValidator;
|
||||
InputManager inputManager;
|
||||
@@ -158,7 +182,7 @@ TimerHandle_t schedulerTimer;
|
||||
|
||||
|
||||
void handleFactoryReset() {
|
||||
if (configManager.factoryReset()) {
|
||||
if (configManager.resetAllToDefaults()) {
|
||||
delay(3000);
|
||||
ESP.restart();
|
||||
}
|
||||
@@ -176,21 +200,22 @@ void setup()
|
||||
SPI.begin(hwConfig.ethSpiSck, hwConfig.ethSpiMiso, hwConfig.ethSpiMosi);
|
||||
delay(50);
|
||||
|
||||
// Initialize Configuration (this loads device identity from SD card)
|
||||
// Initialize Configuration (loads factory identity from NVS + user settings from SD)
|
||||
configManager.begin();
|
||||
|
||||
inputManager.begin();
|
||||
inputManager.setFactoryResetLongPressCallback(handleFactoryReset);
|
||||
|
||||
|
||||
// Set factory values:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// REMOVED: Manual device identity setters
|
||||
// Device identity (UID, hwType, hwVersion) is now READ-ONLY in production firmware
|
||||
// These values are set by factory firmware and stored permanently in NVS
|
||||
// Production firmware loads them once at boot and keeps them in RAM
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
configManager.setDeviceUID("PV202508190002");
|
||||
configManager.setHwType("BellPlus");
|
||||
configManager.setHwVersion("1.0");
|
||||
configManager.setFwVersion("1.1");
|
||||
LOG_INFO("Device identity initialized");
|
||||
// Update firmware version (this is the ONLY identity field that can be set)
|
||||
configManager.setFwVersion(FW_VERSION);
|
||||
LOG_INFO("Firmware version: %s", FW_VERSION);
|
||||
|
||||
|
||||
// Display device information after configuration is loaded
|
||||
@@ -277,12 +302,13 @@ void setup()
|
||||
|
||||
// BellEngine already initialized and registered earlier for health validation
|
||||
|
||||
// Initialize Communication Manager
|
||||
// Initialize Communication Manager (now with PubSubClient MQTT)
|
||||
communication.begin();
|
||||
communication.setPlayerReference(&player);
|
||||
communication.setFileManagerReference(&fileManager);
|
||||
communication.setTimeKeeperReference(&timekeeper);
|
||||
communication.setFirmwareValidatorReference(&firmwareValidator);
|
||||
|
||||
player.setDependencies(&communication, &fileManager);
|
||||
player.setBellEngine(&bellEngine); // Connect the beast!
|
||||
|
||||
@@ -294,7 +320,15 @@ void setup()
|
||||
|
||||
// Set up network callbacks
|
||||
networking.setNetworkCallbacks(
|
||||
[]() { communication.onNetworkConnected(); }, // onConnected
|
||||
[]() {
|
||||
communication.onNetworkConnected();
|
||||
// Start AsyncWebServer when network becomes available
|
||||
if (networking.getState() != NetworkState::WIFI_PORTAL_MODE) {
|
||||
LOG_INFO("🚀 Starting AsyncWebServer on port 80...");
|
||||
server.begin();
|
||||
LOG_INFO("✅ AsyncWebServer started on http://%s", networking.getLocalIP().c_str());
|
||||
}
|
||||
}, // onConnected
|
||||
[]() { communication.onNetworkDisconnected(); } // onDisconnected
|
||||
);
|
||||
|
||||
@@ -302,6 +336,14 @@ void setup()
|
||||
if (networking.isConnected()) {
|
||||
LOG_INFO("Network already connected - triggering MQTT connection");
|
||||
communication.onNetworkConnected();
|
||||
|
||||
// 🔥 CRITICAL: Start AsyncWebServer ONLY when network is ready
|
||||
// Do NOT start if WiFiManager portal is active (port 80 conflict!)
|
||||
LOG_INFO("🚀 Starting AsyncWebServer on port 80...");
|
||||
server.begin();
|
||||
LOG_INFO("✅ AsyncWebServer started and listening on http://%s", networking.getLocalIP().c_str());
|
||||
} else {
|
||||
LOG_WARNING("⚠️ Network not ready - AsyncWebServer will start after connection");
|
||||
}
|
||||
|
||||
delay(500);
|
||||
@@ -309,6 +351,7 @@ void setup()
|
||||
// Initialize OTA Manager and check for updates
|
||||
otaManager.begin();
|
||||
otaManager.setFileManager(&fileManager);
|
||||
otaManager.setPlayer(&player); // Set player reference for idle check
|
||||
|
||||
// 🔥 CRITICAL: Delay OTA check to avoid UDP socket race with MQTT
|
||||
// Both MQTT and OTA HTTP use UDP sockets, must sequence them!
|
||||
@@ -320,8 +363,9 @@ void setup()
|
||||
// Register OTA Manager with health monitor
|
||||
healthMonitor.setOTAManager(&otaManager);
|
||||
|
||||
// Start the server
|
||||
server.begin();
|
||||
// Note: AsyncWebServer will be started by network callbacks when connection is ready
|
||||
// This avoids port 80 conflicts with WiFiManager's captive portal
|
||||
|
||||
|
||||
// 🔥 START RUNTIME VALIDATION: All subsystems are now initialized
|
||||
// Begin extended runtime validation if we're in testing mode
|
||||
@@ -333,6 +377,7 @@ void setup()
|
||||
LOG_INFO("✅ Firmware already validated - normal operation mode");
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// INITIALIZATION COMPLETE
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -340,7 +385,7 @@ void setup()
|
||||
// • BellEngine creates high-priority timing task on Core 1
|
||||
// • Telemetry creates monitoring task for load tracking
|
||||
// • Player creates duration timer for playback control
|
||||
// • Communication creates MQTT reconnection timers
|
||||
// • Communication creates MQTT task on Core 0 with PubSubClient
|
||||
// • Networking creates connection management timers
|
||||
// ✅ Bell configuration automatically loaded by ConfigManager
|
||||
// ✅ System ready for MQTT commands, WebSocket connections, and UDP discovery
|
||||
@@ -360,7 +405,7 @@ void setup()
|
||||
* • BellEngine: High-priority task on Core 1 for microsecond timing
|
||||
* • Telemetry: Background monitoring task for system health
|
||||
* • Player: Timer-based duration control for melody playback
|
||||
* • Communication: Event-driven MQTT/WebSocket handling
|
||||
* • Communication: MQTT task on Core 0 + Event-driven WebSocket
|
||||
* • Networking: Automatic connection management
|
||||
*
|
||||
* The main loop only handles lightweight operations that don't require
|
||||
@@ -371,26 +416,6 @@ void setup()
|
||||
*/
|
||||
void loop()
|
||||
{
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// INTENTIONALLY MINIMAL - ALL WORK DONE BY DEDICATED TASKS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
//
|
||||
// The loop() function is kept empty by design to ensure maximum
|
||||
// performance for the high-precision BellEngine running on Core 1.
|
||||
//
|
||||
// All system functionality is handled by dedicated FreeRTOS tasks:
|
||||
// • 🔥 BellEngine: Microsecond-precision timing (Core 1, Priority 6)
|
||||
// • 📊 Telemetry: System monitoring (Background task)
|
||||
// • 🎵 Player: Duration management (FreeRTOS timers)
|
||||
// • 📡 Communication: MQTT/WebSocket (Event-driven)
|
||||
// • 🌐 Networking: Connection management (Timer-based)
|
||||
//
|
||||
// If you need to add periodic functionality, consider creating a new
|
||||
// dedicated task instead of putting it here.
|
||||
|
||||
// Uncomment the line below for debugging system status:
|
||||
// Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap());
|
||||
|
||||
// Feed watchdog only during firmware validation
|
||||
if (firmwareValidator.isInTestingMode()) {
|
||||
esp_task_wdt_reset();
|
||||
@@ -403,6 +428,13 @@ void loop()
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 DEBUG: Log every 10 seconds to verify we're still running
|
||||
static unsigned long lastLog = 0;
|
||||
if (millis() - lastLog > 10000) {
|
||||
LOG_DEBUG("❤️ Loop alive, free heap: %d", ESP.getFreeHeap());
|
||||
lastLog = millis();
|
||||
}
|
||||
|
||||
// Keep the loop responsive but not busy
|
||||
delay(100); // ⏱️ 100ms delay to prevent busy waiting
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user