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