mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 13:27:14 +00:00
feat(network): Unit A — explicit default-route management
Builds on PR #14012's NetworkComponent + PR #14255's priority list to make the user's stated interface priority actually drive runtime default-route selection. Without this, ESP-IDF's auto-selection picks the default netif by each netif's hardcoded `route_prio` field (WiFi STA = 100, Ethernet = 50, WiFi AP = 10) — which inverts the user's intent on same-subnet multi-homing configurations where wifi+ethernet share a broadcast domain. Changes: - NetworkComponent gains an IP_EVENT handler registered in setup() that re-arbitrates the default netif on every interface up/down. The handler walks the priority list in order, picks the highest-priority netif that is up, and calls esp_netif_set_default_netif() on it. ESP-IDF then sets its internal "manual override" flag so subsequent auto-selection events don't undo our choice. - New StaticVector<NetworkPriorityEntry, 4> stores the priority list with zero heap allocation. The interface-name string pointer is a YAML literal with static storage duration. - The timeout_ms field is parsed and stored but not yet consumed by Unit A; it's wired up for Unit D (runtime timeout fallback). - New getters get_active_interface() / get_active_netif() expose the currently-active interface for Unit C consumers. - Python codegen iterates CORE.data[KEY_NETWORK_PRIORITY] and emits add_priority_entry() calls per YAML order. Field-tested on ESP32-S3 with W5500 SPI ethernet + WiFi STA on the same subnet. The log line "[network] Default interface: <name>" confirms the arbitration logic fires correctly on IP_EVENT_*_GOT_IP. Standalone — no schema changes, single-interface configs unaffected.
This commit is contained in:
@@ -456,3 +456,10 @@ async def to_code(config):
|
||||
async def network_component_to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
# Pass the priority list to the C++ component. NetworkComponent::add_priority_entry
|
||||
# captures the interface-name string literal pointer; CORE.data[KEY_NETWORK_PRIORITY]
|
||||
# holds the normalized list of dicts (`{"interface": str, "timeout": int|None}`).
|
||||
for entry in CORE.data.get(KEY_NETWORK_PRIORITY, []):
|
||||
timeout_ms = entry["timeout"] if entry["timeout"] is not None else 0
|
||||
cg.add(var.add_priority_entry(entry["interface"], timeout_ms))
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_NETWORK) && defined(USE_ESP32)
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
|
||||
namespace esphome::network {
|
||||
|
||||
static const char *const TAG = "network";
|
||||
@@ -27,6 +31,88 @@ void NetworkComponent::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Register an IP_EVENT handler so we can re-arbitrate the default netif on every
|
||||
// interface up/down. ESP-IDF's built-in auto-selection picks by route_prio (WiFi STA = 100
|
||||
// > Ethernet = 50), which inverts the user's stated priority for same-subnet configurations.
|
||||
// We register AFTER esp-idf's internal handler, so our esp_netif_set_default_netif() call
|
||||
// wins and stays sticky thanks to esp-idf's "manual override" flag.
|
||||
err = esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &NetworkComponent::event_handler_, this);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "IP_EVENT handler register failed: %s — default route arbitration disabled",
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// Defensive: arbitrate now in case an interface came up before our handler registered
|
||||
// (unlikely given our AFTER_BLUETOOTH priority but cheap).
|
||||
this->update_default_netif_();
|
||||
}
|
||||
|
||||
void NetworkComponent::add_priority_entry(const char *interface, uint32_t timeout_ms) {
|
||||
if (this->priority_list_.size() >= MAX_NETWORK_PRIORITY_ENTRIES) {
|
||||
ESP_LOGW(TAG, "Priority list full; ignoring '%s'", interface);
|
||||
return;
|
||||
}
|
||||
this->priority_list_.push_back({interface, timeout_ms});
|
||||
}
|
||||
|
||||
const char *NetworkComponent::interface_to_ifkey_(const char *interface) {
|
||||
// Standard ESP-IDF netif keys. esphome's wifi/ethernet/openthread components create
|
||||
// netifs using these defaults.
|
||||
if (std::strcmp(interface, "ethernet") == 0)
|
||||
return "ETH_DEF";
|
||||
if (std::strcmp(interface, "wifi") == 0)
|
||||
return "WIFI_STA_DEF"; // STA carries uplink; AP never wins default route
|
||||
if (std::strcmp(interface, "openthread") == 0)
|
||||
return "OT_DEF";
|
||||
if (std::strcmp(interface, "modem") == 0)
|
||||
return "PPP_DEF";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void NetworkComponent::event_handler_(void *arg, esp_event_base_t /*base*/, int32_t /*id*/, void * /*data*/) {
|
||||
auto *self = static_cast<NetworkComponent *>(arg);
|
||||
self->update_default_netif_();
|
||||
}
|
||||
|
||||
void NetworkComponent::update_default_netif_() {
|
||||
// No priority list configured → leave ESP-IDF's route_prio-based auto-selection alone.
|
||||
// Single-interface configs behave exactly as before.
|
||||
if (this->priority_list_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &entry : this->priority_list_) {
|
||||
const char *ifkey = interface_to_ifkey_(entry.interface);
|
||||
if (ifkey == nullptr)
|
||||
continue;
|
||||
|
||||
esp_netif_t *netif = esp_netif_get_handle_from_ifkey(ifkey);
|
||||
if (netif == nullptr)
|
||||
continue; // component for this interface hasn't run setup() yet
|
||||
|
||||
// is_netif_up returns true only when the netif has link + IP, which is what
|
||||
// we want for "this interface can carry traffic right now."
|
||||
if (!esp_netif_is_netif_up(netif))
|
||||
continue;
|
||||
|
||||
if (netif != this->active_netif_) {
|
||||
ESP_LOGI(TAG, "Default interface: %s", entry.interface);
|
||||
esp_netif_set_default_netif(netif);
|
||||
this->active_interface_ = entry.interface;
|
||||
this->active_netif_ = netif;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// No priority-listed interface is currently up.
|
||||
if (this->active_netif_ != nullptr) {
|
||||
ESP_LOGD(TAG, "No active interface in priority list");
|
||||
this->active_interface_ = nullptr;
|
||||
this->active_netif_ = nullptr;
|
||||
// We intentionally don't clear esp-idf's default — the next interface that comes
|
||||
// up will trigger our handler again and we'll re-pick.
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::network
|
||||
|
||||
@@ -2,13 +2,53 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_NETWORK) && defined(USE_ESP32)
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
|
||||
namespace esphome::network {
|
||||
|
||||
// Cap matches the number of interface types the priority list accepts in YAML
|
||||
// (ethernet, wifi, openthread, modem). StaticVector keeps zero heap allocation.
|
||||
inline constexpr size_t MAX_NETWORK_PRIORITY_ENTRIES = 4;
|
||||
|
||||
struct NetworkPriorityEntry {
|
||||
const char *interface; // YAML name: "ethernet", "wifi", "openthread", "modem"
|
||||
uint32_t timeout_ms; // 0 = no timeout; consumed by Unit D (runtime fallback)
|
||||
};
|
||||
|
||||
class NetworkComponent : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
// AFTER_BLUETOOTH: BLE controller must initialize before esp_netif_init per IDF guidance.
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_BLUETOOTH; }
|
||||
|
||||
// Codegen-time priority list construction. Called once per `network: priority:` entry
|
||||
// in YAML order. The interface name pointer must have static storage duration.
|
||||
void add_priority_entry(const char *interface, uint32_t timeout_ms);
|
||||
|
||||
// Currently-active interface in priority order (the one set as default netif).
|
||||
// Returns nullptr if no priority list is configured or no interface is up.
|
||||
const char *get_active_interface() const { return this->active_interface_; }
|
||||
esp_netif_t *get_active_netif() const { return this->active_netif_; }
|
||||
|
||||
protected:
|
||||
// Maps a YAML interface name to its ESP-IDF netif if-key.
|
||||
// Returns nullptr if the interface name is not recognized.
|
||||
static const char *interface_to_ifkey_(const char *interface);
|
||||
|
||||
// ESP-IDF event handler trampoline. Fires on IP_EVENT_* and re-arbitrates the default netif.
|
||||
static void event_handler_(void *arg, esp_event_base_t base, int32_t id, void *data);
|
||||
|
||||
// Walk priority_list_ in order. Set the highest-priority netif that is up as the
|
||||
// ESP-IDF default. No-op if priority_list_ is empty (single-interface configs).
|
||||
void update_default_netif_();
|
||||
|
||||
StaticVector<NetworkPriorityEntry, MAX_NETWORK_PRIORITY_ENTRIES> priority_list_;
|
||||
const char *active_interface_{nullptr};
|
||||
esp_netif_t *active_netif_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::network
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user