From ad62c79a40da14a834efa88bbad6a9db622cf4e5 Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 1 Sep 2026 21:56:36 +0200 Subject: [PATCH] power save - Reduce EMS-ESP energy consumption #3213 --- data/pre_load.json | 1 + interface/src/app/main/types.ts | 1 + .../src/app/settings/ApplicationSettings.tsx | 12 ++ mock-api/restServer.ts | 1 + project-words.txt | 4 +- src/core/main.cpp | 3 + src/core/network.cpp | 126 +++++++++++++----- src/core/network.h | 4 + src/web/WebSettingsService.cpp | 3 + src/web/WebSettingsService.h | 1 + 10 files changed, 124 insertions(+), 32 deletions(-) diff --git a/data/pre_load.json b/data/pre_load.json index 24e5fea66..36c23849c 100644 --- a/data/pre_load.json +++ b/data/pre_load.json @@ -129,6 +129,7 @@ "eth_power": 15, "eth_phy_addr": 0, "eth_clock_mode": 1, + "eth_10mbit": false, "modbus_enabled": false, "modbus_port": 502, "modbus_max_clients": 10, diff --git a/interface/src/app/main/types.ts b/interface/src/app/main/types.ts index 70d91b29d..5cced48dd 100644 --- a/interface/src/app/main/types.ts +++ b/interface/src/app/main/types.ts @@ -39,6 +39,7 @@ export interface Settings { eth_power: number; eth_phy_addr: number; eth_clock_mode: number; + eth_10mbit: boolean; platform: string; modbus_enabled: boolean; modbus_port: number; diff --git a/interface/src/app/settings/ApplicationSettings.tsx b/interface/src/app/settings/ApplicationSettings.tsx index d9626d41b..1375340af 100644 --- a/interface/src/app/settings/ApplicationSettings.tsx +++ b/interface/src/app/settings/ApplicationSettings.tsx @@ -820,6 +820,18 @@ const ApplicationSettings = () => { )} + {data.phy_type !== 0 && ( + + } + label="Force 10Mbit half-duplex" + /> + )} )} diff --git a/mock-api/restServer.ts b/mock-api/restServer.ts index 919620cb9..842ae4147 100644 --- a/mock-api/restServer.ts +++ b/mock-api/restServer.ts @@ -58,6 +58,7 @@ let settings = { eth_power: 15, eth_phy_addr: 0, eth_clock_mode: 1, + eth_10mbit: false, led_type: 0, platform: 'ESP32', modbus_enabled: false, diff --git a/project-words.txt b/project-words.txt index 242eb64b1..2aa5d4e27 100644 --- a/project-words.txt +++ b/project-words.txt @@ -1345,4 +1345,6 @@ Sumr eraseap roomtempoffset dhcpc -wifioff \ No newline at end of file +wifioff +autonegotiation +Mbit \ No newline at end of file diff --git a/src/core/main.cpp b/src/core/main.cpp index 68130146c..d6597b301 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -28,4 +28,7 @@ void setup() { void loop() { application.loop(); +#ifndef EMSESP_STANDALONE + delay(1); // block for a tick so the idle task gets to run and the core can clock-gate +#endif } diff --git a/src/core/network.cpp b/src/core/network.cpp index b62baeb64..e201654eb 100644 --- a/src/core/network.cpp +++ b/src/core/network.cpp @@ -57,6 +57,7 @@ void Network::begin() { eth_power_ = settings.eth_power; eth_phy_addr_ = settings.eth_phy_addr; eth_clock_mode_ = settings.eth_clock_mode; + eth_10mbit_ = settings.eth_10mbit; }); // get Access Point settings @@ -78,39 +79,16 @@ void Network::begin() { phase_ = initialPhase(); - // Initialise WiFi once when the Network service starts. - // persistent(false) keeps credentials in RAM so we don't auto-join leftover NVS config. - WiFi.persistent(false); - WiFi.setAutoReconnect(false); - WiFi.mode(WIFI_STA); + startWiFiRadio(); - // Keep the radio up. disconnect(wifioff=true) maps to STA.end() -> esp_wifi_stop() - if (WiFi.STA.started()) { - WiFi.disconnect(false, true); // drop leftover association, wipe RAM config +#if CONFIG_IDF_TARGET_ESP32 + // Ethernet is switched off but the board still has a PHY wired up. Hold its power/oscillator + // enable low so an unused LAN8720 isn't left clocked and burning current for the whole uptime. + if (phy_type_ == PHY_type::PHY_TYPE_NONE && eth_power_ != -1) { + pinMode(eth_power_, OUTPUT); + digitalWrite(eth_power_, LOW); } - - WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); - WiFi.setHostname(hostname_.c_str()); // updates shared default_hostname buffer - WiFi.enableSTA(true); // no-op if already STA; recreates netif after a full off - WiFi.STA.setHostname(hostname_.c_str()); // pushes to esp_netif_set_hostname - WiFi.enableIPv6(true); - if (staticIPConfig_) { - WiFi.config(localIP_, gatewayIP_, subnetMask_, dnsIP1_, dnsIP2_); // configure for static IP - } - - // www.esp32.com/viewtopic.php?t=12055 - if (bandwidth20_) { - esp_wifi_set_bandwidth(static_cast(ESP_IF_WIFI_STA), WIFI_BW_HT20); - } else { - esp_wifi_set_bandwidth(static_cast(ESP_IF_WIFI_STA), WIFI_BW_HT40); - } - if (nosleep_) { - WiFi.setSleep(false); // turn off sleep - WIFI_PS_NONE - } - - // scan settings give connect issues since arduino 2.0.14 and arduino 3.x.x with some wifi systems - // WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); // default is FAST_SCAN - // WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); // is default, no need to set +#endif // avoid duplicate registration, so register the lambdas only once across the lifetime of this Network instance if (!wifi_events_registered_) { @@ -326,6 +304,10 @@ void Network::checkConnection() { network_iface_ = NetIface::NONE; if (lost_iface == NetIface::ETHERNET) { LOG_WARNING("Ethernet connection lost"); + // restore the radio now so the WiFi and AP fallbacks have something to work with + if (wifi_radio_off_) { + startWiFiRadio(); + } return; } begin(); @@ -502,6 +484,59 @@ const char * Network::disconnectReason([[maybe_unused]] uint8_t code) { return ""; } +// Bring up the STA interface and apply all the radio-level settings to it +// Safe to call repeatedly - enableSTA() is a no-op when the STA is already running, and recreates the netif after stopWiFiRadio() has torn it down +void Network::startWiFiRadio() { +#ifndef EMSESP_STANDALONE + WiFi.persistent(false); // keep credentials in RAM so we don't auto-join leftover NVS config + WiFi.setAutoReconnect(false); + WiFi.mode(WIFI_STA); + + // Keep the radio up. disconnect(wifioff=true) maps to STA.end() -> esp_wifi_stop() + if (WiFi.STA.started()) { + WiFi.disconnect(false, true); // drop leftover association, wipe RAM config + } + + // scan settings give connect issues since arduino 2.0.14 and arduino 3.x.x with some wifi systems + WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); // default is FAST_SCAN + WiFi.setHostname(hostname_.c_str()); // updates shared default_hostname buffer + WiFi.enableSTA(true); // no-op if already STA; recreates netif after a full off + WiFi.STA.setHostname(hostname_.c_str()); // pushes to esp_netif_set_hostname + WiFi.enableIPv6(true); + if (staticIPConfig_) { + WiFi.config(localIP_, gatewayIP_, subnetMask_, dnsIP1_, dnsIP2_); // configure for static IP + } + + // www.esp32.com/viewtopic.php?t=12055 + if (bandwidth20_) { + esp_wifi_set_bandwidth(static_cast(ESP_IF_WIFI_STA), WIFI_BW_HT20); + } else { + esp_wifi_set_bandwidth(static_cast(ESP_IF_WIFI_STA), WIFI_BW_HT40); + } + if (nosleep_) { + WiFi.setSleep(false); // turn off sleep - WIFI_PS_NONE + } else { + WiFi.setSleep(true); // WIFI_PS_MIN_MODEM + } + + wifi_radio_off_ = false; +#endif +} + +// Power down the WiFi radio completely (esp_wifi_stop), used when Ethernet is carrying the traffic. +// An idle STA still keeps the receiver powered, which on an Ethernet gateway is a pure waste of heat. +void Network::stopWiFiRadio() { +#ifndef EMSESP_STANDALONE + if (wifi_radio_off_) { + return; + } + WiFi.mode(WIFI_OFF); + wifi_radio_off_ = true; + wifi_connect_pending_ = false; + LOG_INFO("Powering down WiFi radio, Ethernet is active"); +#endif +} + // WiFi management void Network::startWIFI() { #ifndef EMSESP_STANDALONE @@ -510,6 +545,11 @@ void Network::startWIFI() { return; } + // the radio was powered down while Ethernet was up, so bring it back before trying to associate + if (wifi_radio_off_) { + startWiFiRadio(); + } + // exit if WiFi or Ethernet is already connected, or if we have no SSID or another Wifi.begin() is already in progress if (WiFi.isConnected() || ssid_.isEmpty() || ethernet_connected() || wifi_connect_pending_) { return; @@ -610,6 +650,17 @@ void Network::startEthernet() { eth_hostname_handler_registered_ = true; } + // Forcing 10BASE-T roughly halves the PHY's power draw, which on these boards is usually the + // hottest component. All three must be set before ETH.begin(), and in this order - the speed + // and duplex setters are ignored while autonegotiation is still on. Half duplex is deliberate: + // a switch port that can't negotiate falls back to parallel detection, which is always half, + // so forcing full here would be a duplex mismatch. Opt-in only for that reason. + if (eth_10mbit_) { + ETH.setAutoNegotiation(false); + ETH.setLinkSpeed(10); + ETH.setFullDuplex(false); + } + if (ETH.begin(type, eth_phy_addr_, 23, 18, eth_power_, (eth_clock_mode_t)eth_clock_mode_)) { // LOG_DEBUG("Ethernet module found - initialising"); ethernet_started_ = true; @@ -739,6 +790,14 @@ void Network::findNetworks() { stopAP(); } + // Ethernet is carrying the traffic so the STA is dead weight - shut the radio down. + // It is brought back by checkConnection() when the link drops, or by startWIFI() if we + // fall through to the WiFi phase. Do this before startmDNS() so we don't register on a + // netif we are about to destroy. + if (network_iface_ == NetIface::ETHERNET) { + stopWiFiRadio(); + } + // count the number of restarts (for Wifi and Eth) if (juststopped_) { juststopped_ = false; @@ -823,6 +882,11 @@ void Network::startAP() { return; } + // the AP needs the radio, which may have been powered down while Ethernet was up + if (wifi_radio_off_) { + startWiFiRadio(); + } + if (!WiFi.softAPConfig(ap_localIP_, ap_gatewayIP_, ap_subnetMask_)) { LOG_DEBUG("softAPConfig failed"); return; diff --git a/src/core/network.h b/src/core/network.h index 13ae4ab41..d97320af0 100644 --- a/src/core/network.h +++ b/src/core/network.h @@ -186,6 +186,8 @@ class Network { void startAP(); void startWIFI(); void startEthernet(); + void startWiFiRadio(); + void stopWiFiRadio(); void setWiFiPower(uint8_t tx_power); const char * disconnectReason(uint8_t code); void stopAP(); @@ -216,6 +218,7 @@ class Network { bool ethernet_started_ = false; // ETH.begin() succeeded; the driver runs for the lifetime of the boot bool wifi_ever_connected_ = false; // set true once we've successfully obtained an IP bool ethernet_ever_connected_ = false; // set true once we've successfully obtained an IP + bool wifi_radio_off_ = false; // STA powered down because Ethernet is carrying the traffic // Network and AP settings bool enableMDNS_; @@ -236,6 +239,7 @@ class Network { int8_t eth_power_; uint8_t eth_phy_addr_; uint8_t eth_clock_mode_; + bool eth_10mbit_; // AP settings uint8_t ap_provisionMode_; diff --git a/src/web/WebSettingsService.cpp b/src/web/WebSettingsService.cpp index e46d1e093..abdb80b00 100644 --- a/src/web/WebSettingsService.cpp +++ b/src/web/WebSettingsService.cpp @@ -79,6 +79,7 @@ void WebSettings::read(WebSettings & settings, JsonObject root) { root["eth_power"] = settings.eth_power; root["eth_phy_addr"] = settings.eth_phy_addr; root["eth_clock_mode"] = settings.eth_clock_mode; + root["eth_10mbit"] = settings.eth_10mbit; root["modbus_enabled"] = settings.modbus_enabled; root["modbus_port"] = settings.modbus_port; root["modbus_max_clients"] = settings.modbus_max_clients; @@ -120,6 +121,7 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) { settings.eth_power = root["eth_power"]; settings.eth_phy_addr = root["eth_phy_addr"]; settings.eth_clock_mode = root["eth_clock_mode"]; + settings.eth_10mbit = root["eth_10mbit"]; settings.led_type = root["led_type"]; // 1 = RGB-LED reset_flags(); @@ -145,6 +147,7 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) { if (settings.phy_type != PHY_type::PHY_TYPE_NONE) { check_flag(original_settings.eth_power, settings.eth_power, ChangeFlags::RESTART); check_flag(original_settings.eth_clock_mode, settings.eth_clock_mode, ChangeFlags::RESTART); + check_flag(original_settings.eth_10mbit, settings.eth_10mbit, ChangeFlags::RESTART); if (settings.eth_power != -1) { // Ethernet Power -1 means disabled EMSESP::system_.remove_gpio(settings.eth_power, true); } diff --git a/src/web/WebSettingsService.h b/src/web/WebSettingsService.h index d8a600f81..1f6398aa9 100644 --- a/src/web/WebSettingsService.h +++ b/src/web/WebSettingsService.h @@ -119,6 +119,7 @@ class WebSettings { int8_t eth_power; // -1 means disabled uint8_t eth_phy_addr; uint8_t eth_clock_mode; + bool eth_10mbit; // force 10BASE-T half-duplex, no auto-negotiation bool developer_mode; // developer mode bool disable_reset; // disable reset