mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
feat(ethernet): Unit B + B+ — enable_on_boot lifecycle with lazy-init
Brings ethernet to feature parity with wifi's per-interface lifecycle, and applies the same lazy-init pattern as wifi's Unit B+ so enable_on_boot:false genuinely reclaims DMA-capable internal SRAM. Unit B — lifecycle API surface (mirrors WiFiComponent): - New `enable_on_boot: true` (default) YAML option on ethernet. - New set_enable_on_boot(), enable(), disable(), is_disabled(), is_enabled() methods on EthernetComponent. - ESP32 path: enable() calls esp_eth_start(), disable() calls esp_eth_stop(). - RP2040 path: stub methods that log a warning — arduino-pico's LwipIntfDev doesn't expose a clean start/stop hook; schema parity only. Unit B+ — lazy-init refactor (mirrors WiFiComponent::wifi_lazy_init_()): - New ethernet_lazy_init_() method (idempotent, guarded by ethernet_initialized_ flag) holds the entire heavy init body that used to live in setup(): SPI bus init, netif creation, MAC/PHY allocation, eth driver install, netif attach, event handler registration. - setup() becomes thin: 300ms power-stabilization delay, then if enable_on_boot_=true call lazy_init + esp_eth_start, else mark disabled_=true and return. - enable() calls ethernet_lazy_init_() first, then esp_eth_start() — so a runtime enable after enable_on_boot:false works end-to-end. Safe-default getter guards — external callers (sendspin, ethernet_info, mdns, etc.) may invoke MAC/IP/duplex queries before/regardless of whether ethernet is enabled. Without guards these call into esp_eth_ioctl(null, ...) and esp_netif_get_*(null, ...), producing error spam + erroneous mark_failed() calls during dump_config(): - get_eth_mac_address_raw() falls back to esp_read_mac(ESP_MAC_ETH) — the hardware MAC, same value the driver would have returned. - get_duplex_mode() returns ETH_DUPLEX_HALF. - get_link_speed() returns ETH_SPEED_10M. - get_ip_addresses() returns empty (zero) addresses. - dump_connect_params_() early-returns with "(uninitialized)" log line. For a user's reboot-to-toggle workflow with both interfaces declared but only one active per boot: the inactive interface costs zero DMA-capable memory. WiFi-side reclaims ~15-30KB DMA-capable, ethernet-side reclaims ~3-8KB (W5500 SPI driver is gentler than wifi). Field-tested on ESP32-S3 + W5500. Verifies clean dump_config() output and no false "ethernet was marked as failed" state when ethernet is dormant.
This commit is contained in:
@@ -17,6 +17,7 @@ from esphome.const import (
|
||||
CONF_DNS1,
|
||||
CONF_DNS2,
|
||||
CONF_DOMAIN,
|
||||
CONF_ENABLE_ON_BOOT,
|
||||
CONF_GATEWAY,
|
||||
CONF_ID,
|
||||
CONF_INTERRUPT_PIN,
|
||||
@@ -352,6 +353,7 @@ BASE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
|
||||
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
||||
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_DISCONNECT): automation.validate_automation(single=True),
|
||||
}
|
||||
@@ -503,6 +505,9 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
|
||||
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]
|
||||
|
||||
if CONF_MANUAL_IP in config:
|
||||
|
||||
@@ -124,6 +124,17 @@ class EthernetComponent final : public Component {
|
||||
void on_powerdown() override { powerdown(); }
|
||||
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);
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void set_manual_ip(const ManualIP &manual_ip);
|
||||
@@ -194,6 +205,16 @@ class EthernetComponent final : public Component {
|
||||
void finish_connect_();
|
||||
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
|
||||
void notify_ip_state_listeners_();
|
||||
#endif
|
||||
@@ -287,6 +308,17 @@ class EthernetComponent final : public Component {
|
||||
bool started_{false};
|
||||
bool connected_{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
|
||||
uint8_t ipv6_count_{0};
|
||||
bool ipv6_setup_done_{false};
|
||||
|
||||
@@ -137,6 +137,24 @@ void EthernetComponent::setup() {
|
||||
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;
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
@@ -366,9 +384,41 @@ void EthernetComponent::setup() {
|
||||
ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error");
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
/* start Ethernet driver state machine */
|
||||
err = esp_eth_start(this->eth_handle_);
|
||||
ESPHL_ERROR_CHECK(err, "ETH start error");
|
||||
this->ethernet_initialized_ = true;
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -482,6 +532,8 @@ void EthernetComponent::dump_config() {
|
||||
|
||||
network::IPAddresses EthernetComponent::get_ip_addresses() {
|
||||
network::IPAddresses addresses;
|
||||
if (!this->ethernet_initialized_)
|
||||
return addresses; // all-zero IPs
|
||||
esp_netif_ip_info_t ip;
|
||||
esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip);
|
||||
if (err != ESP_OK) {
|
||||
@@ -704,6 +756,10 @@ void EthernetComponent::start_connect_() {
|
||||
}
|
||||
|
||||
void EthernetComponent::dump_connect_params_() {
|
||||
if (!this->ethernet_initialized_) {
|
||||
ESP_LOGCONFIG(TAG, " uninitialized/disabled");
|
||||
return;
|
||||
}
|
||||
esp_netif_ip_info_t ip;
|
||||
esp_netif_get_ip_info(this->eth_netif_, &ip);
|
||||
const ip_addr_t *dns_ip1;
|
||||
@@ -771,6 +827,13 @@ void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy
|
||||
#endif
|
||||
|
||||
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
|
||||
if (!this->ethernet_initialized_) {
|
||||
// External callers (sendspin, ethernet_info, mdns, etc.) may ask for the MAC
|
||||
// before/regardless of whether ethernet is enabled. Fall back to the system MAC
|
||||
// assigned to the ETH interface — same value the driver would have returned.
|
||||
esp_read_mac(mac, ESP_MAC_ETH);
|
||||
return;
|
||||
}
|
||||
esp_err_t err;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac);
|
||||
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
|
||||
@@ -790,6 +853,8 @@ const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
|
||||
}
|
||||
|
||||
eth_duplex_t EthernetComponent::get_duplex_mode() {
|
||||
if (!this->ethernet_initialized_)
|
||||
return ETH_DUPLEX_HALF;
|
||||
esp_err_t err;
|
||||
eth_duplex_t duplex_mode;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode);
|
||||
@@ -798,6 +863,8 @@ eth_duplex_t EthernetComponent::get_duplex_mode() {
|
||||
}
|
||||
|
||||
eth_speed_t EthernetComponent::get_link_speed() {
|
||||
if (!this->ethernet_initialized_)
|
||||
return ETH_SPEED_10M;
|
||||
esp_err_t err;
|
||||
eth_speed_t 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_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
|
||||
|
||||
#endif // USE_ETHERNET && USE_RP2040
|
||||
|
||||
Reference in New Issue
Block a user