Compare commits

...
Author SHA1 Message Date
J. Nick Koston 61ed44da1f Merge branch 'dev' into ethernet-manual-ip-progmem 2026-10-26 21:33:13 -10:00
J. Nick Koston de9449841e [ethernet] Check the manual IP table layout
The extern declaration now has its size, so a longer generated table fails to compile, and a codegen test checks the 20 octets and their order.
2026-10-26 18:38:27 -10:00
J. Nick Koston 0d7ff93bd3 [ethernet] Keep manual IP settings in flash
USE_ETHERNET_MANUAL_IP is only defined when manual_ip is set and ethernet has one instance, so the optional<ManualIP> member goes away; the settings are a PROGMEM table read on the stack when they are applied.
2026-10-26 18:18:49 -10:00
7 changed files with 119 additions and 81 deletions

No files matched your search

+9 -12
View File
@@ -7,7 +7,6 @@ from esphome.components.network import (
add_use_address,
get_network_priority,
get_priority_interfaces_from_full_config,
ip_address_literal,
require_ipv4,
)
from esphome.config_helpers import (
@@ -239,7 +238,6 @@ MANUAL_IP_SCHEMA = cv.Schema(
)
EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component)
ManualIP = ethernet_ns.struct("ManualIP")
def _is_framework_spi_polling_mode_supported() -> bool:
@@ -625,15 +623,10 @@ def _final_validate_spi(config: ConfigType) -> None:
)
def manual_ip(config: ConfigType) -> cg.StructInitializer:
return cg.StructInitializer(
ManualIP,
("static_ip", ip_address_literal(config[CONF_STATIC_IP])),
("gateway", ip_address_literal(config[CONF_GATEWAY])),
("subnet", ip_address_literal(config[CONF_SUBNET])),
("dns1", ip_address_literal(config[CONF_DNS1])),
("dns2", ip_address_literal(config[CONF_DNS2])),
)
def _manual_ip_table(config: ConfigType) -> list[int]:
"""Pack manual_ip in the ETHERNET_MANUAL_IP layout."""
keys = (CONF_STATIC_IP, CONF_GATEWAY, CONF_SUBNET, CONF_DNS1, CONF_DNS2)
return [octet for key in keys for octet in config[key].packed]
def phy_register(address: int, value: int, page: int) -> cg.StructInitializer:
@@ -685,7 +678,11 @@ async def to_code(config: ConfigType) -> None:
if CONF_MANUAL_IP in config:
cg.add_define("USE_ETHERNET_MANUAL_IP")
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
cg.extern_progmem_array(
"esphome::ethernet::ETHERNET_MANUAL_IP",
cg.uint8,
_manual_ip_table(config[CONF_MANUAL_IP]),
)
# Add compile-time define for PHY types with specific code
if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]):
@@ -10,6 +10,15 @@ EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-
EthernetComponent::EthernetComponent() { global_eth_component = this; }
#ifdef USE_ETHERNET_MANUAL_IP
ManualIP load_manual_ip() {
uint8_t b[ETHERNET_MANUAL_IP_SIZE];
progmem_memcpy(b, ETHERNET_MANUAL_IP, sizeof(b));
auto ip = [&b](size_t i) { return network::IPAddress(b[i], b[i + 1], b[i + 2], b[i + 3]); };
return ManualIP{ip(0), ip(4), ip(8), ip(12), ip(16)};
}
#endif
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void EthernetComponent::notify_ip_state_listeners_() {
auto ips = this->get_ip_addresses();
@@ -103,6 +103,14 @@ struct ManualIP {
network::IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default.
};
#ifdef USE_ETHERNET_MANUAL_IP
/// The configured static IP, gateway, subnet, dns1 and dns2 as 4 octets each, generated by codegen
/// (components/ethernet/__init__.py) and kept in flash.
static constexpr size_t ETHERNET_MANUAL_IP_SIZE = 5 * 4;
extern const uint8_t ETHERNET_MANUAL_IP[ETHERNET_MANUAL_IP_SIZE] PROGMEM;
ManualIP load_manual_ip();
#endif
struct PHYRegister {
uint32_t address;
uint32_t value;
@@ -151,9 +159,6 @@ class EthernetComponent final : public Component {
#endif
void set_type(EthernetType type) { this->type_ = type; }
#ifdef USE_ETHERNET_MANUAL_IP
void set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
#endif
void set_fixed_mac(const std::array<uint8_t, MAC_ADDRESS_SIZE> &mac) { this->fixed_mac_ = mac; }
network::IPAddresses get_ip_addresses();
@@ -333,9 +338,6 @@ class EthernetComponent final : public Component {
#endif // USE_RP2
// Common members
#ifdef USE_ETHERNET_MANUAL_IP
optional<ManualIP> manual_ip_{};
#endif
uint32_t connect_begin_;
// Group all uint8_t types together (enums and bools)
@@ -763,9 +763,7 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
event_name = "ETH connected";
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
if (global_eth_component->manual_ip_.has_value()) {
global_eth_component->notify_ip_state_listeners_();
}
global_eth_component->notify_ip_state_listeners_();
#endif
#if USE_NETWORK_IPV6
// Start SLAAC on link-up, not after the DHCPv4 lease. This also restores the
@@ -889,19 +887,13 @@ void EthernetComponent::start_connect_() {
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
}
esp_netif_ip_info_t info;
esp_netif_ip_info_t info{};
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
info.ip = this->manual_ip_->static_ip;
info.gw = this->manual_ip_->gateway;
info.netmask = this->manual_ip_->subnet;
} else
const ManualIP manual_ip = load_manual_ip();
info.ip = manual_ip.static_ip;
info.gw = manual_ip.gateway;
info.netmask = manual_ip.subnet;
#endif
{
info.ip.addr = 0;
info.gw.addr = 0;
info.netmask.addr = 0;
}
esp_netif_dhcp_status_t status = ESP_NETIF_DHCP_INIT;
@@ -919,35 +911,32 @@ void EthernetComponent::start_connect_() {
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
// Set DNS through esp_netif so the servers are stored in the netif's own
// dns[] array; raw dns_setserver() would be lost when the default-route
// arbitration re-applies the default netif's DNS.
// Log-only on failure: the link still has a working IP/gateway, so degraded
// name resolution does not justify marking the whole component failed.
esp_netif_dns_info_t dns{};
if (this->manual_ip_->dns1.is_set()) {
dns.ip = this->manual_ip_->dns1;
err = esp_netif_set_dns_info(this->eth_netif_, ESP_NETIF_DNS_MAIN, &dns);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Set main DNS failed: %s", esp_err_to_name(err));
}
}
if (this->manual_ip_->dns2.is_set()) {
dns.ip = this->manual_ip_->dns2;
err = esp_netif_set_dns_info(this->eth_netif_, ESP_NETIF_DNS_BACKUP, &dns);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Set backup DNS failed: %s", esp_err_to_name(err));
}
}
} else
#endif
{
err = esp_netif_dhcpc_start(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
ESPHL_ERROR_CHECK(err, "DHCPC start error");
// Set DNS through esp_netif so the servers are stored in the netif's own
// dns[] array; raw dns_setserver() would be lost when the default-route
// arbitration re-applies the default netif's DNS.
// Log-only on failure: the link still has a working IP/gateway, so degraded
// name resolution does not justify marking the whole component failed.
esp_netif_dns_info_t dns{};
if (manual_ip.dns1.is_set()) {
dns.ip = manual_ip.dns1;
err = esp_netif_set_dns_info(this->eth_netif_, ESP_NETIF_DNS_MAIN, &dns);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Set main DNS failed: %s", esp_err_to_name(err));
}
}
if (manual_ip.dns2.is_set()) {
dns.ip = manual_ip.dns2;
err = esp_netif_set_dns_info(this->eth_netif_, ESP_NETIF_DNS_BACKUP, &dns);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Set backup DNS failed: %s", esp_err_to_name(err));
}
}
#else
err = esp_netif_dhcpc_start(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
ESPHL_ERROR_CHECK(err, "DHCPC start error");
}
#endif
#if USE_NETWORK_IPV6
// Attempt to create IPv6 link-local address
// We MUST attempt this here, not just in finish_connect_(), because with
@@ -56,15 +56,12 @@ void EthernetComponent::setup() {
// Set hostname before begin() so the LWIP netif gets it
this->eth_->hostname(App.get_name().c_str());
// Configure static IP if set (must be done before begin())
// Configure static IP (must be done before begin())
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
IPAddress ip(this->manual_ip_->static_ip);
IPAddress gateway(this->manual_ip_->gateway);
IPAddress subnet(this->manual_ip_->subnet);
IPAddress dns1(this->manual_ip_->dns1);
IPAddress dns2(this->manual_ip_->dns2);
this->eth_->config(ip, gateway, subnet, dns1, dns2);
{
const ManualIP manual_ip = load_manual_ip();
this->eth_->config(IPAddress(manual_ip.static_ip), IPAddress(manual_ip.gateway), IPAddress(manual_ip.subnet),
IPAddress(manual_ip.dns1), IPAddress(manual_ip.dns2));
}
#endif
@@ -290,20 +287,17 @@ void EthernetComponent::start_connect_() {
// Hostname is already set in setup() via LwipIntf::setHostname()
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
// Static IP was already configured before begin() in setup()
// Set DNS servers
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns1;
dns_setserver(0, &d);
}
if (this->manual_ip_->dns2.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns2;
dns_setserver(1, &d);
}
// Static IP was already configured before begin() in setup()
// Set DNS servers
const ManualIP manual_ip = load_manual_ip();
LwIPLock lock;
if (manual_ip.dns1.is_set()) {
ip_addr_t d = manual_ip.dns1;
dns_setserver(0, &d);
}
if (manual_ip.dns2.is_set()) {
ip_addr_t d = manual_ip.dns2;
dns_setserver(1, &d);
}
#endif
}
@@ -0,0 +1,18 @@
esphome:
name: test
esp32:
board: esp32dev
ethernet:
type: LAN8720
mdc_pin: GPIO23
mdio_pin: GPIO18
clk:
mode: CLK_EXT_IN
pin: GPIO0
manual_ip:
static_ip: 192.168.1.50
gateway: 192.168.1.1
subnet: 255.255.255.0
dns1: 1.1.1.1
@@ -0,0 +1,29 @@
"""The manual IP table codegen emits must match the ETHERNET_MANUAL_IP layout in ethernet_component.h."""
from collections.abc import Callable
from pathlib import Path
def test_manual_ip_table_layout(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Twenty octets: static IP, gateway, subnet, dns1, dns2 (unset DNS is zero)."""
main_cpp = generate_main(component_config_path("manual_ip.yaml"))
expected = (
"192, 168, 1, 50, 192, 168, 1, 1, 255, 255, 255, 0, 1, 1, 1, 1, 0, 0, 0, 0"
)
assert (
f"esphome::ethernet::ETHERNET_MANUAL_IP[] PROGMEM = {{{expected}}};" in main_cpp
)
assert "set_manual_ip(" not in main_cpp
def test_no_manual_ip_emits_no_table(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("rmii_defaults.yaml"))
assert "ETHERNET_MANUAL_IP" not in main_cpp