#include "Networking.hpp" #include "../ConfigManager/ConfigManager.hpp" #include "../Logging/Logging.hpp" #include #include // Static instance for callbacks (with safety checks) Networking* Networking::_instance = nullptr; Networking::Networking(ConfigManager& configManager) : _configManager(configManager) , _state(NetworkState::DISCONNECTED) , _activeConnection(ConnectionType::NONE) , _lastConnectionAttempt(0) , _bootStartTime(0) , _bootSequenceComplete(false) , _ethernetCableConnected(false) , _wifiManager(nullptr) , _reconnectionTimer(nullptr) { // Safety check for multiple instances if (_instance != nullptr) { LOG_WARNING("Multiple Networking instances detected! Previous instance will be overridden."); } _instance = this; _wifiManager = new WiFiManager(); } Networking::~Networking() { // Clear static instance safely if (_instance == this) { _instance = nullptr; } // Cleanup timer if (_reconnectionTimer) { xTimerDelete(_reconnectionTimer, portMAX_DELAY); _reconnectionTimer = nullptr; } // Cleanup WiFiManager if (_wifiManager) { delete _wifiManager; _wifiManager = nullptr; } } void Networking::begin() { LOG_INFO("Initializing Networking System"); _bootStartTime = millis(); // Create reconnection timer _reconnectionTimer = xTimerCreate("reconnectionTimer", pdMS_TO_TICKS(RECONNECTION_INTERVAL), pdTRUE, (void*)0, reconnectionTimerCallback); // Setup network event handler WiFi.onEvent(networkEventHandler); // Configure WiFiManager _wifiManager->setDebugOutput(false); _wifiManager->setConfigPortalTimeout(180); // 3 minutes // Start Ethernet hardware auto& hwConfig = _configManager.getHardwareConfig(); ETH.begin(hwConfig.ethPhyType, hwConfig.ethPhyAddr, hwConfig.ethPhyCs, hwConfig.ethPhyIrq, hwConfig.ethPhyRst, SPI); // Start connection sequence LOG_INFO("Starting network connection sequence..."); startEthernetConnection(); } void Networking::startEthernetConnection() { LOG_INFO("Attempting Ethernet connection..."); setState(NetworkState::CONNECTING_ETHERNET); // Check if Ethernet hardware initialization failed if (!ETH.linkUp()) { LOG_WARNING("Ethernet hardware not detected or failed to initialize"); LOG_INFO("Falling back to WiFi immediately"); startWiFiConnection(); return; } // Ethernet will auto-connect via events // Set timeout for Ethernet attempt (5 seconds) _lastConnectionAttempt = millis(); // Start reconnection timer to handle timeout xTimerStart(_reconnectionTimer, 0); } void Networking::startWiFiConnection() { LOG_INFO("Attempting WiFi connection..."); setState(NetworkState::CONNECTING_WIFI); if (!hasValidWiFiCredentials()) { LOG_WARNING("No valid WiFi credentials found"); if (shouldStartPortal()) { startWiFiPortal(); } return; } LOG_INFO("Using WiFiManager saved credentials"); WiFi.mode(WIFI_STA); applyNetworkConfig(false); // false = WiFi config // Let WiFiManager handle credentials (uses saved SSID/password) WiFi.begin(); _lastConnectionAttempt = millis(); // Start reconnection timer to handle timeout xTimerStart(_reconnectionTimer, 0); } void Networking::startWiFiPortal() { LOG_INFO("Starting WiFi configuration portal..."); setState(NetworkState::WIFI_PORTAL_MODE); WiFi.mode(WIFI_AP_STA); auto& netConfig = _configManager.getNetworkConfig(); String apName = "Vesper-" + _configManager.getDeviceUID(); LOG_INFO("WiFi Portal: SSID='%s', Password='%s'", apName.c_str(), netConfig.apPass.c_str()); if (_wifiManager->autoConnect(apName.c_str(), netConfig.apPass.c_str())) { LOG_INFO("WiFi configured successfully via portal"); onWiFiConnected(); } else { LOG_ERROR("WiFi portal configuration failed"); setState(NetworkState::DISCONNECTED); // Start reconnection timer to try again xTimerStart(_reconnectionTimer, 0); } } void Networking::handleReconnection() { if (_state == NetworkState::CONNECTED_ETHERNET || _state == NetworkState::CONNECTED_WIFI) { return; // Already connected } LOG_DEBUG("Attempting reconnection..."); // Check for Ethernet timeout (fall back to WiFi) if (_state == NetworkState::CONNECTING_ETHERNET) { unsigned long now = millis(); if (now - _lastConnectionAttempt > 5000) { // 5 second timeout LOG_INFO("Ethernet connection timeout - falling back to WiFi"); startWiFiConnection(); return; } return; // Still waiting for Ethernet } // Check for WiFi timeout (try again) if (_state == NetworkState::CONNECTING_WIFI) { unsigned long now = millis(); if (now - _lastConnectionAttempt > 10000) { // 10 second timeout LOG_INFO("WiFi connection timeout - retrying"); startWiFiConnection(); // Retry WiFi } return; // Still waiting for WiFi } // State is DISCONNECTED - decide what to try if (_ethernetCableConnected) { LOG_INFO("Ethernet cable detected - trying Ethernet"); startEthernetConnection(); } else { LOG_INFO("No Ethernet - trying WiFi"); if (hasValidWiFiCredentials()) { startWiFiConnection(); } else if (shouldStartPortal()) { startWiFiPortal(); } else { LOG_WARNING("No WiFi credentials and boot sequence complete - waiting"); } } } // ════════════════════════════════════════════════════════════════════════════ // HEALTH CHECK IMPLEMENTATION // ════════════════════════════════════════════════════════════════════════════ bool Networking::isHealthy() const { // Check if we have any active connection if (_activeConnection == ConnectionType::NONE) { LOG_DEBUG("Networking: Unhealthy - No active connection"); return false; } // Check connection state if (_state != NetworkState::CONNECTED_ETHERNET && _state != NetworkState::CONNECTED_WIFI) { LOG_DEBUG("Networking: Unhealthy - Not in connected state"); return false; } // Check IP address validity String ip = getLocalIP(); if (ip == "0.0.0.0" || ip.isEmpty()) { LOG_DEBUG("Networking: Unhealthy - Invalid IP address"); return false; } // For WiFi connections, check signal strength if (_activeConnection == ConnectionType::WIFI) { if (WiFi.status() != WL_CONNECTED) { LOG_DEBUG("Networking: Unhealthy - WiFi not connected"); return false; } // Check signal strength (RSSI should be better than -80 dBm) int32_t rssi = WiFi.RSSI(); if (rssi < -80) { LOG_DEBUG("Networking: Unhealthy - Poor WiFi signal: %d dBm", rssi); return false; } } // For Ethernet connections, check link status if (_activeConnection == ConnectionType::ETHERNET) { if (!ETH.linkUp()) { LOG_DEBUG("Networking: Unhealthy - Ethernet link down"); return false; } } return true; } void Networking::setState(NetworkState newState) { if (_state != newState) { LOG_DEBUG("Network state: %d -> %d", (int)_state, (int)newState); _state = newState; } } void Networking::setActiveConnection(ConnectionType type) { if (_activeConnection != type) { LOG_INFO("Active connection changed: %d -> %d", (int)_activeConnection, (int)type); _activeConnection = type; } } void Networking::notifyConnectionChange(bool connected) { if (connected && _onNetworkConnected) { _onNetworkConnected(); } else if (!connected && _onNetworkDisconnected) { _onNetworkDisconnected(); } } // Event handlers void Networking::onEthernetConnected() { LOG_INFO("Ethernet connected successfully"); setState(NetworkState::CONNECTED_ETHERNET); setActiveConnection(ConnectionType::ETHERNET); // Stop WiFi if it was running if (WiFi.getMode() != WIFI_OFF) { WiFi.disconnect(true); WiFi.mode(WIFI_OFF); } // Stop reconnection timer xTimerStop(_reconnectionTimer, 0); notifyConnectionChange(true); } void Networking::onEthernetDisconnected() { LOG_WARNING("Ethernet disconnected"); if (_activeConnection == ConnectionType::ETHERNET) { setState(NetworkState::DISCONNECTED); setActiveConnection(ConnectionType::NONE); notifyConnectionChange(false); // Start reconnection attempts xTimerStart(_reconnectionTimer, 0); } } void Networking::onWiFiConnected() { LOG_INFO("WiFi connected successfully - IP: %s", WiFi.localIP().toString().c_str()); setState(NetworkState::CONNECTED_WIFI); setActiveConnection(ConnectionType::WIFI); // Stop reconnection timer xTimerStop(_reconnectionTimer, 0); // Mark boot sequence as complete _bootSequenceComplete = true; notifyConnectionChange(true); } void Networking::onWiFiDisconnected() { LOG_WARNING("WiFi disconnected"); if (_activeConnection == ConnectionType::WIFI) { setState(NetworkState::DISCONNECTED); setActiveConnection(ConnectionType::NONE); notifyConnectionChange(false); // Start reconnection attempts xTimerStart(_reconnectionTimer, 0); } } void Networking::onEthernetCableChange(bool connected) { _ethernetCableConnected = connected; LOG_INFO("Ethernet cable %s", connected ? "connected" : "disconnected"); if (connected && _activeConnection != ConnectionType::ETHERNET) { // Cable connected and we're not using Ethernet - try to connect startEthernetConnection(); } } // Utility methods void Networking::applyNetworkConfig(bool ethernet) { auto& netConfig = _configManager.getNetworkConfig(); if (netConfig.useStaticIP) { LOG_INFO("Applying static IP configuration"); if (ethernet) { ETH.config(netConfig.ip, netConfig.gateway, netConfig.subnet, netConfig.dns1, netConfig.dns2); } else { WiFi.config(netConfig.ip, netConfig.gateway, netConfig.subnet, netConfig.dns1, netConfig.dns2); } } else { LOG_INFO("Using DHCP configuration"); } if (ethernet) { ETH.setHostname(netConfig.hostname.c_str()); } else { WiFi.setHostname(netConfig.hostname.c_str()); } } bool Networking::hasValidWiFiCredentials() { // Check if WiFiManager has saved credentials return WiFi.SSID().length() > 0; } 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 bool Networking::isConnected() const { return _activeConnection != ConnectionType::NONE; } String Networking::getLocalIP() const { switch (_activeConnection) { case ConnectionType::ETHERNET: return ETH.localIP().toString(); case ConnectionType::WIFI: return WiFi.localIP().toString(); default: return "0.0.0.0"; } } void Networking::forceReconnect() { LOG_INFO("Forcing reconnection..."); setState(NetworkState::RECONNECTING); setActiveConnection(ConnectionType::NONE); // Disconnect everything if (WiFi.getMode() != WIFI_OFF) { WiFi.disconnect(true); WiFi.mode(WIFI_OFF); } // Restart connection sequence delay(1000); startEthernetConnection(); } // Static callbacks void Networking::networkEventHandler(arduino_event_id_t event, arduino_event_info_t info) { if (!_instance) return; LOG_DEBUG("Network event: %d", event); switch (event) { case ARDUINO_EVENT_ETH_START: LOG_DEBUG("ETH Started"); break; case ARDUINO_EVENT_ETH_CONNECTED: LOG_DEBUG("ETH Cable Connected"); _instance->onEthernetCableChange(true); break; case ARDUINO_EVENT_ETH_GOT_IP: LOG_INFO("ETH Got IP: %s", ETH.localIP().toString().c_str()); _instance->applyNetworkConfig(true); _instance->onEthernetConnected(); break; case ARDUINO_EVENT_ETH_DISCONNECTED: LOG_WARNING("ETH Cable Disconnected"); _instance->onEthernetCableChange(false); _instance->onEthernetDisconnected(); break; case ARDUINO_EVENT_ETH_STOP: LOG_INFO("ETH Stopped"); _instance->onEthernetDisconnected(); break; case ARDUINO_EVENT_WIFI_STA_GOT_IP: LOG_INFO("WiFi Got IP: %s", WiFi.localIP().toString().c_str()); _instance->onWiFiConnected(); break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: LOG_WARNING("WiFi Disconnected"); _instance->onWiFiDisconnected(); break; case ARDUINO_EVENT_WIFI_STA_CONNECTED: LOG_DEBUG("WiFi STA Connected"); break; default: break; } } void Networking::reconnectionTimerCallback(TimerHandle_t xTimer) { if (_instance) { _instance->handleReconnection(); // Check if boot sequence should be marked complete if (!_instance->_bootSequenceComplete && (millis() - _instance->_bootStartTime > BOOT_TIMEOUT)) { _instance->_bootSequenceComplete = true; LOG_INFO("Boot sequence timeout - no more portal attempts"); } } }