[ethernet] Add enable_on_boot lifecycle + lazy-init to reclaim DMA-capable SRAM (#16607)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-06-01 15:30:07 -05:00
1 parent 4e48682468
commit 2454ad1645
6 files changed
+219 -3

No files matched your search

+28
View File
@@ -2,6 +2,7 @@ from dataclasses import dataclass
import logging import logging
from esphome import automation, pins from esphome import automation, pins
from esphome.automation import Condition
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.network import ip_address_literal from esphome.components.network import ip_address_literal
from esphome.config_helpers import filter_source_files_from_platform from esphome.config_helpers import filter_source_files_from_platform
@@ -13,6 +14,7 @@ from esphome.const import (
CONF_DNS1, CONF_DNS1,
CONF_DNS2, CONF_DNS2,
CONF_DOMAIN, CONF_DOMAIN,
CONF_ENABLE_ON_BOOT,
CONF_GATEWAY, CONF_GATEWAY,
CONF_ID, CONF_ID,
CONF_INTERRUPT_PIN, CONF_INTERRUPT_PIN,
@@ -217,6 +219,10 @@ MANUAL_IP_SCHEMA = cv.Schema(
EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component) EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component)
ManualIP = ethernet_ns.struct("ManualIP") ManualIP = ethernet_ns.struct("ManualIP")
EthernetConnectedCondition = ethernet_ns.class_("EthernetConnectedCondition", Condition)
EthernetEnabledCondition = ethernet_ns.class_("EthernetEnabledCondition", Condition)
EthernetEnableAction = ethernet_ns.class_("EthernetEnableAction", automation.Action)
EthernetDisableAction = ethernet_ns.class_("EthernetDisableAction", automation.Action)
def _is_framework_spi_polling_mode_supported() -> bool: def _is_framework_spi_polling_mode_supported() -> bool:
@@ -348,6 +354,7 @@ BASE_SCHEMA = cv.Schema(
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True), cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True), cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True),
} }
@@ -494,6 +501,9 @@ async def to_code(config):
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]])) cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
# enable_on_boot defaults to true in C++ - only set if false
if not config[CONF_ENABLE_ON_BOOT]:
cg.add(var.set_enable_on_boot(False))
CORE.data.setdefault(KEY_ETHERNET, {})[ETHERNET_TYPE_KEY] = config[CONF_TYPE] CORE.data.setdefault(KEY_ETHERNET, {})[ETHERNET_TYPE_KEY] = config[CONF_TYPE]
if CONF_MANUAL_IP in config: if CONF_MANUAL_IP in config:
@@ -715,3 +725,21 @@ def _filter_source_files() -> list[str]:
FILTER_SOURCE_FILES = _filter_source_files FILTER_SOURCE_FILES = _filter_source_files
async def _new_pvariable_to_code(config, id_, template_arg, args):
return cg.new_Pvariable(id_, template_arg)
for _name, _cls in (
("ethernet.connected", EthernetConnectedCondition),
("ethernet.enabled", EthernetEnabledCondition),
):
automation.register_condition(_name, _cls, cv.Schema({}))(_new_pvariable_to_code)
for _name, _cls in (
("ethernet.enable", EthernetEnableAction),
("ethernet.disable", EthernetDisableAction),
):
automation.register_action(_name, _cls, cv.Schema({}), synchronous=True)(
_new_pvariable_to_code
)
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ETHERNET
#include "ethernet_component.h"
namespace esphome::ethernet {
template<typename... Ts> class EthernetConnectedCondition : public Condition<Ts...> {
public:
bool check(const Ts &...x) override { return global_eth_component->is_connected(); }
};
template<typename... Ts> class EthernetEnabledCondition : public Condition<Ts...> {
public:
bool check(const Ts &...x) override { return global_eth_component->is_enabled(); }
};
template<typename... Ts> class EthernetEnableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_eth_component->enable(); }
};
template<typename... Ts> class EthernetDisableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_eth_component->disable(); }
};
} // namespace esphome::ethernet
#endif
@@ -124,6 +124,17 @@ class EthernetComponent final : public Component {
void on_powerdown() override { powerdown(); } void on_powerdown() override { powerdown(); }
bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; } bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; }
// Per-interface lifecycle (parallels WiFiComponent::enable/disable/is_disabled).
// enable_on_boot defaults to true; when false, setup() runs all the driver/netif
// installation but skips esp_eth_start(), keeping the link cold until enable() is
// called. This is the primary lever for memory reclamation in multi-interface
// configurations where only one interface should carry traffic at a time.
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
void enable();
void disable();
bool is_disabled() { return this->disabled_; }
bool is_enabled() { return !this->disabled_; }
void set_type(EthernetType type); void set_type(EthernetType type);
#ifdef USE_ETHERNET_MANUAL_IP #ifdef USE_ETHERNET_MANUAL_IP
void set_manual_ip(const ManualIP &manual_ip); void set_manual_ip(const ManualIP &manual_ip);
@@ -194,6 +205,16 @@ class EthernetComponent final : public Component {
void finish_connect_(); void finish_connect_();
void dump_connect_params_(); void dump_connect_params_();
#ifdef USE_ESP32
// ESP-IDF only: defers the SPI bus init, netif creation, MAC/PHY install, driver
// install, netif attach, and event handler registration (which together allocate
// ~3-8KB of DMA-capable internal SRAM via SPI driver state + eth driver RX queue)
// until ethernet actually needs to come up. Idempotent — guarded by the
// ethernet_initialized_ flag. Called from setup() when enable_on_boot_=true, or
// from enable() on first runtime enable. Mirrors wifi_lazy_init_() in WiFi.
void ethernet_lazy_init_();
#endif
#ifdef USE_ETHERNET_IP_STATE_LISTENERS #ifdef USE_ETHERNET_IP_STATE_LISTENERS
void notify_ip_state_listeners_(); void notify_ip_state_listeners_();
#endif #endif
@@ -287,6 +308,17 @@ class EthernetComponent final : public Component {
bool started_{false}; bool started_{false};
bool connected_{false}; bool connected_{false};
bool got_ipv4_address_{false}; bool got_ipv4_address_{false};
// Codegen-time YAML option. When false, setup() defers esp_eth_start().
bool enable_on_boot_{true};
// Mirror of "is the link intentionally stopped" — set when setup() honors
// enable_on_boot=false, cleared by enable(), set again by disable().
bool disabled_{false};
#ifdef USE_ESP32
// Tracks whether ethernet_lazy_init_() has completed successfully. Allows enable()
// to be called at runtime after enable_on_boot:false without re-allocating, and
// ensures setup() skips the heavy init when enable_on_boot_ is false.
bool ethernet_initialized_{false};
#endif
#if LWIP_IPV6 #if LWIP_IPV6
uint8_t ipv6_count_{0}; uint8_t ipv6_count_{0};
bool ipv6_setup_done_{false}; bool ipv6_setup_done_{false};
@@ -138,6 +138,24 @@ void EthernetComponent::setup() {
delay(300); // NOLINT delay(300); // NOLINT
} }
if (this->enable_on_boot_) {
this->ethernet_lazy_init_();
if (!this->ethernet_initialized_) {
// lazy_init bailed early via ESPHL_ERROR_CHECK or mark_failed; nothing more to do.
return;
}
esp_err_t err = esp_eth_start(this->eth_handle_);
ESPHL_ERROR_CHECK(err, "ETH start error");
} else {
ESP_LOGCONFIG(TAG, "Skipping init (enable_on_boot: false)");
this->disabled_ = true;
}
}
void EthernetComponent::ethernet_lazy_init_() {
if (this->ethernet_initialized_)
return;
esp_err_t err; esp_err_t err;
#ifdef USE_ETHERNET_SPI #ifdef USE_ETHERNET_SPI
@@ -371,9 +389,41 @@ void EthernetComponent::setup() {
ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error"); ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error");
#endif /* USE_NETWORK_IPV6 */ #endif /* USE_NETWORK_IPV6 */
/* start Ethernet driver state machine */ this->ethernet_initialized_ = true;
err = esp_eth_start(this->eth_handle_); }
ESPHL_ERROR_CHECK(err, "ETH start error");
void EthernetComponent::enable() {
if (!this->disabled_)
return;
ESP_LOGD(TAG, "Enabling");
this->ethernet_lazy_init_();
if (!this->ethernet_initialized_) {
ESP_LOGE(TAG, "Cannot enable - init failed");
return;
}
esp_err_t err = esp_eth_start(this->eth_handle_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_eth_start failed: %s", esp_err_to_name(err));
return;
}
this->disabled_ = false;
// The ETH_EVENT_START handler will set started_=true; the loop state machine
// will then drive the STOPPED -> CONNECTING -> CONNECTED transitions.
this->enable_loop();
}
void EthernetComponent::disable() {
if (this->disabled_)
return;
ESP_LOGD(TAG, "Disabling");
esp_err_t err = esp_eth_stop(this->eth_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_eth_stop failed: %s — disabling anyway", esp_err_to_name(err));
}
this->disabled_ = true;
// ETH_EVENT_STOP will clear started_; loop() will transition to STOPPED.
} }
void EthernetComponent::dump_config() { void EthernetComponent::dump_config() {
@@ -487,6 +537,8 @@ void EthernetComponent::dump_config() {
network::IPAddresses EthernetComponent::get_ip_addresses() { network::IPAddresses EthernetComponent::get_ip_addresses() {
network::IPAddresses addresses; network::IPAddresses addresses;
if (!this->ethernet_initialized_)
return addresses; // all-zero IPs
esp_netif_ip_info_t ip; esp_netif_ip_info_t ip;
esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip); esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip);
if (err != ESP_OK) { if (err != ESP_OK) {
@@ -709,6 +761,10 @@ void EthernetComponent::start_connect_() {
} }
void EthernetComponent::dump_connect_params_() { void EthernetComponent::dump_connect_params_() {
if (!this->ethernet_initialized_) {
ESP_LOGCONFIG(TAG, " uninitialized/disabled");
return;
}
esp_netif_ip_info_t ip; esp_netif_ip_info_t ip;
esp_netif_get_ip_info(this->eth_netif_, &ip); esp_netif_get_ip_info(this->eth_netif_, &ip);
const ip_addr_t *dns_ip1; const ip_addr_t *dns_ip1;
@@ -776,6 +832,16 @@ void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy
#endif #endif
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
if (!this->ethernet_initialized_) {
// External callers (mdns, ethernet_info, etc.) may ask for the MAC before/regardless
// of whether ethernet is enabled. Use the configured MAC if set, else the system ETH MAC.
if (this->fixed_mac_.has_value()) {
memcpy(mac, this->fixed_mac_->data(), 6);
} else {
esp_read_mac(mac, ESP_MAC_ETH);
}
return;
}
esp_err_t err; esp_err_t err;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac); err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac);
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error"); ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
@@ -795,6 +861,8 @@ const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
} }
eth_duplex_t EthernetComponent::get_duplex_mode() { eth_duplex_t EthernetComponent::get_duplex_mode() {
if (!this->ethernet_initialized_)
return ETH_DUPLEX_HALF;
esp_err_t err; esp_err_t err;
eth_duplex_t duplex_mode; eth_duplex_t duplex_mode;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode); err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode);
@@ -803,6 +871,8 @@ eth_duplex_t EthernetComponent::get_duplex_mode() {
} }
eth_speed_t EthernetComponent::get_link_speed() { eth_speed_t EthernetComponent::get_link_speed() {
if (!this->ethernet_initialized_)
return ETH_SPEED_10M;
esp_err_t err; esp_err_t err;
eth_speed_t speed; eth_speed_t speed;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed); err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed);
@@ -361,6 +361,23 @@ void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; } void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
void EthernetComponent::enable() {
// RP2040 uses arduino-pico's LwipIntfDev which manages link state internally;
// there is no clean enable/disable hook today. The YAML option is accepted on
// RP2040 for schema parity but has no effect.
if (!this->disabled_)
return;
ESP_LOGW(TAG, "enable_on_boot/disable not supported");
this->disabled_ = false;
}
void EthernetComponent::disable() {
if (this->disabled_)
return;
ESP_LOGW(TAG, "enable_on_boot/disable not supported");
this->disabled_ = true;
}
} // namespace esphome::ethernet } // namespace esphome::ethernet
#endif // USE_ETHERNET && USE_RP2040 #endif // USE_ETHERNET && USE_RP2040
@@ -0,0 +1,39 @@
ethernet:
id: eth
type: W5500
clk_pin: 19
mosi_pin: 21
miso_pin: 23
cs_pin: 18
interrupt_pin: 36
reset_pin: 22
enable_on_boot: false
manual_ip:
static_ip: 192.168.178.56
gateway: 192.168.178.1
subnet: 255.255.255.0
mac_address: "02:AA:BB:CC:DD:01"
interface: spi2
esphome:
on_boot:
priority: 200
then:
- if:
condition:
not:
ethernet.enabled:
then:
- ethernet.enable:
button:
- platform: template
name: "Disable Ethernet"
on_press:
- ethernet.disable:
binary_sensor:
- platform: template
name: "Ethernet Connected"
lambda: |-
return id(eth).is_connected();