[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:
6 files changed
+219
-3
No files matched your search
@@ -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
|
||||||
|
)
|
||||||
@@ -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();
|
||||||
Reference in new issue
Block a user