Added MQTT Logs, and improved OTA and NTP to Async

This commit is contained in:
2025-12-28 18:39:13 +02:00
parent 8d397c6dd5
commit 0f0b67cab9
18 changed files with 568 additions and 123 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ vesper/CLAUDE.md
vesper/flutter/ vesper/flutter/
vesper/docs_manual/ vesper/docs_manual/
Doxyfile Doxyfile
vesper/.claude/

View File

@@ -13,6 +13,7 @@
#include "../../Telemetry/Telemetry.hpp" #include "../../Telemetry/Telemetry.hpp"
#include "../../Logging/Logging.hpp" #include "../../Logging/Logging.hpp"
#include "../ResponseBuilder/ResponseBuilder.hpp" #include "../ResponseBuilder/ResponseBuilder.hpp"
#include "../CommunicationRouter/CommunicationRouter.hpp"
CommandHandler::CommandHandler(ConfigManager& configManager, OTAManager& otaManager) CommandHandler::CommandHandler(ConfigManager& configManager, OTAManager& otaManager)
: _configManager(configManager) : _configManager(configManager)
@@ -23,6 +24,7 @@ CommandHandler::CommandHandler(ConfigManager& configManager, OTAManager& otaMana
, _firmwareValidator(nullptr) , _firmwareValidator(nullptr)
, _clientManager(nullptr) , _clientManager(nullptr)
, _telemetry(nullptr) , _telemetry(nullptr)
, _communicationRouter(nullptr)
, _responseCallback(nullptr) {} , _responseCallback(nullptr) {}
CommandHandler::~CommandHandler() {} CommandHandler::~CommandHandler() {}
@@ -51,6 +53,10 @@ void CommandHandler::setTelemetryReference(Telemetry* telemetry) {
_telemetry = telemetry; _telemetry = telemetry;
} }
void CommandHandler::setCommunicationRouterReference(CommunicationRouter* comm) {
_communicationRouter = comm;
}
void CommandHandler::setResponseCallback(ResponseCallback callback) { void CommandHandler::setResponseCallback(ResponseCallback callback) {
_responseCallback = callback; _responseCallback = callback;
} }
@@ -1025,6 +1031,10 @@ void CommandHandler::handleSystemCommand(JsonVariant contents, const MessageCont
handleSetSerialLogLevelCommand(contents, context); handleSetSerialLogLevelCommand(contents, context);
} else if (action == "set_sd_log_level") { } else if (action == "set_sd_log_level") {
handleSetSdLogLevelCommand(contents, context); handleSetSdLogLevelCommand(contents, context);
} else if (action == "set_mqtt_log_level") {
handleSetMqttLogLevelCommand(contents, context);
} else if (action == "set_mqtt_enabled") {
handleSetMqttEnabledCommand(contents, context);
} else { } else {
LOG_WARNING("Unknown system action: %s", action.c_str()); LOG_WARNING("Unknown system action: %s", action.c_str());
sendErrorResponse("system", "Unknown action: " + action, context); sendErrorResponse("system", "Unknown action: " + action, context);
@@ -1094,3 +1104,71 @@ void CommandHandler::handleSetSdLogLevelCommand(JsonVariant contents, const Mess
} }
} }
void CommandHandler::handleSetMqttLogLevelCommand(JsonVariant contents, const MessageContext& context) {
if (!contents.containsKey("level")) {
sendErrorResponse("set_mqtt_log_level", "Missing level parameter", context);
return;
}
uint8_t level = contents["level"].as<uint8_t>();
// Set the level in ConfigManager
if (_configManager.setMqttLogLevel(level)) {
// Apply the level to Logging immediately
Logging::setMqttLogLevel((Logging::LogLevel)level);
// Save to SD card
bool saved = _configManager.saveGeneralConfig();
if (saved) {
sendSuccessResponse("set_mqtt_log_level",
"MQTT log level set to " + String(level) + " and saved", context);
LOG_INFO("MQTT log level updated to %d", level);
} else {
sendErrorResponse("set_mqtt_log_level",
"Log level set but failed to save to SD card", context);
LOG_ERROR("Failed to save MQTT log level to SD card");
}
} else {
sendErrorResponse("set_mqtt_log_level",
"Invalid log level (must be 0-5)", context);
}
}
void CommandHandler::handleSetMqttEnabledCommand(JsonVariant contents, const MessageContext& context) {
if (!contents.containsKey("enabled")) {
sendErrorResponse("set_mqtt_enabled", "Missing enabled parameter", context);
return;
}
bool enabled = contents["enabled"].as<bool>();
// Set MQTT enabled state in ConfigManager
_configManager.setMqttEnabled(enabled);
// Save to SD card
bool saved = _configManager.saveGeneralConfig();
if (saved) {
sendSuccessResponse("set_mqtt_enabled",
String("MQTT ") + (enabled ? "enabled" : "disabled") + " and saved", context);
LOG_INFO("MQTT %s by user command", enabled ? "enabled" : "disabled");
// If disabling, disconnect MQTT immediately
// If enabling, trigger connection attempt
if (_communicationRouter) {
if (!enabled) {
_communicationRouter->getMQTTClient().disconnect();
} else {
_communicationRouter->getMQTTClient().connect();
}
} else {
LOG_WARNING("CommunicationRouter reference not set - cannot control MQTT");
}
} else {
sendErrorResponse("set_mqtt_enabled",
"MQTT state changed but failed to save to SD card", context);
LOG_ERROR("Failed to save MQTT enabled state to SD card");
}
}

View File

