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