power save - Reduce EMS-ESP energy consumption #3213

This commit is contained in:
proddy
2026-09-01 21:56:36 +02:00
parent e3b36a4084
commit ad62c79a40
10 changed files with 124 additions and 32 deletions
+1
View File
@@ -129,6 +129,7 @@
"eth_power": 15, "eth_power": 15,
"eth_phy_addr": 0, "eth_phy_addr": 0,
"eth_clock_mode": 1, "eth_clock_mode": 1,
"eth_10mbit": false,
"modbus_enabled": false, "modbus_enabled": false,
"modbus_port": 502, "modbus_port": 502,
"modbus_max_clients": 10, "modbus_max_clients": 10,
+1
View File
@@ -39,6 +39,7 @@ export interface Settings {
eth_power: number; eth_power: number;
eth_phy_addr: number; eth_phy_addr: number;
eth_clock_mode: number; eth_clock_mode: number;
eth_10mbit: boolean;
platform: string; platform: string;
modbus_enabled: boolean; modbus_enabled: boolean;
modbus_port: number; modbus_port: number;
@@ -820,6 +820,18 @@ const ApplicationSettings = () => {
</Grid> </Grid>
</Grid> </Grid>
)} )}
{data.phy_type !== 0 && (
<BlockFormControlLabel
control={
<Checkbox
checked={data.eth_10mbit}
onChange={updateFormValue}
name="eth_10mbit"
/>
}
label="Force 10Mbit half-duplex"
/>
)}
</> </>
)} )}
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
+1
View File
@@ -58,6 +58,7 @@ let settings = {
eth_power: 15, eth_power: 15,
eth_phy_addr: 0, eth_phy_addr: 0,
eth_clock_mode: 1, eth_clock_mode: 1,
eth_10mbit: false,
led_type: 0, led_type: 0,
platform: 'ESP32', platform: 'ESP32',
modbus_enabled: false, modbus_enabled: false,
+3 -1
View File
@@ -1345,4 +1345,6 @@ Sumr
eraseap eraseap
roomtempoffset roomtempoffset
dhcpc dhcpc
wifioff wifioff
autonegotiation
Mbit
+3
View File
@@ -28,4 +28,7 @@ void setup() {
void loop() { void loop() {
application.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
} }
+95 -31
View File
@@ -57,6 +57,7 @@ void Network::begin() {
eth_power_ = settings.eth_power; eth_power_ = settings.eth_power;
eth_phy_addr_ = settings.eth_phy_addr; eth_phy_addr_ = settings.eth_phy_addr;
eth_clock_mode_ = settings.eth_clock_mode; eth_clock_mode_ = settings.eth_clock_mode;
eth_10mbit_ = settings.eth_10mbit;
}); });
// get Access Point settings // get Access Point settings
@@ -78,39 +79,16 @@ void Network::begin() {
phase_ = initialPhase(); phase_ = initialPhase();
// Initialise WiFi once when the Network service starts. startWiFiRadio();
// 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);
// Keep the radio up. disconnect(wifioff=true) maps to STA.end() -> esp_wifi_stop() #if CONFIG_IDF_TARGET_ESP32
if (WiFi.STA.started()) { // Ethernet is switched off but the board still has a PHY wired up. Hold its power/oscillator
WiFi.disconnect(false, true); // drop leftover association, wipe RAM config // 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);
} }
#endif
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<wifi_interface_t>(ESP_IF_WIFI_STA), WIFI_BW_HT20);
} else {
esp_wifi_set_bandwidth(static_cast<wifi_interface_t>(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
// avoid duplicate registration, so register the lambdas only once across the lifetime of this Network instance // avoid duplicate registration, so register the lambdas only once across the lifetime of this Network instance
if (!wifi_events_registered_) { if (!wifi_events_registered_) {
@@ -326,6 +304,10 @@ void Network::checkConnection() {
network_iface_ = NetIface::NONE; network_iface_ = NetIface::NONE;
if (lost_iface == NetIface::ETHERNET) { if (lost_iface == NetIface::ETHERNET) {
LOG_WARNING("Ethernet connection lost"); 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; return;
} }
begin(); begin();
@@ -502,6 +484,59 @@ const char * Network::disconnectReason([[maybe_unused]] uint8_t code) {
return ""; 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<wifi_interface_t>(ESP_IF_WIFI_STA), WIFI_BW_HT20);
} else {
esp_wifi_set_bandwidth(static_cast<wifi_interface_t>(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 // WiFi management
void Network::startWIFI() { void Network::startWIFI() {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
@@ -510,6 +545,11 @@ void Network::startWIFI() {
return; 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 // 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_) { if (WiFi.isConnected() || ssid_.isEmpty() || ethernet_connected() || wifi_connect_pending_) {
return; return;
@@ -610,6 +650,17 @@ void Network::startEthernet() {
eth_hostname_handler_registered_ = true; 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_)) { if (ETH.begin(type, eth_phy_addr_, 23, 18, eth_power_, (eth_clock_mode_t)eth_clock_mode_)) {
// LOG_DEBUG("Ethernet module found - initialising"); // LOG_DEBUG("Ethernet module found - initialising");
ethernet_started_ = true; ethernet_started_ = true;
@@ -739,6 +790,14 @@ void Network::findNetworks() {
stopAP(); 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) // count the number of restarts (for Wifi and Eth)
if (juststopped_) { if (juststopped_) {
juststopped_ = false; juststopped_ = false;
@@ -823,6 +882,11 @@ void Network::startAP() {
return; 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_)) { if (!WiFi.softAPConfig(ap_localIP_, ap_gatewayIP_, ap_subnetMask_)) {
LOG_DEBUG("softAPConfig failed"); LOG_DEBUG("softAPConfig failed");
return; return;
+4
View File
@@ -186,6 +186,8 @@ class Network {
void startAP(); void startAP();
void startWIFI(); void startWIFI();
void startEthernet(); void startEthernet();
void startWiFiRadio();
void stopWiFiRadio();
void setWiFiPower(uint8_t tx_power); void setWiFiPower(uint8_t tx_power);
const char * disconnectReason(uint8_t code); const char * disconnectReason(uint8_t code);
void stopAP(); 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 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 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 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 // Network and AP settings
bool enableMDNS_; bool enableMDNS_;
@@ -236,6 +239,7 @@ class Network {
int8_t eth_power_; int8_t eth_power_;
uint8_t eth_phy_addr_; uint8_t eth_phy_addr_;
uint8_t eth_clock_mode_; uint8_t eth_clock_mode_;
bool eth_10mbit_;
// AP settings // AP settings
uint8_t ap_provisionMode_; uint8_t ap_provisionMode_;
+3
View File
@@ -79,6 +79,7 @@ void WebSettings::read(WebSettings & settings, JsonObject root) {
root["eth_power"] = settings.eth_power; root["eth_power"] = settings.eth_power;
root["eth_phy_addr"] = settings.eth_phy_addr; root["eth_phy_addr"] = settings.eth_phy_addr;
root["eth_clock_mode"] = settings.eth_clock_mode; root["eth_clock_mode"] = settings.eth_clock_mode;
root["eth_10mbit"] = settings.eth_10mbit;
root["modbus_enabled"] = settings.modbus_enabled; root["modbus_enabled"] = settings.modbus_enabled;
root["modbus_port"] = settings.modbus_port; root["modbus_port"] = settings.modbus_port;
root["modbus_max_clients"] = settings.modbus_max_clients; 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_power = root["eth_power"];
settings.eth_phy_addr = root["eth_phy_addr"]; settings.eth_phy_addr = root["eth_phy_addr"];
settings.eth_clock_mode = root["eth_clock_mode"]; settings.eth_clock_mode = root["eth_clock_mode"];
settings.eth_10mbit = root["eth_10mbit"];
settings.led_type = root["led_type"]; // 1 = RGB-LED settings.led_type = root["led_type"]; // 1 = RGB-LED
reset_flags(); reset_flags();
@@ -145,6 +147,7 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) {
if (settings.phy_type != PHY_type::PHY_TYPE_NONE) { 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_power, settings.eth_power, ChangeFlags::RESTART);
check_flag(original_settings.eth_clock_mode, settings.eth_clock_mode, 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 if (settings.eth_power != -1) { // Ethernet Power -1 means disabled
EMSESP::system_.remove_gpio(settings.eth_power, true); EMSESP::system_.remove_gpio(settings.eth_power, true);
} }
+1
View File
@@ -119,6 +119,7 @@ class WebSettings {
int8_t eth_power; // -1 means disabled int8_t eth_power; // -1 means disabled
uint8_t eth_phy_addr; uint8_t eth_phy_addr;
uint8_t eth_clock_mode; uint8_t eth_clock_mode;
bool eth_10mbit; // force 10BASE-T half-duplex, no auto-negotiation
bool developer_mode; // developer mode bool developer_mode; // developer mode
bool disable_reset; // disable reset bool disable_reset; // disable reset