@@ -34,6 +34,7 @@ class Timekeeper;
class FirmwareValidator; class FirmwareValidator;
class ClientManager; class ClientManager;
class Telemetry; class Telemetry;
class CommunicationRouter;
class CommandHandler { class CommandHandler {
public: public:
@@ -67,6 +68,7 @@ public:
void setFirmwareValidatorReference(FirmwareValidator* fv); void setFirmwareValidatorReference(FirmwareValidator* fv);
void setClientManagerReference(ClientManager* cm); void setClientManagerReference(ClientManager* cm);
void setTelemetryReference(Telemetry* telemetry); void setTelemetryReference(Telemetry* telemetry);
void setCommunicationRouterReference(CommunicationRouter* comm);
/** /**
* @brief Set response callback for sending responses back * @brief Set response callback for sending responses back
@@ -90,6 +92,7 @@ private:
FirmwareValidator* _firmwareValidator; FirmwareValidator* _firmwareValidator;
ClientManager* _clientManager; ClientManager* _clientManager;
Telemetry* _telemetry; Telemetry* _telemetry;
CommunicationRouter* _communicationRouter;
ResponseCallback _responseCallback; ResponseCallback _responseCallback;
// Response helpers // Response helpers
@@ -146,4 +149,8 @@ private:
// Log Level Commands // Log Level Commands
void handleSetSerialLogLevelCommand(JsonVariant contents, const MessageContext& context); void handleSetSerialLogLevelCommand(JsonVariant contents, const MessageContext& context);
void handleSetSdLogLevelCommand(JsonVariant contents, const MessageContext& context); void handleSetSdLogLevelCommand(JsonVariant contents, const MessageContext& context);
void handleSetMqttLogLevelCommand(JsonVariant contents, const MessageContext& context);
// MQTT Control Commands
void handleSetMqttEnabledCommand(JsonVariant contents, const MessageContext& context);
}; };

View File

@@ -60,6 +60,21 @@ void CommunicationRouter::begin() {
_mqttClient.setCallback([this](const String& topic, const String& payload) { _mqttClient.setCallback([this](const String& topic, const String& payload) {
onMqttMessage(topic, payload); onMqttMessage(topic, payload);
}); });
// Setup MQTT logging callback
String logTopic = "vesper/" + _configManager.getDeviceUID() + "/logs";
Logging::setMqttPublishCallback(
[this](const String& topic, const String& payload, int qos) {
_mqttClient.publish(topic, payload, qos, false);
},
logTopic
);
// Apply MQTT log level from config
uint8_t mqttLogLevel = _configManager.getMqttLogLevel();
Logging::setMqttLogLevel((Logging::LogLevel)mqttLogLevel);
LOG_INFO("MQTT logging enabled with level %d on topic: %s", mqttLogLevel, logTopic.c_str());
LOG_INFO("✅ MQTT client initialized"); LOG_INFO("✅ MQTT client initialized");
} catch (...) { } catch (...) {
LOG_ERROR("❌ MQTT initialization failed, but WebSocket is still available"); LOG_ERROR("❌ MQTT initialization failed, but WebSocket is still available");
@@ -69,6 +84,10 @@ void CommunicationRouter::begin() {
_commandHandler.setClientManagerReference(&_clientManager); _commandHandler.setClientManagerReference(&_clientManager);
LOG_INFO("ClientManager reference set for CommandHandler"); LOG_INFO("ClientManager reference set for CommandHandler");
// 🔥 Set CommunicationRouter reference for MQTT control commands
_commandHandler.setCommunicationRouterReference(this);
LOG_INFO("CommunicationRouter reference set for CommandHandler");
// Setup command handler response callback // Setup command handler response callback
_commandHandler.setResponseCallback([this](const String& response, const CommandHandler::MessageContext& context) { _commandHandler.setResponseCallback([this](const String& response, const CommandHandler::MessageContext& context) {
sendResponse(response, context); sendResponse(response, context);
@@ -121,7 +140,7 @@ void CommunicationRouter::setupUdpDiscovery() {
StaticJsonDocument<128> req; StaticJsonDocument<128> req;
DeserializationError err = deserializeJson(req, msg); DeserializationError err = deserializeJson(req, msg);
if (!err) { if (!err) {
shouldReply = (req["op"] == "discover" && req["svc"] == "vesper"); shouldReply = (req["op"] == "discover");
} }
} }
@@ -136,7 +155,7 @@ void CommunicationRouter::setupUdpDiscovery() {
doc["id"] = _configManager.getDeviceUID(); doc["id"] = _configManager.getDeviceUID();
doc["ip"] = _networking.getLocalIP(); doc["ip"] = _networking.getLocalIP();
char wsUrl[64]; char wsUrl[64];
snprintf(wsUrl, sizeof(wsUrl), "ws://%s/ws", _networking.getLocalIP().c_str()); snprintf(wsUrl, sizeof(wsUrl), "ws://%s:80/ws", _networking.getLocalIP().c_str());
doc["ws"] = wsUrl; doc["ws"] = wsUrl;
doc["port"] = 80; doc["port"] = 80;
doc["fw"] = "2.0"; doc["fw"] = "2.0";

View File

@@ -74,6 +74,9 @@ public:
size_t getWebSocketClientCount() const; size_t getWebSocketClientCount() const;
bool isHealthy() const; bool isHealthy() const;
// Component accessors
MQTTAsyncClient& getMQTTClient() { return _mqttClient; }
// Broadcast methods // Broadcast methods
void broadcastStatus(const String& statusMessage); void broadcastStatus(const String& statusMessage);
void broadcastStatus(const JsonDocument& statusJson); void broadcastStatus(const JsonDocument& statusJson);

View File

@@ -14,19 +14,31 @@ MQTTAsyncClient::MQTTAsyncClient(ConfigManager& configManager, Networking& netwo
, _networking(networking) , _networking(networking)
, _messageCallback(nullptr) , _messageCallback(nullptr)
, _mqttReconnectTimer(nullptr) , _mqttReconnectTimer(nullptr)
, _heartbeatTimer(nullptr) { , _networkStabilizationTimer(nullptr)
, _heartbeatTimer(nullptr)
, _reconnectAttempts(0)
, _lastConnectionAttempt(0) {
_instance = this; // Set static instance pointer _instance = this; // Set static instance pointer
// Create reconnection timer // Create reconnection timer (initial delay will be calculated dynamically)
_mqttReconnectTimer = xTimerCreate( _mqttReconnectTimer = xTimerCreate(
"mqttReconnect", // Timer name (for debugging) "mqttReconnect", // Timer name (for debugging)
pdMS_TO_TICKS(MQTT_RECONNECT_DELAY), // Period: 5000ms = 5 seconds pdMS_TO_TICKS(MQTT_RECONNECT_BASE_DELAY), // Initial period: 5000ms = 5 seconds
pdFALSE, // One-shot (false) or Auto-reload (true) pdFALSE, // One-shot (false) or Auto-reload (true)
(void*)0, // Timer ID (can store data) (void*)0, // Timer ID (can store data)
mqttReconnectTimerCallback // Callback function when timer expires mqttReconnectTimerCallback // Callback function when timer expires
); );
// Create network stabilization timer (one-shot, 2 seconds)
_networkStabilizationTimer = xTimerCreate(
"mqttNetStable", // Timer name
pdMS_TO_TICKS(NETWORK_STABILIZATION_DELAY), // Period: 2000ms = 2 seconds
pdFALSE, // One-shot timer
(void*)0, // Timer ID
networkStabilizationTimerCallback // Callback function
);
// Create heartbeat timer (auto-reload every 30 seconds) // Create heartbeat timer (auto-reload every 30 seconds)
_heartbeatTimer = xTimerCreate( _heartbeatTimer = xTimerCreate(
"mqttHeartbeat", // Timer name "mqttHeartbeat", // Timer name
@@ -42,6 +54,10 @@ MQTTAsyncClient::~MQTTAsyncClient() {
if (_mqttReconnectTimer) { if (_mqttReconnectTimer) {
xTimerDelete(_mqttReconnectTimer, portMAX_DELAY); xTimerDelete(_mqttReconnectTimer, portMAX_DELAY);
} }
if (_networkStabilizationTimer) {
xTimerStop(_networkStabilizationTimer, 0);
xTimerDelete(_networkStabilizationTimer, portMAX_DELAY);
}
if (_heartbeatTimer) { if (_heartbeatTimer) {
xTimerStop(_heartbeatTimer, 0); xTimerStop(_heartbeatTimer, 0);
xTimerDelete(_heartbeatTimer, portMAX_DELAY); xTimerDelete(_heartbeatTimer, portMAX_DELAY);
@@ -98,12 +114,21 @@ void MQTTAsyncClient::begin() {
} }
void MQTTAsyncClient::connect() { void MQTTAsyncClient::connect() {
auto& mqttConfig = _configManager.getMqttConfig();
// 🔥 Check if MQTT is enabled
if (!mqttConfig.enabled) {
LOG_DEBUG("MQTT is disabled in configuration - skipping connection");
return;
}
if (_mqttClient.connected()) { if (_mqttClient.connected()) {
LOG_DEBUG("Already connected to MQTT"); LOG_DEBUG("Already connected to MQTT");
return; return;
} }
auto& mqttConfig = _configManager.getMqttConfig(); // Track connection attempt
_lastConnectionAttempt = millis();
LOG_INFO("Free heap BEFORE MQTT connect: %d bytes", ESP.getFreeHeap()); LOG_INFO("Free heap BEFORE MQTT connect: %d bytes", ESP.getFreeHeap());
@@ -118,6 +143,12 @@ void MQTTAsyncClient::disconnect() {
} }
uint16_t MQTTAsyncClient::publish(const String& topic, const String& payload, int qos, bool retain) { uint16_t MQTTAsyncClient::publish(const String& topic, const String& payload, int qos, bool retain) {
// Check if connected before attempting to publish
if (!_mqttClient.connected()) {
// Don't log error here - would cause infinite loop with MQTT logging
return 0;
}
// Build full topic (if relative) // Build full topic (if relative)
String fullTopic = topic.startsWith("vesper/") ? topic : _dataTopic; String fullTopic = topic.startsWith("vesper/") ? topic : _dataTopic;
@@ -125,9 +156,8 @@ uint16_t MQTTAsyncClient::publish(const String& topic, const String& payload, in
if (packetId > 0) { if (packetId > 0) {
LOG_DEBUG("Published to %s: %s (packetId=%d)", fullTopic.c_str(), payload.c_str(), packetId); 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());
} }
// REMOVED: Error logging here to prevent infinite recursion with MQTT logs
return packetId; return packetId;
} }
@@ -141,13 +171,28 @@ bool MQTTAsyncClient::isConnected() const {
} }
void MQTTAsyncClient::onNetworkConnected() { void MQTTAsyncClient::onNetworkConnected() {
LOG_DEBUG("Network connected - waiting 2 seconds for network stack to stabilize..."); auto& mqttConfig = _configManager.getMqttConfig();
// Small delay to ensure network stack is fully ready // 🔥 Only attempt connection if MQTT is enabled
delay(2000); if (!mqttConfig.enabled) {
LOG_DEBUG("Network connected but MQTT is disabled - skipping MQTT connection");
return;
}
LOG_DEBUG("Network stable - connecting to MQTT"); LOG_DEBUG("Network connected - scheduling MQTT connection after 2s stabilization (non-blocking)");
// Reset reconnect attempts on fresh network connection
_reconnectAttempts = 0;
// 🔥 CRITICAL FIX: Use non-blocking timer instead of delay()
// This prevents blocking UDP discovery, WebSocket connections, and async operations
if (_networkStabilizationTimer) {
xTimerStart(_networkStabilizationTimer, 0);
} else {
LOG_ERROR("Network stabilization timer not initialized!");
// Fallback to immediate connection (better than blocking)
connect(); connect();
}
} }
void MQTTAsyncClient::onNetworkDisconnected() { void MQTTAsyncClient::onNetworkDisconnected() {
@@ -167,6 +212,9 @@ void MQTTAsyncClient::onMqttConnect(bool sessionPresent) {
LOG_INFO("✅ Connected to MQTT broker (session present: %s)", sessionPresent ? "yes" : "no"); LOG_INFO("✅ Connected to MQTT broker (session present: %s)", sessionPresent ? "yes" : "no");
LOG_INFO("🔍 Free heap AFTER MQTT connect: %d bytes", ESP.getFreeHeap()); LOG_INFO("🔍 Free heap AFTER MQTT connect: %d bytes", ESP.getFreeHeap());
// Reset reconnection attempts on successful connection
_reconnectAttempts = 0;
// Subscribe to control topic // Subscribe to control topic
subscribe(); subscribe();
@@ -175,6 +223,8 @@ void MQTTAsyncClient::onMqttConnect(bool sessionPresent) {
} }
void MQTTAsyncClient::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) { void MQTTAsyncClient::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
auto& mqttConfig = _configManager.getMqttConfig();
const char* reasonStr; const char* reasonStr;
switch(reason) { switch(reason) {
case AsyncMqttClientDisconnectReason::TCP_DISCONNECTED: case AsyncMqttClientDisconnectReason::TCP_DISCONNECTED:
@@ -205,8 +255,24 @@ void MQTTAsyncClient::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
// Stop heartbeat timer when disconnected // Stop heartbeat timer when disconnected
stopHeartbeat(); stopHeartbeat();
// 🔥 Don't attempt reconnection if MQTT is disabled
if (!mqttConfig.enabled) {
LOG_INFO("MQTT is disabled - not attempting reconnection");
return;
}
if (_networking.isConnected()) { if (_networking.isConnected()) {
LOG_INFO("Network still connected - scheduling MQTT reconnection in %d seconds", MQTT_RECONNECT_DELAY / 1000); // Increment reconnection attempts
_reconnectAttempts++;
// Calculate backoff delay
unsigned long reconnectDelay = getReconnectDelay();
LOG_INFO("Network still connected - scheduling MQTT reconnection #%d in %lu seconds (backoff active)",
_reconnectAttempts, reconnectDelay / 1000);
// Update timer period with new delay
xTimerChangePeriod(_mqttReconnectTimer, pdMS_TO_TICKS(reconnectDelay), 0);
xTimerStart(_mqttReconnectTimer, 0); xTimerStart(_mqttReconnectTimer, 0);
} else { } else {
LOG_INFO("Network is down - waiting for network to reconnect"); LOG_INFO("Network is down - waiting for network to reconnect");
@@ -343,3 +409,42 @@ void MQTTAsyncClient::heartbeatTimerCallback(TimerHandle_t xTimer) {
MQTTAsyncClient::_instance->publishHeartbeat(); MQTTAsyncClient::_instance->publishHeartbeat();
} }
} }
// ═══════════════════════════════════════════════════════════════════════════════════
// NETWORK STABILIZATION - NON-BLOCKING TIMER APPROACH
// ═══════════════════════════════════════════════════════════════════════════════════
void MQTTAsyncClient::connectAfterStabilization() {
LOG_DEBUG("Network stabilization complete - connecting to MQTT");
connect();
}
void MQTTAsyncClient::networkStabilizationTimerCallback(TimerHandle_t xTimer) {
if (MQTTAsyncClient::_instance) {
MQTTAsyncClient::_instance->connectAfterStabilization();
}
}
// ═══════════════════════════════════════════════════════════════════════════════════
// EXPONENTIAL BACKOFF CALCULATION
// ═══════════════════════════════════════════════════════════════════════════════════
unsigned long MQTTAsyncClient::getReconnectDelay() {
// First 3 attempts: Quick retries (5 seconds each)
if (_reconnectAttempts <= MQTT_MAX_QUICK_RETRIES) {
return MQTT_RECONNECT_BASE_DELAY;
}
// After quick retries: Exponential backoff
// Formula: base_delay * 2^(attempts - quick_retries)
// Examples: 10s, 20s, 40s, 80s, 160s, 300s (capped at 5 minutes)
uint8_t backoffPower = _reconnectAttempts - MQTT_MAX_QUICK_RETRIES;
unsigned long delay = MQTT_RECONNECT_BASE_DELAY * (1 << backoffPower); // 2^backoffPower
// Cap at maximum delay (5 minutes)
if (delay > MQTT_RECONNECT_MAX_DELAY) {
delay = MQTT_RECONNECT_MAX_DELAY;
}
return delay;
}

View File

@@ -108,11 +108,22 @@ private:
void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total); void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total);
void onMqttPublish(uint16_t packetId); void onMqttPublish(uint16_t packetId);
// Reconnection Timer // Reconnection Timer with Exponential Backoff
TimerHandle_t _mqttReconnectTimer; TimerHandle_t _mqttReconnectTimer;
static const unsigned long MQTT_RECONNECT_DELAY = 5000; // 5 seconds static const unsigned long MQTT_RECONNECT_BASE_DELAY = 5000; // 5 seconds base
static const unsigned long MQTT_RECONNECT_MAX_DELAY = 300000; // 5 minutes max
static const uint8_t MQTT_MAX_QUICK_RETRIES = 3; // Try 3 times quickly
uint8_t _reconnectAttempts; // Track failed attempts
unsigned long _lastConnectionAttempt; // Track last attempt time
void attemptReconnection(); void attemptReconnection();
static void mqttReconnectTimerCallback(TimerHandle_t xTimer); static void mqttReconnectTimerCallback(TimerHandle_t xTimer);
unsigned long getReconnectDelay(); // Calculate backoff delay
// Network Stabilization Timer (non-blocking replacement for delay)
TimerHandle_t _networkStabilizationTimer;
static const unsigned long NETWORK_STABILIZATION_DELAY = 2000; // 2 seconds
void connectAfterStabilization();
static void networkStabilizationTimerCallback(TimerHandle_t xTimer);
// Heartbeat Timer (30 seconds) // Heartbeat Timer (30 seconds)
TimerHandle_t _heartbeatTimer; TimerHandle_t _heartbeatTimer;

View File

@@ -1163,6 +1163,16 @@ bool ConfigManager::setSdLogLevel(uint8_t level) {
return true; return true;
} }
bool ConfigManager::setMqttLogLevel(uint8_t level) {
if (level > 5) { // Max level is VERBOSE (5)
LOG_WARNING("ConfigManager - ⚠️ Invalid MQTT log level %d, valid range is 0-5", level);
return false;
}
generalConfig.mqttLogLevel = level;
LOG_DEBUG("ConfigManager - MQTT log level set to %d", level);
return true;
}
bool ConfigManager::loadGeneralConfig() { bool ConfigManager::loadGeneralConfig() {
if (!ensureSDCard()) return false; if (!ensureSDCard()) return false;
@@ -1187,9 +1197,17 @@ bool ConfigManager::loadGeneralConfig() {
if (doc.containsKey("sdLogLevel")) { if (doc.containsKey("sdLogLevel")) {
generalConfig.sdLogLevel = doc["sdLogLevel"].as<uint8_t>(); generalConfig.sdLogLevel = doc["sdLogLevel"].as<uint8_t>();
} }
if (doc.containsKey("mqttLogLevel")) {
generalConfig.mqttLogLevel = doc["mqttLogLevel"].as<uint8_t>();
}
if (doc.containsKey("mqttEnabled")) {
generalConfig.mqttEnabled = doc["mqttEnabled"].as<bool>();
mqttConfig.enabled = generalConfig.mqttEnabled; // Sync with mqttConfig
}
LOG_DEBUG("ConfigManager - General config loaded - Serial log level: %d, SD log level: %d", LOG_DEBUG("ConfigManager - General config loaded - Serial log level: %d, SD log level: %d, MQTT log level: %d, MQTT enabled: %s",
generalConfig.serialLogLevel, generalConfig.sdLogLevel); generalConfig.serialLogLevel, generalConfig.sdLogLevel, generalConfig.mqttLogLevel,
generalConfig.mqttEnabled ? "true" : "false");
return true; return true;
} }
@@ -1199,6 +1217,8 @@ bool ConfigManager::saveGeneralConfig() {
StaticJsonDocument<256> doc; StaticJsonDocument<256> doc;
doc["serialLogLevel"] = generalConfig.serialLogLevel; doc["serialLogLevel"] = generalConfig.serialLogLevel;
doc["sdLogLevel"] = generalConfig.sdLogLevel; doc["sdLogLevel"] = generalConfig.sdLogLevel;
doc["mqttLogLevel"] = generalConfig.mqttLogLevel;
doc["mqttEnabled"] = generalConfig.mqttEnabled;
char buffer[256]; char buffer[256];
size_t len = serializeJson(doc, buffer, sizeof(buffer)); size_t len = serializeJson(doc, buffer, sizeof(buffer));
@@ -1209,6 +1229,6 @@ bool ConfigManager::saveGeneralConfig() {
} }
saveFileToSD("/settings", "generalConfig.json", buffer); saveFileToSD("/settings", "generalConfig.json", buffer);
LOG_DEBUG("ConfigManager - General config saved"); LOG_DEBUG("ConfigManager - General config saved (MQTT enabled: %s)", generalConfig.mqttEnabled ? "true" : "false");
return true; return true;
} }

View File

@@ -79,11 +79,12 @@ public:
* Username defaults to deviceUID for unique identification. * Username defaults to deviceUID for unique identification.
*/ */
struct MqttConfig { struct MqttConfig {
IPAddress host = IPAddress(72,61,191,197); // 📡 Local Mosquitto broker IPAddress host = IPAddress(72,61,191,197); // 📡 MQTT broker (default cloud broker)
int port = 1883; // 🔌 Standard MQTT port (non-SSL) int port = 1883; // 🔌 Standard MQTT port (non-SSL)
String user; // 👤 Auto-set to deviceUID String user; // 👤 Auto-set to deviceUID
String password = "vesper"; // 🔑 Default password String password = "vesper"; // 🔑 Default password
bool useSSL = false; // 🔒 SSL disabled for local broker bool useSSL = false; // 🔒 SSL disabled for local broker
bool enabled = true; // 🔘 MQTT enabled by default (can be toggled via command)
}; };
/** /**
@@ -213,6 +214,8 @@ public:
struct GeneralConfig { struct GeneralConfig {
uint8_t serialLogLevel = 5; uint8_t serialLogLevel = 5;
uint8_t sdLogLevel = 0; uint8_t sdLogLevel = 0;
uint8_t mqttLogLevel = 0;
bool mqttEnabled = true; // MQTT enabled by default
}; };
private: private:
@@ -398,6 +401,10 @@ public:
// General Config methods // General Config methods
bool setSerialLogLevel(uint8_t level); bool setSerialLogLevel(uint8_t level);
bool setSdLogLevel(uint8_t level); bool setSdLogLevel(uint8_t level);
bool setMqttLogLevel(uint8_t level);
uint8_t getMqttLogLevel() const { return generalConfig.mqttLogLevel; }
void setMqttEnabled(bool enabled) { generalConfig.mqttEnabled = enabled; mqttConfig.enabled = enabled; }
bool getMqttEnabled() const { return generalConfig.mqttEnabled; }
bool loadGeneralConfig(); bool loadGeneralConfig();
bool saveGeneralConfig(); bool saveGeneralConfig();

View File

@@ -58,12 +58,30 @@ bool FileManager::ensureDirectoryExists(const String& dirPath) {
bool FileManager::downloadFile(const String& url, const String& directory, const String& filename) { bool FileManager::downloadFile(const String& url, const String& directory, const String& filename) {
LOG_INFO("Starting download from: %s", url.c_str()); LOG_INFO("Starting download from: %s", url.c_str());
// Check if URL is HTTPS
bool isHttps = url.startsWith("https://");
HTTPClient http; HTTPClient http;
// Configure HTTP client based on protocol
if (isHttps) {
WiFiClientSecure* secureClient = new WiFiClientSecure();
secureClient->setInsecure(); // Skip certificate validation for Firebase
http.begin(*secureClient, url);
LOG_DEBUG("Using HTTPS with secure client");
} else {
http.begin(url); http.begin(url);
LOG_DEBUG("Using HTTP");
}
http.setTimeout(30000); // 30 second timeout for large files
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS); // Follow redirects automatically
LOG_DEBUG("Sending HTTP GET request...");
int httpCode = http.GET(); int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) { if (httpCode != HTTP_CODE_OK && httpCode != HTTP_CODE_MOVED_PERMANENTLY && httpCode != HTTP_CODE_FOUND) {
LOG_ERROR("HTTP GET failed, error: %s", http.errorToString(httpCode).c_str()); LOG_ERROR("HTTP GET failed, code: %d, error: %s", httpCode, http.errorToString(httpCode).c_str());
http.end(); http.end();
return false; return false;
} }
@@ -92,17 +110,59 @@ bool FileManager::downloadFile(const String& url, const String& directory, const
return false; return false;
} }
// Get stream and file size
WiFiClient* stream = http.getStreamPtr(); WiFiClient* stream = http.getStreamPtr();
uint8_t buffer[1024]; int contentLength = http.getSize();
int bytesRead; LOG_DEBUG("Content length: %d bytes", contentLength);
while (http.connected() && (bytesRead = stream->readBytes(buffer, sizeof(buffer))) > 0) { uint8_t buffer[512]; // Smaller buffer for better responsiveness
size_t totalBytes = 0;
unsigned long lastYield = millis();
unsigned long lastLog = millis();
// Download with aggressive watchdog feeding
while (http.connected() && (contentLength <= 0 || totalBytes < contentLength)) {
// Check available data
size_t availableSize = stream->available();
if (availableSize) {
// Read available data (up to buffer size)
size_t readSize = availableSize > sizeof(buffer) ? sizeof(buffer) : availableSize;
int bytesRead = stream->readBytes(buffer, readSize);
if (bytesRead > 0) {
file.write(buffer, bytesRead); file.write(buffer, bytesRead);
totalBytes += bytesRead;
// Log progress every 5KB
if (millis() - lastLog > 5000) {
LOG_DEBUG("Download progress: %u bytes", totalBytes);
lastLog = millis();
}
}
}
// Aggressive task yielding every 100ms to prevent watchdog timeout
if (millis() - lastYield > 100) {
yield();
vTaskDelay(1 / portTICK_PERIOD_MS); // Let other tasks run
lastYield = millis();
}
// Exit if no data and connection closed
if (!availableSize && !http.connected()) {
break;
}
// Small delay if no data available yet
if (!availableSize) {
delay(10);
}
} }
file.close(); file.close();
http.end(); http.end();
LOG_INFO("Download complete, file saved to: %s", fullPath.c_str()); LOG_INFO("Download complete, file saved to: %s (%u bytes)", fullPath.c_str(), totalBytes);
return true; return true;
} }

View File

@@ -22,6 +22,7 @@
#include <SD.h> #include <SD.h>
#include <HTTPClient.h> #include <HTTPClient.h>
#include <WiFiClient.h> #include <WiFiClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "../Logging/Logging.hpp" #include "../Logging/Logging.hpp"
#include "../ConfigManager/ConfigManager.hpp" #include "../ConfigManager/ConfigManager.hpp"

View File

@@ -1,7 +1,10 @@
#include "Logging.hpp" #include "Logging.hpp"
// Initialize static member // Initialize static members
Logging::LogLevel Logging::currentLevel = Logging::VERBOSE; // Default to DEBUG Logging::LogLevel Logging::currentLevel = Logging::VERBOSE; // Default to VERBOSE
Logging::LogLevel Logging::mqttLogLevel = Logging::NONE; // Default MQTT logs OFF
Logging::MqttPublishCallback Logging::mqttPublishCallback = nullptr;
String Logging::mqttLogTopic = "";
void Logging::setLevel(LogLevel level) { void Logging::setLevel(LogLevel level) {
currentLevel = level; currentLevel = level;
@@ -12,6 +15,21 @@ Logging::LogLevel Logging::getLevel() {
return currentLevel; return currentLevel;
} }
void Logging::setMqttLogLevel(LogLevel level) {
mqttLogLevel = level;
Serial.printf("[LOGGING] MQTT log level set to %d\n", level);
}
Logging::LogLevel Logging::getMqttLogLevel() {
return mqttLogLevel;
}
void Logging::setMqttPublishCallback(MqttPublishCallback callback, const String& logTopic) {
mqttPublishCallback = callback;
mqttLogTopic = logTopic;
Serial.printf("[LOGGING] MQTT publish callback registered for topic: %s\n", logTopic.c_str());
}
bool Logging::isLevelEnabled(LogLevel level) { bool Logging::isLevelEnabled(LogLevel level) {
return currentLevel >= level; return currentLevel >= level;
} }
@@ -62,11 +80,59 @@ void Logging::verbose(const char* format, ...) {
} }
void Logging::log(LogLevel level, const char* levelStr, const char* format, va_list args) { void Logging::log(LogLevel level, const char* levelStr, const char* format, va_list args) {
Serial.printf("[%s] ", levelStr);
// Print the formatted message // Print the formatted message
char buffer[512]; char buffer[512];
vsnprintf(buffer, sizeof(buffer), format, args); vsnprintf(buffer, sizeof(buffer), format, args);
// Serial output
Serial.printf("[%s] ", levelStr);
Serial.print(buffer); Serial.print(buffer);
Serial.println(); Serial.println();
// MQTT output (if enabled and callback is set)
if (mqttLogLevel >= level && mqttPublishCallback) {
publishToMqtt(level, levelStr, buffer);
}
}
void Logging::publishToMqtt(LogLevel level, const char* levelStr, const char* message) {
if (!mqttPublishCallback || mqttLogTopic.isEmpty()) {
return;
}
// CRITICAL: Prevent infinite recursion if MQTT publish fails
// Temporarily disable MQTT logging during publish to avoid cascading errors
static bool isPublishing = false;
if (isPublishing) {
return; // Already publishing, don't create recursive log loop
}
isPublishing = true;
// Build JSON manually to minimize stack usage (no StaticJsonDocument)
// Format: {"level":"🟢 INFO","message":"text","timestamp":12345}
String payload;
payload.reserve(600); // Pre-allocate to avoid fragmentation
payload = "{\"level\":\"";
payload += levelStr;
payload += "\",\"message\":\"";
// Escape special JSON characters in message
String escapedMsg = message;
escapedMsg.replace("\\", "\\\\");
escapedMsg.replace("\"", "\\\"");
escapedMsg.replace("\n", "\\n");
escapedMsg.replace("\r", "\\r");
payload += escapedMsg;
payload += "\",\"timestamp\":";
payload += millis();
payload += "}";
// Publish with QoS 1 (guaranteed delivery)
// Note: If this fails, it won't trigger another MQTT log due to isPublishing flag
mqttPublishCallback(mqttLogTopic, payload, 1);
isPublishing = false;
} }

View File

@@ -19,6 +19,9 @@
#include <Arduino.h> #include <Arduino.h>
// Forward declaration
class MQTTAsyncClient;
class Logging { class Logging {
public: public:
// Log Levels // Log Levels
@@ -31,8 +34,14 @@ public:
VERBOSE = 5 // Nearly every command gets printed VERBOSE = 5 // Nearly every command gets printed
}; };
// MQTT Log Publishing Callback
using MqttPublishCallback = std::function<void(const String& topic, const String& payload, int qos)>;
private: private:
static LogLevel currentLevel; static LogLevel currentLevel;
static LogLevel mqttLogLevel;
static MqttPublishCallback mqttPublishCallback;
static String mqttLogTopic;
public: public:
// Set the active log level // Set the active log level
@@ -41,6 +50,15 @@ public:
// Get current log level // Get current log level
static LogLevel getLevel(); static LogLevel getLevel();
// Set MQTT log level (independent from serial logging)
static void setMqttLogLevel(LogLevel level);
// Get MQTT log level
static LogLevel getMqttLogLevel();
// Set MQTT callback for publishing logs
static void setMqttPublishCallback(MqttPublishCallback callback, const String& logTopic);
// Logging functions // Logging functions
static void error(const char* format, ...); static void error(const char* format, ...);
static void warning(const char* format, ...); static void warning(const char* format, ...);
@@ -53,6 +71,7 @@ public:
private: private:
static void log(LogLevel level, const char* levelStr, const char* format, va_list args); static void log(LogLevel level, const char* levelStr, const char* format, va_list args);
static void publishToMqtt(LogLevel level, const char* levelStr, const char* message);
}; };
// Convenience macros for easier use // Convenience macros for easier use

View File

@@ -21,7 +21,8 @@ OTAManager::OTAManager(ConfigManager& configManager)
, _isEmergency(false) , _isEmergency(false)
, _progressCallback(nullptr) , _progressCallback(nullptr)
, _statusCallback(nullptr) , _statusCallback(nullptr)
, _scheduledCheckTimer(NULL) { , _scheduledCheckTimer(NULL)
, _initialCheckTimer(NULL) {
} }
OTAManager::~OTAManager() { OTAManager::~OTAManager() {
@@ -30,6 +31,11 @@ OTAManager::~OTAManager() {
xTimerDelete(_scheduledCheckTimer, portMAX_DELAY); xTimerDelete(_scheduledCheckTimer, portMAX_DELAY);
_scheduledCheckTimer = NULL; _scheduledCheckTimer = NULL;
} }
if (_initialCheckTimer != NULL) {
xTimerStop(_initialCheckTimer, 0);
xTimerDelete(_initialCheckTimer, portMAX_DELAY);
_initialCheckTimer = NULL;
}
} }
void OTAManager::begin() { void OTAManager::begin() {
@@ -51,6 +57,23 @@ void OTAManager::begin() {
} else { } else {
LOG_ERROR("Failed to create OTA scheduled check timer!"); LOG_ERROR("Failed to create OTA scheduled check timer!");
} }
// 🔥 NEW: Create one-shot timer for initial boot check (5 seconds after boot)
// This prevents blocking during critical connection phase
_initialCheckTimer = xTimerCreate(
"OTA_InitCheck",
pdMS_TO_TICKS(5000), // 5 seconds delay
pdFALSE, // One-shot timer
this, // Timer ID (pass OTAManager instance)
initialCheckCallback
);
if (_initialCheckTimer != NULL) {
xTimerStart(_initialCheckTimer, 0);
LOG_INFO("OTA initial check scheduled for 5 seconds after boot (non-blocking)");
} else {
LOG_ERROR("Failed to create OTA initial check timer!");
}
} }
void OTAManager::setFileManager(FileManager* fm) { void OTAManager::setFileManager(FileManager* fm) {
@@ -61,6 +84,21 @@ void OTAManager::setPlayer(Player* player) {
_player = player; _player = player;
} }
// ✅ NEW: Static timer callback for initial boot check
void OTAManager::initialCheckCallback(TimerHandle_t xTimer) {
OTAManager* ota = static_cast<OTAManager*>(pvTimerGetTimerID(xTimer));
if (ota) {
LOG_INFO("🚀 Running initial OTA check (non-blocking, async)");
ota->performInitialCheck();
}
}
// ✅ NEW: Perform initial OTA check (async, non-blocking)
void OTAManager::performInitialCheck() {
// This runs asynchronously, won't block WebSocket/UDP/MQTT
checkForUpdates();
}
// ✅ NEW: Static timer callback for scheduled checks // ✅ NEW: Static timer callback for scheduled checks
void OTAManager::scheduledCheckCallback(TimerHandle_t xTimer) { void OTAManager::scheduledCheckCallback(TimerHandle_t xTimer) {
OTAManager* ota = static_cast<OTAManager*>(pvTimerGetTimerID(xTimer)); OTAManager* ota = static_cast<OTAManager*>(pvTimerGetTimerID(xTimer));

View File

@@ -122,6 +122,11 @@ private:
TimerHandle_t _scheduledCheckTimer; TimerHandle_t _scheduledCheckTimer;
static void scheduledCheckCallback(TimerHandle_t xTimer); static void scheduledCheckCallback(TimerHandle_t xTimer);
// Initial boot check timer (non-blocking delayed check)
TimerHandle_t _initialCheckTimer;
static void initialCheckCallback(TimerHandle_t xTimer);
void performInitialCheck(); // Async initial check after boot
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();

View File

@@ -6,7 +6,7 @@ void Telemetry::begin() {
for (uint8_t i = 0; i < 16; i++) { for (uint8_t i = 0; i < 16; i++) {
strikeCounters[i] = 0; strikeCounters[i] = 0;
bellLoad[i] = 0; bellLoad[i] = 0;
bellMaxLoad[i] = 60; // Default max load bellMaxLoad[i] = 200; // Default max load
} }
coolingActive = false; coolingActive = false;

View File

@@ -135,16 +135,16 @@ unsigned long Timekeeper::getTime() {
void Timekeeper::syncTimeWithNTP() { void Timekeeper::syncTimeWithNTP() {
// Check if we have network connection and required dependencies // Check if we have network connection and required dependencies
if (!_networking || !_configManager) { if (!_networking || !_configManager) {
LOG_ERROR("Cannot sync time: Networking or ConfigManager not set"); LOG_WARNING("Cannot sync time: Networking or ConfigManager not set - using RTC time");
return; return;
} }
if (!_networking->isConnected()) { if (!_networking->isConnected()) {
LOG_WARNING("Cannot sync time: No network connection"); LOG_INFO("No network connection - skipping NTP sync, using RTC time");
return; return;
} }
LOG_INFO("Syncing time with NTP server..."); LOG_INFO("⏰ Starting non-blocking NTP sync...");
// Get config from ConfigManager // Get config from ConfigManager
auto& timeConfig = _configManager->getTimeConfig(); auto& timeConfig = _configManager->getTimeConfig();
@@ -152,30 +152,23 @@ void Timekeeper::syncTimeWithNTP() {
// Configure NTP with settings from config // Configure NTP with settings from config
configTime(timeConfig.gmtOffsetSec, timeConfig.daylightOffsetSec, timeConfig.ntpServer.c_str()); configTime(timeConfig.gmtOffsetSec, timeConfig.daylightOffsetSec, timeConfig.ntpServer.c_str());
// Wait for time sync with timeout // 🔥 NON-BLOCKING: Try to get time immediately without waiting
struct tm timeInfo; struct tm timeInfo;
int attempts = 0; if (getLocalTime(&timeInfo, 100)) { // 100ms timeout instead of blocking
while (!getLocalTime(&timeInfo) && attempts < 10) { // Success! Update RTC with synchronized time
LOG_DEBUG("Waiting for NTP sync... attempt %d", attempts + 1);
delay(1000);
attempts++;
}
if (attempts >= 10) {
LOG_ERROR("Failed to obtain time from NTP server after 10 attempts");
return;
}
// Update RTC with synchronized time
rtc.adjust(DateTime(timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday, rtc.adjust(DateTime(timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec)); timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec));
LOG_INFO("Time synced successfully: %04d-%02d-%02d %02d:%02d:%02d", LOG_INFO("✅ NTP sync successful: %04d-%02d-%02d %02d:%02d:%02d",
timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday, timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec); timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
// Reload today's events since the time might have changed significantly // Reload today's events since the time might have changed significantly
loadTodaysEvents(); loadTodaysEvents();
} else {
// No internet or NTP server unreachable - this is NORMAL for local networks
LOG_INFO("⚠️ NTP sync skipped (no internet) - using RTC time. This is normal for local networks.");
}
} }
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════

View File

@@ -62,7 +62,7 @@
* 👨‍💻 AUTHOR: BellSystems bonamin * 👨‍💻 AUTHOR: BellSystems bonamin
*/ */
#define FW_VERSION "131" #define FW_VERSION "137"
/* /*
@@ -72,6 +72,7 @@
* v0.1 (100) - Vesper Launch Beta * v0.1 (100) - Vesper Launch Beta
* v1.2 (120) - Added Log Level Configuration via App/MQTT * v1.2 (120) - Added Log Level Configuration via App/MQTT
* v1.3 (130) - Added Telemetry Reports to App, Various Playback Fixes * v1.3 (130) - Added Telemetry Reports to App, Various Playback Fixes
* v137 - Made OTA and MQTT delays Async
* ═══════════════════════════════════════════════════════════════════════════════ * ═══════════════════════════════════════════════════════════════════════════════
* NOTE: Versions are now stored as integers (v1.3 = 130) * NOTE: Versions are now stored as integers (v1.3 = 130)
* ═══════════════════════════════════════════════════════════════════════════════ * ═══════════════════════════════════════════════════════════════════════════════
@@ -196,7 +197,8 @@ void setup()
{ {
// Initialize Serial Communications (for debugging) & I2C Bus (for Hardware Control) // Initialize Serial Communications (for debugging) & I2C Bus (for Hardware Control)
Serial.begin(115200); Serial.begin(115200);
Serial.println("Hello, VESPER System Initialized! - PontikoTest"); Serial.print("VESPER System Booting UP! - Version ");
Serial.println(FW_VERSION);
Wire.begin(4,15); Wire.begin(4,15);
auto& hwConfig = configManager.getHardwareConfig(); auto& hwConfig = configManager.getHardwareConfig();
SPI.begin(hwConfig.ethSpiSck, hwConfig.ethSpiMiso, hwConfig.ethSpiMosi); SPI.begin(hwConfig.ethSpiSck, hwConfig.ethSpiMiso, hwConfig.ethSpiMosi);
@@ -340,53 +342,55 @@ void setup()
// 🔔 CONNECT BELLENGINE TO COMMUNICATION FOR DING NOTIFICATIONS! // 🔔 CONNECT BELLENGINE TO COMMUNICATION FOR DING NOTIFICATIONS!
bellEngine.setCommunicationManager(&communication); bellEngine.setCommunicationManager(&communication);
// Track if AsyncWebServer has been started to prevent duplicates
static bool webServerStarted = false;
// Set up network callbacks // Set up network callbacks
networking.setNetworkCallbacks( networking.setNetworkCallbacks(
[]() { [&webServerStarted]() {
communication.onNetworkConnected(); communication.onNetworkConnected();
// Sync time with NTP server when network becomes available
LOG_INFO("⏰ Syncing time with NTP server..."); // Non-blocking NTP sync (graceful without internet)
timekeeper.syncTimeWithNTP(); timekeeper.syncTimeWithNTP();
// Start AsyncWebServer when network becomes available
if (networking.getState() != NetworkState::WIFI_PORTAL_MODE) { // Start AsyncWebServer when network becomes available (only once!)
if (!webServerStarted && networking.getState() != NetworkState::WIFI_PORTAL_MODE) {
LOG_INFO("🚀 Starting AsyncWebServer on port 80..."); LOG_INFO("🚀 Starting AsyncWebServer on port 80...");
server.begin(); server.begin();
LOG_INFO("✅ AsyncWebServer started on http://%s", networking.getLocalIP().c_str()); LOG_INFO("✅ AsyncWebServer started on http://%s", networking.getLocalIP().c_str());
webServerStarted = true;
} }
}, // onConnected }, // onConnected
[]() { communication.onNetworkDisconnected(); } // onDisconnected []() { communication.onNetworkDisconnected(); } // onDisconnected
); );
// If already connected, trigger MQTT connection manually // If already connected, trigger MQTT connection and setup manually
if (networking.isConnected()) { if (networking.isConnected()) {
LOG_INFO("Network already connected - triggering MQTT connection"); LOG_INFO("Network already connected - initializing services");
communication.onNetworkConnected(); communication.onNetworkConnected();
// Sync time with NTP server if network is already connected // Non-blocking NTP sync (graceful without internet)
LOG_INFO("⏰ Syncing time with NTP server...");
timekeeper.syncTimeWithNTP(); timekeeper.syncTimeWithNTP();
// 🔥 CRITICAL: Start AsyncWebServer ONLY when network is ready // 🔥 CRITICAL: Start AsyncWebServer ONLY when network is ready
// Do NOT start if WiFiManager portal is active (port 80 conflict!) // Do NOT start if WiFiManager portal is active (port 80 conflict!)
if (!webServerStarted && networking.getState() != NetworkState::WIFI_PORTAL_MODE) {
LOG_INFO("🚀 Starting AsyncWebServer on port 80..."); LOG_INFO("🚀 Starting AsyncWebServer on port 80...");
server.begin(); server.begin();
LOG_INFO("✅ AsyncWebServer started and listening on http://%s", networking.getLocalIP().c_str()); LOG_INFO("✅ AsyncWebServer started on http://%s", networking.getLocalIP().c_str());
webServerStarted = true;
}
} else { } else {
LOG_WARNING("⚠️ Network not ready - AsyncWebServer will start after connection"); LOG_WARNING("⚠️ Network not ready - services will start after connection");
} }
delay(500); // Initialize OTA Manager
// 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 otaManager.setPlayer(&player); // Set player reference for idle check
// 🔥 CRITICAL: Delay OTA check to avoid UDP socket race with MQTT // 🔥 FIX: OTA check will happen asynchronously via scheduled timer (no blocking delay)
// Both MQTT and OTA HTTP use UDP sockets, must sequence them! // UDP discovery setup can happen immediately without conflicts
delay(2000);
LOG_INFO("Starting OTA update check after network stabilization...");
otaManager.checkForUpdates();
communication.setupUdpDiscovery(); communication.setupUdpDiscovery();
// Register OTA Manager with health monitor // Register OTA Manager with health monitor
@@ -457,6 +461,14 @@ void loop()
} }
} }
// 🔥 CRITICAL: Clean up dead WebSocket connections every 2 seconds
// This prevents ghost connections from blocking new clients
static unsigned long lastWsCleanup = 0;
if (millis() - lastWsCleanup > 2000) {
ws.cleanupClients();
lastWsCleanup = millis();
}
// 🔥 DEBUG: Log every 10 seconds to verify we're still running // 🔥 DEBUG: Log every 10 seconds to verify we're still running
static unsigned long lastLog = 0; static unsigned long lastLog = 0;
if (millis() - lastLog > 10000) { if (millis() - lastLog > 10000) {