#include #include #include #include #include #include "ConfigSettings.h" #include "Network.h" #include "Web.h" #include "Sockets.h" #include "Utils.h" #include "SSDP.h" #include "MQTT.h" // ESP-IDF includes for W5500 SPI Ethernet support #include #include #include #include #include #include #include #include extern ConfigSettings settings; extern Web webServer; extern SocketEmitter sockEmit; extern MQTTClass mqtt; extern rebootDelay_t rebootDelay; extern Network net; extern SomfyShadeController somfy; static unsigned long _lastHeapEmit = 0; static bool _apScanning = false; static uint32_t _lastMaxHeap = 0; static uint32_t _lastHeap = 0; int connectRetries = 0; void Network::end() { SSDP.end(); mqtt.end(); sockEmit.end(); delay(100); } bool Network::setup() { WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); WiFi.persistent(false); WiFi.setAutoReconnect(false); WiFi.onEvent(this->networkEvent); this->disconnectTime = millis(); if(WiFi.status() == WL_CONNECTED) WiFi.disconnect(true, true); if(settings.connType == conn_types_t::wifi || settings.connType == conn_types_t::unset) { WiFi.persistent(false); if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname); Serial.print("WiFi Mode: "); Serial.println(WiFi.getMode()); WiFi.mode(WIFI_STA); } sockEmit.begin(); return true; } conn_types_t Network::preferredConnType() { switch(settings.connType) { case conn_types_t::wifi: return settings.WIFI.ssid[0] != '\0' ? conn_types_t::wifi : conn_types_t::ap; case conn_types_t::unset: case conn_types_t::ap: return conn_types_t::ap; case conn_types_t::ethernetpref: { bool linkUp = settings.Ethernet.isSPIController() ? this->w5500LinkUp : ETH.linkUp(); return settings.WIFI.ssid[0] != '\0' && (!linkUp && this->ethStarted) ? conn_types_t::wifi : conn_types_t::ethernet; } case conn_types_t::ethernet: // If Ethernet failed to start or link is down, fallback to AP { bool linkUp = settings.Ethernet.isSPIController() ? this->w5500LinkUp : ETH.linkUp(); if(!this->ethStarted || !linkUp) { return conn_types_t::ap; } return conn_types_t::ethernet; } default: return settings.connType; } } void Network::loop() { // Check W5500 link status manually (polling mode) this->checkW5500Link(); // ORDER OF OPERATIONS: // ---------------------------------------------- // 1. If we are in the middle of a connection process we need to simply bail after the connect method. The // connect method will take care of our target connection for us. // 2. Check to see what type of target connection we need. // a. If this is an ethernet target then the connection needs to perform a fallback if applicable. // b. If this is a wifi target then we need to first check to see if the SSID is available. // c. If an SSID has not been set then we need to turn on the Soft AP. // 3. If the Soft AP is open and the target is either wifi, ethernet, or ethernetpref then // we need to shut it down if there are no connections and the preferred connection is available. // a. Ethernet: Check for an active ethernet connection. We cannot rely on linkup because the PHY will // report that the link is up when no IP address is being served. // b. WiFi: Perform synchronous scan for APs related to the SSID. If the SSID can be found then perform // the connection process for the WiFi connection. // c. SoftAP: This condition retains the Soft AP because no other connection method is available. conn_types_t ctype = this->preferredConnType(); this->connect(ctype); // Connection timeout handled in connect function as well as the opening of the Soft AP if needed. if(this->connecting()) return; // If we are currently attempting to connect to something then we need to bail here. if(_apScanning) { if(settings.WIFI.hidden || // This user has elected to use a hidden AP. (this->connected() && !settings.WIFI.roaming) || // We are already connected and should not be roaming. (this->softAPOpened && WiFi.softAPgetStationNum() != 0) || // The Soft AP is open and a user is connected. (ctype != conn_types_t::wifi)) { // The Ethernet link is up so we should ignore this scan. Serial.println("Cancelling WiFi STA Scan..."); _apScanning = false; WiFi.scanDelete(); } else { int16_t n = WiFi.scanComplete(); if( n >= 0) { // If the scan is complete but the WiFi isn't ready this can return 0. uint8_t bssid[6]; int32_t channel = 0; if(this->getStrongestAP(settings.WIFI.ssid, bssid, &channel)) { if(!WiFi.BSSID() || memcmp(bssid, WiFi.BSSID(), sizeof(bssid)) != 0) { if(!this->connected()) { Serial.printf("Connecting to AP %02X:%02X:%02X:%02X:%02X:%02X CH: %d\n", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], channel); this->connectWiFi(bssid, channel); } else { Serial.printf("Found stronger AP %02X:%02X:%02X:%02X:%02X:%02X CH: %d\n", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], channel); this->changeAP(bssid, channel); } } } _apScanning = false; } } } if(!this->connecting() && !settings.WIFI.hidden) { if((this->softAPOpened && WiFi.softAPgetStationNum() == 0) || (!this->connected() && ctype == conn_types_t::wifi)) { // If the Soft AP is opened and there are no clients connected then we need to scan for an AP. If // our target exists we will exit out of the Soft AP and start that connection. We are also // going to continuously scan when there is no connection and our preferred connection is wifi. if(ctype == conn_types_t::wifi) { // Scan for an AP but only if we are not already scanning. if(!_apScanning && WiFi.scanNetworks(true, false, true, 300, 0, settings.WIFI.ssid) == -1) { _apScanning = true; } } } else if(this->connected() && ctype == conn_types_t::wifi && settings.WIFI.roaming) { // Periodically look for a roaming AP. if(millis() > SSID_SCAN_INTERVAL + this->lastWifiScan) { //Serial.println("Started scan for access points"); if(!_apScanning && WiFi.scanNetworks(true, false, true, 300, 0, settings.WIFI.ssid) == -1) { _apScanning = true; this->lastWifiScan = millis(); } } } } if(millis() - this->lastEmit > 1500) { // Post our connection status if needed. this->lastEmit = millis(); if(this->connected()) { this->emitSockets(); this->lastEmit = millis(); } esp_task_wdt_reset(); // Make sure we do not reboot here. } sockEmit.loop(); mqtt.loop(); if(settings.ssdpBroadcast && this->connected()) { if(!SSDP.isStarted) SSDP.begin(); if(SSDP.isStarted) SSDP.loop(); } else if(!settings.ssdpBroadcast && SSDP.isStarted) SSDP.end(); } bool Network::changeAP(const uint8_t *bssid, const int32_t channel) { esp_task_wdt_reset(); // Make sure we do not reboot here. if(SSDP.isStarted) SSDP.end(); mqtt.disconnect(); //sockEmit.end(); WiFi.disconnect(false, true); this->connType = conn_types_t::unset; this->_connecting = true; this->connectStart = millis(); WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, channel, bssid); this->connectStart = millis(); return false; } void Network::emitSockets() { this->emitHeap(); if(this->needsBroadcast || (this->connType == conn_types_t::wifi && (abs(abs(WiFi.RSSI()) - abs(this->lastRSSI)) > 1 || WiFi.channel() != this->lastChannel))) { this->emitSockets(255); sockEmit.loop(); this->needsBroadcast = false; } } void Network::emitSockets(uint8_t num) { if(this->connType == conn_types_t::ethernet) { JsonSockEvent *json = sockEmit.beginEmit("ethernet"); json->beginObject(); json->addElem("connected", this->connected()); json->addElem("speed", settings.Ethernet.isSPIController() ? (uint8_t)100 : ETH.linkSpeed()); json->addElem("fullduplex", settings.Ethernet.isSPIController() ? true : ETH.fullDuplex()); json->endObject(); sockEmit.endEmit(num); } else { if(WiFi.status() == WL_CONNECTED) { JsonSockEvent *json = sockEmit.beginEmit("wifiStrength"); json->beginObject(); json->addElem("ssid", WiFi.SSID().c_str()); json->addElem("strength", (int32_t)WiFi.RSSI()); json->addElem("channel", (int32_t)this->channel); json->endObject(); sockEmit.endEmit(num); this->lastRSSI = WiFi.RSSI(); this->lastChannel = WiFi.channel(); } else { JsonSockEvent *json = sockEmit.beginEmit("wifiStrength"); json->beginObject(); json->addElem("ssid", ""); json->addElem("strength", (int8_t)-100); json->addElem("channel", (int8_t)-1); json->endObject(); sockEmit.endEmit(num); json = sockEmit.beginEmit("ethernet"); json->beginObject(); json->addElem("connected", false); json->addElem("speed", (uint8_t)0); json->addElem("fullduplex", false); json->endObject(); sockEmit.endEmit(num); this->lastRSSI = -100; this->lastChannel = -1; } } this->emitHeap(num); } void Network::setConnected(conn_types_t connType) { esp_task_wdt_reset(); this->connType = connType; this->connectTime = millis(); connectRetries = 0; Serial.println("Setting connected..."); if(this->connType == conn_types_t::wifi) { if(this->softAPOpened && WiFi.softAPgetStationNum() == 0) { WiFi.softAPdisconnect(true); WiFi.mode(WIFI_STA); } this->_connecting = false; this->ssid = WiFi.SSID(); this->mac = WiFi.BSSIDstr(); this->strength = WiFi.RSSI(); this->channel = WiFi.channel(); this->connectAttempts++; } else if(this->connType == conn_types_t::ethernet) { if(this->softAPOpened) { Serial.println("Disonnecting from SoftAP"); WiFi.softAPdisconnect(true); WiFi.mode(WIFI_OFF); } this->connectAttempts++; this->_connecting = false; this->wifiFallback = false; } // NET: Begin this in the startup. //sockEmit.begin(); esp_task_wdt_reset(); if(this->connectAttempts == 1) { Serial.println(); if(this->connType == conn_types_t::wifi) { Serial.print("Successfully Connected to WiFi!!!!"); Serial.print(WiFi.localIP()); Serial.print(" ("); Serial.print(this->strength); Serial.println("dbm)"); if(settings.IP.dhcp) { settings.IP.ip = WiFi.localIP(); settings.IP.subnet = WiFi.subnetMask(); settings.IP.gateway = WiFi.gatewayIP(); settings.IP.dns1 = WiFi.dnsIP(0); settings.IP.dns2 = WiFi.dnsIP(1); } } else { Serial.print("Successfully Connected to Ethernet!!! "); if(settings.Ethernet.isSPIController()) { Serial.print(this->w5500IP); Serial.println(" (W5500 SPI)"); if(settings.IP.dhcp) { settings.IP.ip = this->w5500IP; // Get netif info for subnet/gateway if(this->w5500_netif) { esp_netif_ip_info_t ip_info; if(esp_netif_get_ip_info(this->w5500_netif, &ip_info) == ESP_OK) { settings.IP.subnet = IPAddress(esp_ip4_addr_get_byte(&ip_info.netmask, 0), esp_ip4_addr_get_byte(&ip_info.netmask, 1), esp_ip4_addr_get_byte(&ip_info.netmask, 2), esp_ip4_addr_get_byte(&ip_info.netmask, 3)); settings.IP.gateway = IPAddress(esp_ip4_addr_get_byte(&ip_info.gw, 0), esp_ip4_addr_get_byte(&ip_info.gw, 1), esp_ip4_addr_get_byte(&ip_info.gw, 2), esp_ip4_addr_get_byte(&ip_info.gw, 3)); } } } } else { Serial.print(ETH.localIP()); if(ETH.fullDuplex()) { Serial.print(" FULL DUPLEX"); } Serial.print(" "); Serial.print(ETH.linkSpeed()); Serial.println("Mbps"); if(settings.IP.dhcp) { settings.IP.ip = ETH.localIP(); settings.IP.subnet = ETH.subnetMask(); settings.IP.gateway = ETH.gatewayIP(); settings.IP.dns1 = ETH.dnsIP(0); settings.IP.dns2 = ETH.dnsIP(1); } } esp_task_wdt_reset(); JsonSockEvent *json = sockEmit.beginEmit("ethernet"); json->beginObject(); json->addElem("connected", this->connected()); json->addElem("speed", settings.Ethernet.isSPIController() ? (uint8_t)100 : ETH.linkSpeed()); json->addElem("fullduplex", settings.Ethernet.isSPIController() ? true : ETH.fullDuplex()); json->endObject(); sockEmit.endEmit(); esp_task_wdt_reset(); } } else { Serial.println(); Serial.print("Reconnected after "); Serial.print(1.0 * (millis() - this->connectStart)/1000); Serial.print("sec IP: "); if(this->connType == conn_types_t::wifi) { Serial.print(WiFi.localIP()); Serial.print(" "); Serial.print(this->mac); Serial.print(" CH:"); Serial.print(this->channel); Serial.print(" ("); Serial.print(this->strength); Serial.print(" dBm)"); } else { if(settings.Ethernet.isSPIController()) { Serial.print(this->w5500IP); Serial.print(" (W5500)"); } else { Serial.print(ETH.localIP()); if(ETH.fullDuplex()) { Serial.print(" FULL DUPLEX"); } Serial.print(" "); Serial.print(ETH.linkSpeed()); Serial.print("Mbps"); } } Serial.print(" Disconnected "); Serial.print(this->connectAttempts - 1); Serial.println(" times"); } SSDP.setHTTPPort(80); SSDP.setSchemaURL(0, "upnp.xml"); SSDP.setChipId(0, this->getChipId()); SSDP.setDeviceType(0, "urn:schemas-rstrouse-org:device:ESPSomfyRTS:1"); SSDP.setName(0, settings.hostname); //SSDP.setSerialNumber(0, "C2496952-5610-47E6-A968-2FC19737A0DB"); //SSDP.setUUID(0, settings.uuid); SSDP.setModelName(0, "ESPSomfy RTS"); if(strlen(settings.chipModel) == 0) SSDP.setModelNumber(0, "ESP32"); else { char sModel[20] = ""; snprintf(sModel, sizeof(sModel), "ESP32-%s", settings.chipModel); SSDP.setModelNumber(0, sModel); } SSDP.setModelURL(0, "https://github.com/rstrouse/ESPSomfy-RTS"); SSDP.setManufacturer(0, "rstrouse"); SSDP.setManufacturerURL(0, "https://github.com/rstrouse"); SSDP.setURL(0, "/"); SSDP.setActive(0, true); esp_task_wdt_reset(); // Try to start mDNS, but don't spam errors if it fails // mDNS errors are normal and non-blocking static bool mDNSLogged = false; if(MDNS.begin(settings.hostname)) { if(!mDNSLogged) { Serial.printf("MDNS Responder Started: serverId=%s\n", settings.serverId); mDNSLogged = true; } // Suppress mDNS addService errors by checking if service already exists // or by wrapping in try-catch equivalent // Note: mDNS errors are normal when network is not fully ready MDNS.addService("http", "tcp", 80); MDNS.addService("espsomfy_rts", "tcp", 8080); MDNS.addServiceTxt("espsomfy_rts", "tcp", "serverId", String(settings.serverId)); MDNS.addServiceTxt("espsomfy_rts", "tcp", "model", "ESPSomfyRTS"); MDNS.addServiceTxt("espsomfy_rts", "tcp", "version", String(settings.fwVersion.name)); } // Reduce mDNS verbosity by setting log level (if supported) // The errors are harmless and occur when mDNS tries to register before network is ready if(settings.ssdpBroadcast) { esp_task_wdt_reset(); SSDP.begin(); } else if(SSDP.isStarted) SSDP.end(); esp_task_wdt_reset(); this->emitSockets(); settings.printAvailHeap(); this->needsBroadcast = true; } bool Network::connectWired() { bool linkUp = settings.Ethernet.isSPIController() ? this->w5500GotIP : ETH.linkUp(); if(linkUp) { // If the ethernet link is re-established then we need to shut down wifi. if(WiFi.status() == WL_CONNECTED) { //sockEmit.end(); WiFi.disconnect(true); WiFi.mode(WIFI_OFF); } if(this->connType != conn_types_t::ethernet) this->setConnected(conn_types_t::ethernet); return true; } else if(this->ethStarted) { // There is no wired connection so we need to fallback if appropriate. if(settings.connType == conn_types_t::ethernetpref && settings.WIFI.ssid[0] != '\0') return this->connectWiFi(); } if(this->connectAttempts > 0) { Serial.printf("Ethernet Connection Lost... %d Reconnecting ", this->connectAttempts); Serial.println(this->mac); } else Serial.println("Connecting to Wired Ethernet"); this->_connecting = true; this->connTarget = conn_types_t::ethernet; this->connType = conn_types_t::unset; if(!this->ethStarted) { // Currently the ethernet module will leak memory if you call begin more than once. this->ethStarted = true; // Don't disable WiFi yet - we'll do it only if Ethernet succeeds // WiFi.mode(WIFI_OFF); if(settings.hostname[0] != '\0') ETH.setHostname(settings.hostname); else ETH.setHostname("ESPSomfy-RTS"); Serial.print("Set hostname to:"); Serial.println(ETH.getHostname()); bool ethBeginSuccess = false; // Check if this is a SPI-based controller (W5500) bool isSPI = settings.Ethernet.isSPIController(); if(isSPI) { // W5500 uses SPI interface - use ESP-IDF APIs Serial.println("Initializing W5500 via SPI..."); Serial.printf("SPI Pins - MOSI:%d MISO:%d SCLK:%d CS:%d INT:%d RST:%d\n", settings.Ethernet.MOSIPin, settings.Ethernet.MISOPin, settings.Ethernet.SCLKPin, settings.Ethernet.CSPin, settings.Ethernet.INTPin, settings.Ethernet.RSTPin); // Validate SPI pins if(settings.Ethernet.MOSIPin < 0 || settings.Ethernet.MISOPin < 0 || settings.Ethernet.SCLKPin < 0 || settings.Ethernet.CSPin < 0 || settings.Ethernet.INTPin < 0) { Serial.println("ERROR: Invalid SPI pin configuration for W5500"); ethBeginSuccess = false; } else { // Hardware reset W5500 if RST pin is defined if(settings.Ethernet.RSTPin >= 0) { pinMode(settings.Ethernet.RSTPin, OUTPUT); digitalWrite(settings.Ethernet.RSTPin, LOW); delay(10); digitalWrite(settings.Ethernet.RSTPin, HIGH); delay(100); } // Initialize SPI bus spi_bus_config_t buscfg = {}; buscfg.mosi_io_num = settings.Ethernet.MOSIPin; buscfg.miso_io_num = settings.Ethernet.MISOPin; buscfg.sclk_io_num = settings.Ethernet.SCLKPin; buscfg.quadwp_io_num = -1; buscfg.quadhd_io_num = -1; buscfg.max_transfer_sz = 0; esp_err_t ret = spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO); if(ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { Serial.printf("Failed to initialize SPI bus: %s\n", esp_err_to_name(ret)); ethBeginSuccess = false; } else { // Configure SPI device for W5500 spi_device_interface_config_t devcfg = {}; devcfg.command_bits = 16; devcfg.address_bits = 8; devcfg.mode = 0; devcfg.clock_speed_hz = 20 * 1000 * 1000; // 20 MHz devcfg.spics_io_num = settings.Ethernet.CSPin; devcfg.queue_size = 20; spi_device_handle_t spi_handle = NULL; ret = spi_bus_add_device(SPI2_HOST, &devcfg, &spi_handle); if(ret != ESP_OK) { Serial.printf("Failed to add SPI device: %s\n", esp_err_to_name(ret)); ethBeginSuccess = false; } else { // Create W5500 config eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi_handle); w5500_config.int_gpio_num = settings.Ethernet.INTPin; // Create MAC config eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); mac_config.smi_mdc_gpio_num = -1; mac_config.smi_mdio_gpio_num = -1; // Create PHY config eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); phy_config.phy_addr = 1; phy_config.reset_gpio_num = settings.Ethernet.RSTPin; // Create MAC and PHY esp_eth_mac_t *mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); esp_eth_phy_t *phy = esp_eth_phy_new_w5500(&phy_config); if(mac == NULL || phy == NULL) { Serial.println("Failed to create W5500 MAC/PHY"); ethBeginSuccess = false; } else { // Create Ethernet driver config esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy); esp_eth_handle_t eth_handle = NULL; ret = esp_eth_driver_install(ð_config, ð_handle); if(ret != ESP_OK) { Serial.printf("Failed to install Ethernet driver: %s\n", esp_err_to_name(ret)); ethBeginSuccess = false; } else { // Set MAC address - generate from chip ID for uniqueness uint8_t mac_addr[6]; uint64_t chipid = ESP.getEfuseMac(); mac_addr[0] = 0x02; // Locally administered MAC mac_addr[1] = (chipid >> 8) & 0xFF; mac_addr[2] = (chipid >> 16) & 0xFF; mac_addr[3] = (chipid >> 24) & 0xFF; mac_addr[4] = (chipid >> 32) & 0xFF; mac_addr[5] = (chipid >> 40) & 0xFF; esp_eth_ioctl(eth_handle, ETH_CMD_S_MAC_ADDR, mac_addr); Serial.printf("W5500: MAC set to %02X:%02X:%02X:%02X:%02X:%02X\n", mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); // Initialize TCP/IP stack (may already be initialized by WiFi) esp_err_t netif_ret = esp_netif_init(); if(netif_ret != ESP_OK && netif_ret != ESP_ERR_INVALID_STATE) { Serial.printf("Failed to init netif: %s\n", esp_err_to_name(netif_ret)); } // Create default event loop if not exists esp_err_t event_ret = esp_event_loop_create_default(); if(event_ret != ESP_OK && event_ret != ESP_ERR_INVALID_STATE) { Serial.printf("Failed to create event loop: %s\n", esp_err_to_name(event_ret)); } // Note: We don't register ESP-IDF event handlers because they conflict // with Arduino's ETH class. Instead we poll in checkW5500Link(). Serial.println("W5500: Using polling mode"); // Attach to TCP/IP stack esp_netif_config_t netif_config = ESP_NETIF_DEFAULT_ETH(); esp_netif_t *eth_netif = esp_netif_new(&netif_config); if(eth_netif == NULL) { Serial.println("Failed to create netif for Ethernet"); esp_eth_driver_uninstall(eth_handle); ethBeginSuccess = false; } else { // Store netif and handle for later use this->w5500_netif = eth_netif; this->w5500_eth_handle = eth_handle; esp_eth_netif_glue_handle_t eth_netif_glue = esp_eth_new_netif_glue(eth_handle); esp_netif_attach(eth_netif, eth_netif_glue); // Start Ethernet driver (DHCP will be started when link is up) Serial.println("W5500: Starting driver..."); ret = esp_eth_start(eth_handle); if(ret != ESP_OK) { Serial.printf("Failed to start Ethernet: %s\n", esp_err_to_name(ret)); ethBeginSuccess = false; } else { ethBeginSuccess = true; Serial.println("W5500 initialized successfully!"); } } } } } } } } else if(!isSPI) { // RMII-based PHY (LAN8720, TLK110, etc.) // Only initialize RMII if it's NOT a SPI controller Serial.println("Initializing RMII PHY..."); Serial.printf("PHY Type:%d Address:%d PWR:%d MDC:%d MDIO:%d CLK:%d\n", settings.Ethernet.phyType, settings.Ethernet.phyAddress, settings.Ethernet.PWRPin, settings.Ethernet.MDCPin, settings.Ethernet.MDIOPin, settings.Ethernet.CLKMode); ethBeginSuccess = ETH.begin(settings.Ethernet.phyAddress, settings.Ethernet.PWRPin, settings.Ethernet.MDCPin, settings.Ethernet.MDIOPin, settings.Ethernet.phyType, settings.Ethernet.CLKMode); } else { // Should not reach here Serial.println("ERROR: Unknown Ethernet controller configuration"); ethBeginSuccess = false; } if(!ethBeginSuccess) { Serial.println("Ethernet Begin failed"); this->ethStarted = false; // Always try to recover by falling back to WiFi or SoftAP Serial.println("Ethernet initialization failed - falling back to WiFi/SoftAP..."); // If WiFi fallback is configured, try WiFi first if(settings.connType == conn_types_t::ethernetpref && settings.WIFI.ssid[0] != '\0') { Serial.println("Attempting WiFi fallback..."); this->wifiFallback = true; WiFi.mode(WIFI_STA); // Re-enable WiFi for fallback return connectWiFi(); } // If WiFi credentials exist but fallback not enabled, enable it temporarily if(settings.WIFI.ssid[0] != '\0') { Serial.println("Ethernet failed, temporarily enabling WiFi fallback..."); this->wifiFallback = true; WiFi.mode(WIFI_STA); return connectWiFi(); } // Last resort: open SoftAP Serial.println("Opening SoftAP as last resort..."); WiFi.mode(WIFI_AP); return false; // Will trigger SoftAP in loop() } else { // Ethernet succeeded, now we can disable WiFi WiFi.mode(WIFI_OFF); if(!settings.IP.dhcp) { if(!ETH.config(settings.IP.ip, settings.IP.gateway, settings.IP.subnet, settings.IP.dns1, settings.IP.dns2)) { Serial.println("Unable to configure static IP address...."); ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); } } else ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); } } this->connectStart = millis(); return true; } void Network::updateHostname() { if(settings.hostname[0] != '\0' && this->connected()) { if(this->connType == conn_types_t::ethernet && strcmp(settings.hostname, ETH.getHostname()) != 0) { Serial.printf("Updating host name to %s...\n", settings.hostname); ETH.setHostname(settings.hostname); MDNS.setInstanceName(settings.hostname); SSDP.setName(0, settings.hostname); } else if(strcmp(settings.hostname, WiFi.getHostname()) != 0) { Serial.printf("Updating host name to %s...\n", settings.hostname); WiFi.setHostname(settings.hostname); MDNS.setInstanceName(settings.hostname); SSDP.setName(0, settings.hostname); } } } bool Network::connectWiFi(const uint8_t *bssid, const int32_t channel) { if(this->softAPOpened && WiFi.softAPgetStationNum() > 0) { // There is a client connected to the soft AP. We do not want to close out the connection. While both the // Soft AP and a wifi connection can coexist on ESP32 the performance is abysmal. WiFi.disconnect(false); this->_connecting = false; this->connType = conn_types_t::unset; return true; } WiFi.setSleep(false); if(!settings.IP.dhcp) { if(!WiFi.config(settings.IP.ip, settings.IP.gateway, settings.IP.subnet, settings.IP.dns1, settings.IP.dns2)) WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); } else WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname); delay(100); if(bssid && channel > 0) { if(WiFi.status() == WL_CONNECTED && WiFi.SSID().compareTo(settings.WIFI.ssid) == 0 && WiFi.channel() == channel) { this->disconnected = 0; return true; } this->connTarget = conn_types_t::wifi; this->connType = conn_types_t::unset; Serial.println("WiFi begin..."); this->_connecting = true; WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, channel, bssid); this->connectStart = millis(); } else if(settings.WIFI.ssid[0] != '\0') { if(WiFi.status() == WL_CONNECTED && WiFi.SSID().compareTo(settings.WIFI.ssid) == 0) { // If we are connected to the target SSID then just return. this->disconnected = 0; this->_connecting = true; return true; } if(this->_connecting) return true; this->_connecting = true; this->connTarget = conn_types_t::wifi; this->connType = conn_types_t::unset; if(this->connectAttempts > 0) { Serial.print("Connection Lost..."); Serial.print(this->mac); Serial.print(" CH:"); Serial.print(this->channel); Serial.print(" ("); Serial.print(this->strength); Serial.println("dbm) "); } else Serial.println("Connecting to AP"); delay(100); // There is also another method simply called hostname() but this is legacy for esp8266. if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname); Serial.print("Set hostname to:"); Serial.println(WiFi.getHostname()); WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); uint8_t _bssid[6]; int32_t _channel = 0; if(!settings.WIFI.hidden && this->getStrongestAP(settings.WIFI.ssid, _bssid, &_channel)) { Serial.printf("Found strongest AP %02X:%02X:%02X:%02X:%02X:%02X CH:%d\n", _bssid[0], _bssid[1], _bssid[2], _bssid[3], _bssid[4], _bssid[5], _channel); WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, _channel, _bssid); } else // If the user has the hidden flag set just connect to whatever the AP gives us. WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase); } this->connectStart = millis(); return true; } bool Network::connect(conn_types_t ctype) { esp_task_wdt_reset(); if(this->connecting()) return true; if(this->disconnectTime == 0) this->disconnectTime = millis(); if(ctype == conn_types_t::ethernet && this->connType != conn_types_t::ethernet) { // Here we need to call the connect to ethernet. this->connectWired(); } else if(ctype == conn_types_t::ap || (!this->connected() && millis() > this->disconnectTime + CONNECT_TIMEOUT)) { if(!this->softAPOpened && !this->openingSoftAP) { this->disconnectTime = millis(); this->openSoftAP(); } else if(this->softAPOpened && !this->openingSoftAP && (ctype == conn_types_t::wifi && this->connType != conn_types_t::wifi && settings.WIFI.hidden)) { // When thge softAP is open then we need to try to connect to wifi repeatedly if the user connects to a hidden SSID. this->connectWiFi(); } } else if((ctype == conn_types_t::wifi && this->connType != conn_types_t::wifi && settings.WIFI.hidden)) { this->connectWiFi(); } return true; } uint32_t Network::getChipId() { uint32_t chipId = 0; uint64_t mac = ESP.getEfuseMac(); for(int i=0; i<17; i=i+8) { chipId |= ((mac >> (40 - i)) & 0xff) << i; } return chipId; } bool Network::getStrongestAP(const char *ssid, uint8_t *bssid, int32_t *channel) { // The new AP must be at least 10dbm greater. int32_t strength = this->connected() ? WiFi.RSSI() + 10 : -127; int32_t chan = -1; memset(bssid, 0x00, 6); esp_task_wdt_delete(NULL); int16_t n = WiFi.scanComplete(); //int16_t n = this->connected() ? WiFi.scanComplete() : WiFi.scanNetworks(false, false, false, 300, 0, ssid); esp_task_wdt_add(NULL); for(int16_t i = 0; i < n; i++) { if(WiFi.SSID(i).compareTo(ssid) == 0) { if(WiFi.RSSI(i) > strength) { strength = WiFi.RSSI(i); memcpy(bssid, WiFi.BSSID(i), 6); *channel = chan = WiFi.channel(i); } } } WiFi.scanDelete(); return chan > 0; } bool Network::openSoftAP() { if(this->softAPOpened || this->openingSoftAP) return true; if(this->connected()) WiFi.disconnect(false); this->openingSoftAP = true; Serial.println(); Serial.println("Turning the HotSpot On"); esp_task_wdt_reset(); // Make sure we do not reboot here. WiFi.softAP(strlen(settings.hostname) > 0 ? settings.hostname : "ESPSomfy RTS", ""); delay(200); return true; } bool Network::connected() { if(this->connecting()) return false; else if(this->connType == conn_types_t::unset) return false; else if(this->connType == conn_types_t::wifi) return WiFi.status() == WL_CONNECTED; else if(this->connType == conn_types_t::ethernet) { return settings.Ethernet.isSPIController() ? this->w5500GotIP : ETH.linkUp(); } else return this->connType != conn_types_t::unset; return false; } bool Network::connecting() { if(this->_connecting && millis() > this->connectStart + CONNECT_TIMEOUT) this->_connecting = false; return this->_connecting; } void Network::clearConnecting() { this->_connecting = false; } void Network::networkEvent(WiFiEvent_t event) { switch(event) { case ARDUINO_EVENT_WIFI_READY: Serial.println("(evt) WiFi interface ready"); break; case ARDUINO_EVENT_WIFI_SCAN_DONE: Serial.printf("(evt) Completed scan for access points (%d)\n", WiFi.scanComplete()); //Serial.println("(evt) Completed scan for access points"); net.lastWifiScan = millis(); break; case ARDUINO_EVENT_WIFI_STA_START: Serial.println("WiFi station mode started"); if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname); break; case ARDUINO_EVENT_WIFI_STA_STOP: Serial.println("(evt) WiFi clients stopped"); break; case ARDUINO_EVENT_WIFI_STA_CONNECTED: Serial.println("(evt) Connected to WiFi STA access point"); break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: Serial.printf("(evt) Disconnected from WiFi STA access point. Connecting: %d\n", net.connecting()); net.connType = conn_types_t::unset; net.disconnectTime = millis(); net.clearConnecting(); break; case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: Serial.println("(evt) Authentication mode of STA access point has changed"); break; case ARDUINO_EVENT_WIFI_STA_GOT_IP: Serial.print("(evt) Got WiFi STA IP: "); Serial.println(WiFi.localIP()); net.connType = conn_types_t::wifi; net.connectTime = millis(); net.setConnected(conn_types_t::wifi); break; case ARDUINO_EVENT_WIFI_STA_LOST_IP: Serial.println("Lost IP address and IP address is reset to 0"); break; case ARDUINO_EVENT_ETH_GOT_IP: // Skip if using W5500 (handled separately) if(settings.Ethernet.isSPIController()) break; // If the Wifi is connected then drop that connection if(WiFi.status() == WL_CONNECTED) WiFi.disconnect(true); Serial.print("Got Ethernet IP "); Serial.println(ETH.localIP()); net.connectTime = millis(); net.connType = conn_types_t::ethernet; if(settings.IP.dhcp) { settings.IP.ip = ETH.localIP(); settings.IP.subnet = ETH.subnetMask(); settings.IP.gateway = ETH.gatewayIP(); settings.IP.dns1 = ETH.dnsIP(0); settings.IP.dns2 = ETH.dnsIP(1); } net.setConnected(conn_types_t::ethernet); break; case ARDUINO_EVENT_ETH_CONNECTED: if(settings.Ethernet.isSPIController()) break; Serial.print("(evt) Ethernet Connected "); break; case ARDUINO_EVENT_ETH_DISCONNECTED: if(settings.Ethernet.isSPIController()) break; Serial.println("(evt) Ethernet Disconnected"); net.connType = conn_types_t::unset; net.disconnectTime = millis(); net.clearConnecting(); break; case ARDUINO_EVENT_ETH_START: if(settings.Ethernet.isSPIController()) break; Serial.println("(evt) Ethernet Started"); net.ethStarted = true; break; case ARDUINO_EVENT_ETH_STOP: if(settings.Ethernet.isSPIController()) break; Serial.println("(evt) Ethernet Stopped"); net.connType = conn_types_t::unset; net.ethStarted = false; break; case ARDUINO_EVENT_WIFI_AP_START: Serial.print("(evt) WiFi SoftAP Started IP:"); Serial.println(WiFi.softAPIP()); net.openingSoftAP = false; net.softAPOpened = true; break; case ARDUINO_EVENT_WIFI_AP_STOP: if(!net.openingSoftAP) Serial.println("(evt) WiFi SoftAP Stopped"); net.softAPOpened = false; break; default: if(event > ARDUINO_EVENT_ETH_START) Serial.printf("(evt) Unknown Ethernet Event %d\n", event); break; } } void Network::emitHeap(uint8_t num) { bool bEmit = false; bool bTimeEmit = millis() - _lastHeapEmit > 15000; bool bRoomEmit = false; bool bValEmit = false; if(num != 255 || this->needsBroadcast) bEmit = true; if(millis() - _lastHeapEmit > 15000) bTimeEmit = true; uint32_t freeHeap = ESP.getFreeHeap(); uint32_t maxHeap = ESP.getMaxAllocHeap(); uint32_t minHeap = ESP.getMinFreeHeap(); if(abs((int)(freeHeap - _lastHeap)) > 1500) bValEmit = true; if(abs((int)(maxHeap - _lastMaxHeap)) > 1500) bValEmit = true; bRoomEmit = sockEmit.activeClients(0) > 0; if(bValEmit) bTimeEmit = millis() - _lastHeapEmit > 7000; if(bEmit || bTimeEmit || bRoomEmit || bValEmit) { JsonSockEvent *json = sockEmit.beginEmit("memStatus"); json->beginObject(); json->addElem("max", maxHeap); json->addElem("free", freeHeap); json->addElem("min", minHeap); json->addElem("total", ESP.getHeapSize()); json->endObject(); if(num == 255 && bTimeEmit && bValEmit) { sockEmit.endEmit(num); _lastHeapEmit = millis(); _lastHeap = freeHeap; _lastMaxHeap = maxHeap; //Serial.printf("BROAD HEAP: Emit:%d TimeEmit:%d ValEmit:%d\n", bEmit, bTimeEmit, bValEmit); } else if(num != 255) { sockEmit.endEmit(num); //Serial.printf("TARGET HEAP %d: Emit:%d TimeEmit:%d ValEmit:%d\n", num, bEmit, bTimeEmit, bValEmit); } else if(bRoomEmit) { sockEmit.endEmitRoom(0); //Serial.printf("ROOM HEAP: Emit:%d TimeEmit:%d ValEmit:%d\n", bEmit, bTimeEmit, bValEmit); } } } // Check W5500 link status manually void Network::checkW5500Link() { if(!this->ethStarted || !settings.Ethernet.isSPIController() || this->w5500_eth_handle == nullptr) { return; } static unsigned long lastCheck = 0; static bool lastLinkState = false; // Only check every 2 seconds to avoid spam if(millis() - lastCheck < 2000) return; lastCheck = millis(); // Check link status via speed (if we can get speed, link is up) esp_eth_handle_t eth_handle = (esp_eth_handle_t)this->w5500_eth_handle; eth_speed_t speed; esp_err_t ret = esp_eth_ioctl(eth_handle, ETH_CMD_G_SPEED, &speed); if(ret == ESP_OK) { // If we can get speed, link is up bool currentLink = true; if(currentLink != lastLinkState) { lastLinkState = currentLink; if(currentLink) { Serial.println("W5500: Link UP"); this->w5500LinkUp = true; // Start DHCP client now that link is up if(this->w5500_netif) { esp_netif_dhcpc_start(this->w5500_netif); } } else { Serial.println("W5500: Link DOWN"); this->w5500LinkUp = false; this->w5500GotIP = false; this->connType = conn_types_t::unset; } } // If link is up but no IP yet, check for IP if(currentLink && !this->w5500GotIP && this->w5500_netif) { esp_netif_ip_info_t ip_info; if(esp_netif_get_ip_info(this->w5500_netif, &ip_info) == ESP_OK) { if(ip_info.ip.addr != 0) { this->w5500GotIP = true; this->w5500IP = IPAddress( esp_ip4_addr_get_byte(&ip_info.ip, 0), esp_ip4_addr_get_byte(&ip_info.ip, 1), esp_ip4_addr_get_byte(&ip_info.ip, 2), esp_ip4_addr_get_byte(&ip_info.ip, 3) ); Serial.print("W5500: Got IP "); Serial.println(this->w5500IP); // Configure DNS servers from netif or use defaults esp_netif_dns_info_t dns_info; if(esp_netif_get_dns_info(this->w5500_netif, ESP_NETIF_DNS_MAIN, &dns_info) == ESP_OK && dns_info.ip.u_addr.ip4.addr != 0) { ip_addr_t dns_addr; dns_addr.type = IPADDR_TYPE_V4; dns_addr.u_addr.ip4.addr = dns_info.ip.u_addr.ip4.addr; dns_setserver(0, &dns_addr); } else { // Use Google DNS as fallback ip_addr_t dns_addr; IP_ADDR4(&dns_addr, 8, 8, 8, 8); dns_setserver(0, &dns_addr); } this->setConnected(conn_types_t::ethernet); } } } } } // W5500 Event Handler for ESP-IDF events void Network::w5500EventHandler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { Network *self = static_cast(arg); if(event_base == ETH_EVENT) { switch(event_id) { case ETHERNET_EVENT_CONNECTED: Serial.println("W5500: Link Up"); self->w5500LinkUp = true; break; case ETHERNET_EVENT_DISCONNECTED: Serial.println("W5500: Link Down"); self->w5500LinkUp = false; self->w5500GotIP = false; self->connType = conn_types_t::unset; self->disconnectTime = millis(); break; case ETHERNET_EVENT_START: Serial.println("W5500: Started"); break; case ETHERNET_EVENT_STOP: Serial.println("W5500: Stopped"); self->w5500LinkUp = false; self->w5500GotIP = false; break; } } else if(event_base == IP_EVENT && event_id == IP_EVENT_ETH_GOT_IP) { ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; Serial.printf("W5500: Got IP - %d.%d.%d.%d\n", IP2STR(&event->ip_info.ip)); self->w5500GotIP = true; self->w5500IP = IPAddress(esp_ip4_addr_get_byte(&event->ip_info.ip, 0), esp_ip4_addr_get_byte(&event->ip_info.ip, 1), esp_ip4_addr_get_byte(&event->ip_info.ip, 2), esp_ip4_addr_get_byte(&event->ip_info.ip, 3)); self->setConnected(conn_types_t::ethernet); } }