diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 83bd09b643..313818e601 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "crash_handler.h" +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -15,7 +16,6 @@ #include void setup(); // NOLINT(readability-redundant-declaration) -void loop(); // NOLINT(readability-redundant-declaration) // Weak stub for initArduino - overridden when the Arduino component is present extern "C" __attribute__((weak)) void initArduino() {} @@ -65,7 +65,7 @@ TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non- void loop_task(void *pv_params) { setup(); while (true) { - loop(); + App.loop(); } } diff --git a/esphome/components/esp32/helpers.cpp b/esphome/components/esp32/helpers.cpp index 654a35b473..afcec8bfc7 100644 --- a/esphome/components/esp32/helpers.cpp +++ b/esphome/components/esp32/helpers.cpp @@ -9,11 +9,14 @@ #include #include +#include "esphome/core/log.h" #include "esp_random.h" #include "esp_system.h" namespace esphome { +static const char *const TAG = "esp32"; + bool random_bytes(uint8_t *data, size_t len) { esp_fill_random(data, len); return true; @@ -63,22 +66,43 @@ LwIPLock::~LwIPLock() { #endif } +/// Read MAC and validate both the return code and content. +static bool read_valid_mac(uint8_t *mac, esp_err_t err) { return err == ESP_OK && mac_address_is_valid(mac); } + +static constexpr size_t MAC_ADDRESS_SIZE_BITS = MAC_ADDRESS_SIZE * 8; // 48 bits + void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) #if defined(CONFIG_SOC_IEEE802154_SUPPORTED) // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead. - if (has_custom_mac_address()) { - esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48); - } else { - esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48); + // Both paths already read raw eFuse bytes, so there is no CRC-bypass fallback + // (unlike the non-IEEE802154 path where esp_efuse_mac_get_default does CRC checks). + if (has_custom_mac_address() && + read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS))) { + return; + } + if (read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, MAC_ADDRESS_SIZE_BITS))) { + return; } #else - if (has_custom_mac_address()) { - esp_efuse_mac_get_custom(mac); - } else { - esp_efuse_mac_get_default(mac); + if (has_custom_mac_address() && read_valid_mac(mac, esp_efuse_mac_get_custom(mac))) { + return; + } + if (read_valid_mac(mac, esp_efuse_mac_get_default(mac))) { + return; + } + // Default MAC read failed (e.g., eFuse CRC error) - try reading raw eFuse bytes + // directly, bypassing CRC validation. A MAC that passes mac_address_is_valid() + // (non-zero, non-broadcast, unicast) is almost certainly the real factory MAC + // with a corrupted CRC byte, which is far better than returning garbage or zeros. + if (read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, MAC_ADDRESS_SIZE_BITS))) { + ESP_LOGW(TAG, "eFuse MAC CRC failed but raw bytes appear valid - using raw eFuse MAC"); + return; } #endif + // All methods failed - zero the MAC rather than returning garbage + ESP_LOGE(TAG, "Failed to read a valid MAC address from eFuse"); + memset(mac, 0, MAC_ADDRESS_SIZE); } void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); } @@ -88,9 +112,11 @@ bool has_custom_mac_address() { uint8_t mac[6]; // do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails #ifndef USE_ESP32_VARIANT_ESP32 - return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); + return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS) == ESP_OK) && + mac_address_is_valid(mac); #else - return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); + return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS) == ESP_OK) && + mac_address_is_valid(mac); #endif #else return false; diff --git a/esphome/components/packet_transport/__init__.py b/esphome/components/packet_transport/__init__.py index 1930e45e85..0b166bb65c 100644 --- a/esphome/components/packet_transport/__init__.py +++ b/esphome/components/packet_transport/__init__.py @@ -177,13 +177,19 @@ async def register_packet_transport(var, config): cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) is_provider = False - for sens_conf in config.get(CONF_SENSORS, ()): + sensors = config.get(CONF_SENSORS, ()) + binary_sensors = config.get(CONF_BINARY_SENSORS, ()) + if sensors: + cg.add(var.set_sensor_count(len(sensors))) + if binary_sensors: + cg.add(var.set_binary_sensor_count(len(binary_sensors))) + for sens_conf in sensors: is_provider = True sens_id = sens_conf[CONF_ID] sensor = await cg.get_variable(sens_id) bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) cg.add(var.add_sensor(bcst_id, sensor)) - for sens_conf in config.get(CONF_BINARY_SENSORS, ()): + for sens_conf in binary_sensors: is_provider = True sens_id = sens_conf[CONF_ID] sensor = await cg.get_variable(sens_id) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 964037a02c..a2199977aa 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -221,16 +221,20 @@ void PacketTransport::setup() { } #ifdef USE_SENSOR for (auto &sensor : this->sensors_) { - sensor.sensor->add_on_state_callback([this, &sensor](float x) { - this->updated_ = true; + // [&sensor] is safe: sensor refers to a FixedVector element that never reallocates, + // so the reference remains valid for the component's lifetime. + sensor.sensor->add_on_state_callback([&sensor](float x) { + sensor.parent->updated_ = true; sensor.updated = true; }); } #endif #ifdef USE_BINARY_SENSOR for (auto &sensor : this->binary_sensors_) { - sensor.sensor->add_on_state_callback([this, &sensor](bool value) { - this->updated_ = true; + // [&sensor] is safe: sensor refers to a FixedVector element that never reallocates, + // so the reference remains valid for the component's lifetime. + sensor.sensor->add_on_state_callback([&sensor](bool value) { + sensor.parent->updated_ = true; sensor.updated = true; }); } @@ -548,11 +552,11 @@ void PacketTransport::dump_config() { " Ping-pong: %s", this->platform_name_, YESNO(this->is_encrypted_()), YESNO(this->ping_pong_enable_)); #ifdef USE_SENSOR - for (auto sensor : this->sensors_) + for (const auto &sensor : this->sensors_) ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id); #endif #ifdef USE_BINARY_SENSOR - for (auto sensor : this->binary_sensors_) + for (const auto &sensor : this->binary_sensors_) ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.id); #endif for (const auto &host : this->providers_) { diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index b3798738e2..836775bc85 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/core/preferences.h" #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" @@ -37,11 +38,14 @@ struct Provider { #endif }; +class PacketTransport; + #ifdef USE_SENSOR struct Sensor { sensor::Sensor *sensor; const char *id; bool updated; + PacketTransport *parent; }; #endif #ifdef USE_BINARY_SENSOR @@ -49,6 +53,7 @@ struct BinarySensor { binary_sensor::BinarySensor *sensor; const char *id; bool updated; + PacketTransport *parent; }; #endif @@ -60,8 +65,9 @@ class PacketTransport : public PollingComponent { void dump_config() override; #ifdef USE_SENSOR + void set_sensor_count(size_t count) { this->sensors_.init(count); } void add_sensor(const char *id, sensor::Sensor *sensor) { - Sensor st{sensor, id, true}; + Sensor st{sensor, id, true, this}; this->sensors_.push_back(st); } void add_remote_sensor(const char *hostname, const char *remote_id, sensor::Sensor *sensor) { @@ -70,8 +76,9 @@ class PacketTransport : public PollingComponent { } #endif #ifdef USE_BINARY_SENSOR + void set_binary_sensor_count(size_t count) { this->binary_sensors_.init(count); } void add_binary_sensor(const char *id, binary_sensor::BinarySensor *sensor) { - BinarySensor st{sensor, id, true}; + BinarySensor st{sensor, id, true, this}; this->binary_sensors_.push_back(st); } @@ -141,11 +148,11 @@ class PacketTransport : public PollingComponent { std::vector encryption_key_{}; #ifdef USE_SENSOR - std::vector sensors_{}; + FixedVector sensors_{}; string_map_t> remote_sensors_{}; #endif #ifdef USE_BINARY_SENSOR - std::vector binary_sensors_{}; + FixedVector binary_sensors_{}; string_map_t> remote_binary_sensors_{}; #endif diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index a21bd64730..226a669e31 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -125,7 +125,7 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s /// On ESP8266, uses esp_delay() with a callback that checks socket activity. /// On RP2040, uses __wfe() (Wait For Event) to truly sleep until an interrupt /// (for example, CYW43 GPIO or a timer alarm) fires and wakes the CPU. -void socket_delay(uint32_t ms); +void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) /// Signal socket/IO activity and wake the main loop early. /// On ESP8266: sets flag + esp_schedule(). diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 08df385475..c020a8ed58 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -12,21 +12,11 @@ #endif #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" -#ifdef USE_ESP32 -#include -#include -#else -#include -#include -#endif #endif // USE_LWIP_FAST_SELECT #include "esphome/core/version.h" #include "esphome/core/hal.h" #include #include -#ifdef USE_RUNTIME_STATS -#include "esphome/components/runtime_stats/runtime_stats.h" -#endif #ifdef USE_STATUS_LED #include "esphome/components/status_led/status_led.h" @@ -163,66 +153,6 @@ void Application::setup() { this->schedule_dump_config(); } -void Application::loop() { - uint8_t new_app_state = 0; - - // Get the initial loop time at the start - uint32_t last_op_end_time = millis(); - - this->before_loop_tasks_(last_op_end_time); - - for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; - this->current_loop_index_++) { - Component *component = this->looping_components_[this->current_loop_index_]; - - // Update the cached time before each component runs - this->loop_component_start_time_ = last_op_end_time; - - { - this->set_current_component(component); - WarnIfComponentBlockingGuard guard{component, last_op_end_time}; - component->loop(); - // Use the finish method to get the current time as the end time - last_op_end_time = guard.finish(); - } - new_app_state |= component->get_component_state(); - this->app_state_ |= new_app_state; - this->feed_wdt(last_op_end_time); - } - - this->after_loop_tasks_(); - this->app_state_ = new_app_state; - -#ifdef USE_RUNTIME_STATS - // Process any pending runtime stats printing after all components have run - // This ensures stats printing doesn't affect component timing measurements - if (global_runtime_stats != nullptr) { - global_runtime_stats->process_pending_stats(last_op_end_time); - } -#endif - - // Use the last component's end time instead of calling millis() again - auto elapsed = last_op_end_time - this->last_loop_; - if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { - // Even if we overran the loop interval, we still need to select() - // to know if any sockets have data ready - this->yield_with_select_(0); - } else { - uint32_t delay_time = this->loop_interval_ - elapsed; - uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time); - // next_schedule is max 0.5*delay_time - // otherwise interval=0 schedules result in constant looping with almost no sleep - next_schedule = std::max(next_schedule, delay_time / 2); - delay_time = std::min(next_schedule, delay_time); - - this->yield_with_select_(delay_time); - } - this->last_loop_ = last_op_end_time; - - if (this->dump_config_at_ < this->components_.size()) { - this->process_dump_config_(); - } -} void Application::process_dump_config_() { if (this->dump_config_at_ == 0) { @@ -509,41 +439,6 @@ void Application::enable_pending_loops_() { } } -void Application::before_loop_tasks_(uint32_t loop_start_time) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) - // Drain wake notifications first to clear socket for next wake - this->drain_wake_notifications_(); -#endif - - // Process scheduled tasks - this->scheduler.call(loop_start_time); - - // Feed the watchdog timer - this->feed_wdt(loop_start_time); - - // Process any pending enable_loop requests from ISRs - // This must be done before marking in_loop_ = true to avoid race conditions - if (this->has_pending_enable_loop_requests_) { - // Clear flag BEFORE processing to avoid race condition - // If ISR sets it during processing, we'll catch it next loop iteration - // This is safe because: - // 1. Each component has its own pending_enable_loop_ flag that we check - // 2. If we can't process a component (wrong state), enable_pending_loops_() - // will set this flag back to true - // 3. Any new ISR requests during processing will set the flag again - this->has_pending_enable_loop_requests_ = false; - this->enable_pending_loops_(); - } - - // Mark that we're in the loop for safe reentrant modifications - this->in_loop_ = true; -} - -void Application::after_loop_tasks_() { - // Clear the in_loop_ flag to indicate we're done processing components - this->in_loop_ = false; -} - #ifdef USE_LWIP_FAST_SELECT bool Application::register_socket(struct lwip_sock *sock) { // It modifies monitored_sockets_ without locking — must only be called from the main loop. @@ -625,36 +520,10 @@ void Application::unregister_socket_fd(int fd) { #endif +// Only the select() fallback path remains in the .cpp — all other paths are inlined in application.h +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) void Application::yield_with_select_(uint32_t delay_ms) { - // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run. -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) - // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. - // Safe because this runs on the main loop which owns socket lifetime (create, read, close). - if (delay_ms == 0) [[unlikely]] { - yield(); - return; - } - - // Check if any socket already has pending data before sleeping. - // If a socket still has unread data (rcvevent > 0) but the task notification was already - // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. - // This scan preserves select() semantics: return immediately when any fd is ready. - for (struct lwip_sock *sock : this->monitored_sockets_) { - if (esphome_lwip_socket_has_data(sock)) { - yield(); - return; - } - } - - // Sleep with instant wake via FreeRTOS task notification. - // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. - // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — - // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). - ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); - -#elif defined(USE_SOCKET_SELECT_SUPPORT) // Fallback select() path (host platform and any future platforms without fast select). - // ESP32 and LibreTiny are excluded by the #if above — they use the fast path. if (!this->socket_fds_.empty()) [[likely]] { // Update fd_set if socket list has changed if (this->socket_fds_changed_) [[unlikely]] { @@ -701,16 +570,8 @@ void Application::yield_with_select_(uint32_t delay_ms) { } // No sockets registered or select() failed - use regular delay delay(delay_ms); -#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) - // No select support but can wake on socket activity - // ESP8266: via esp_schedule() - // RP2040: via __sev()/__wfe() hardware sleep/wake - socket::socket_delay(delay_ms); -#else - // No select support, use regular delay - delay(delay_ms); -#endif } +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) // App storage — asm label shares the linker symbol with "extern Application App". // char[] is trivially destructible, so no __cxa_atexit or destructor chain is emitted. diff --git a/esphome/core/application.h b/esphome/core/application.h index 26abc15433..06ff30e81f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -27,6 +27,13 @@ #ifdef USE_SOCKET_SELECT_SUPPORT #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" +#ifdef USE_ESP32 +#include +#include +#else +#include +#include +#endif #else #include #ifdef USE_WAKE_LOOP_THREADSAFE @@ -34,9 +41,13 @@ #endif #endif #endif // USE_SOCKET_SELECT_SUPPORT +#ifdef USE_RUNTIME_STATS +#include "esphome/components/runtime_stats/runtime_stats.h" +#endif #if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) namespace esphome::socket { -void socket_wake(); // NOLINT(readability-redundant-declaration) +void socket_wake(); // NOLINT(readability-redundant-declaration) +void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) } // namespace esphome::socket #endif #ifdef USE_BINARY_SENSOR @@ -293,7 +304,7 @@ class Application { void setup(); /// Make a loop iteration. Call this in your loop() function. - void loop(); + inline void ESPHOME_ALWAYS_INLINE loop(); /// Get the name of this Application set by pre_setup(). const StringRef &get_name() const { return this->name_; } @@ -617,8 +628,8 @@ class Application { void enable_component_loop_(Component *component); void enable_pending_loops_(); void activate_looping_component_(uint16_t index); - void before_loop_tasks_(uint32_t loop_start_time); - void after_loop_tasks_(); + inline void ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time); + inline void ESPHOME_ALWAYS_INLINE after_loop_tasks_() { this->in_loop_ = false; } /// Process dump_config output one component per loop iteration. /// Extracted from loop() to keep cold startup/reconnect logging out of the hot path. @@ -628,7 +639,12 @@ class Application { void feed_wdt_arch_(); /// Perform a delay while also monitoring socket file descriptors for readiness +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) + // select() fallback path is too complex to inline (host platform) void yield_with_select_(uint32_t delay_ms); +#else + inline void ESPHOME_ALWAYS_INLINE yield_with_select_(uint32_t delay_ms); +#endif #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) void setup_wake_loop_threadsafe_(); // Create wake notification socket @@ -814,4 +830,134 @@ inline void Application::drain_wake_notifications_() { } #endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) { +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) + // Drain wake notifications first to clear socket for next wake + this->drain_wake_notifications_(); +#endif + + // Process scheduled tasks + this->scheduler.call(loop_start_time); + + // Feed the watchdog timer + this->feed_wdt(loop_start_time); + + // Process any pending enable_loop requests from ISRs + // This must be done before marking in_loop_ = true to avoid race conditions + if (this->has_pending_enable_loop_requests_) { + // Clear flag BEFORE processing to avoid race condition + // If ISR sets it during processing, we'll catch it next loop iteration + // This is safe because: + // 1. Each component has its own pending_enable_loop_ flag that we check + // 2. If we can't process a component (wrong state), enable_pending_loops_() + // will set this flag back to true + // 3. Any new ISR requests during processing will set the flag again + this->has_pending_enable_loop_requests_ = false; + this->enable_pending_loops_(); + } + + // Mark that we're in the loop for safe reentrant modifications + this->in_loop_ = true; +} + +inline void ESPHOME_ALWAYS_INLINE Application::loop() { + uint8_t new_app_state = 0; + + // Get the initial loop time at the start + uint32_t last_op_end_time = millis(); + + this->before_loop_tasks_(last_op_end_time); + + for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; + this->current_loop_index_++) { + Component *component = this->looping_components_[this->current_loop_index_]; + + // Update the cached time before each component runs + this->loop_component_start_time_ = last_op_end_time; + + { + this->set_current_component(component); + WarnIfComponentBlockingGuard guard{component, last_op_end_time}; + component->loop(); + // Use the finish method to get the current time as the end time + last_op_end_time = guard.finish(); + } + new_app_state |= component->get_component_state(); + this->app_state_ |= new_app_state; + this->feed_wdt(last_op_end_time); + } + + this->after_loop_tasks_(); + this->app_state_ = new_app_state; + +#ifdef USE_RUNTIME_STATS + // Process any pending runtime stats printing after all components have run + // This ensures stats printing doesn't affect component timing measurements + if (global_runtime_stats != nullptr) { + global_runtime_stats->process_pending_stats(last_op_end_time); + } +#endif + + // Use the last component's end time instead of calling millis() again + auto elapsed = last_op_end_time - this->last_loop_; + if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { + // Even if we overran the loop interval, we still need to select() + // to know if any sockets have data ready + this->yield_with_select_(0); + } else { + uint32_t delay_time = this->loop_interval_ - elapsed; + uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time); + // next_schedule is max 0.5*delay_time + // otherwise interval=0 schedules result in constant looping with almost no sleep + next_schedule = std::max(next_schedule, delay_time / 2); + delay_time = std::min(next_schedule, delay_time); + + this->yield_with_select_(delay_time); + } + this->last_loop_ = last_op_end_time; + + if (this->dump_config_at_ < this->components_.size()) { + this->process_dump_config_(); + } +} + +// Inline yield_with_select_ for all paths except the select() fallback +#if !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) +inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) { +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) + // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. + // Safe because this runs on the main loop which owns socket lifetime (create, read, close). + if (delay_ms == 0) [[unlikely]] { + yield(); + return; + } + + // Check if any socket already has pending data before sleeping. + // If a socket still has unread data (rcvevent > 0) but the task notification was already + // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. + // This scan preserves select() semantics: return immediately when any fd is ready. + for (struct lwip_sock *sock : this->monitored_sockets_) { + if (esphome_lwip_socket_has_data(sock)) { + yield(); + return; + } + } + + // Sleep with instant wake via FreeRTOS task notification. + // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. + // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — + // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); +#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) + // No select support but can wake on socket activity + // ESP8266: via esp_schedule() + // RP2040: via __sev()/__wfe() hardware sleep/wake + socket::socket_delay(delay_ms); +#else + // No select support, use regular delay + delay(delay_ms); +#endif +} +#endif // !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) + } // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index ee99c54196..1732fc72e8 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -863,7 +863,16 @@ bool mac_address_is_valid(const uint8_t *mac) { is_all_ones = false; } } - return !(is_all_zeros || is_all_ones); + if (is_all_zeros || is_all_ones) { + return false; + } + // Reject multicast MACs (bit 0 of first byte set) - device MACs must be unicast. + // This catches garbage data from corrupted eFuse custom MAC areas, which often + // has random values that would otherwise pass the all-zeros/all-ones check. + if (mac[0] & 0x01) { + return false; + } + return true; } void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index e0d6fe56b1..e3fc6092c3 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1764,7 +1764,10 @@ template struct Callback { // Safe under C++20 (P0593R6): byte copy into aligned storage implicitly // creates objects of implicit-lifetime types (trivially copyable qualifies). Callback cb; // fn and ctx are zero-initialized by default - __builtin_memcpy(&cb.ctx_, &callable, sizeof(DecayF)); + // Decay callable to a local variable first. When F is a function reference + // (e.g. void(&)(int)), &callable would point at machine code, not a pointer variable. + DecayF decayed = std::forward(callable); + __builtin_memcpy(&cb.ctx_, &decayed, sizeof(DecayF)); cb.fn_ = [](void *c, Ts... args) { alignas(DecayF) char buf[sizeof(DecayF)]; __builtin_memcpy(buf, &c, sizeof(DecayF)); diff --git a/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp index 36af087d2c..5ad25c2d7d 100644 --- a/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp +++ b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp @@ -5,6 +5,7 @@ namespace esphome::packet_transport::testing { TEST(PacketTransportBinarySensorTest, AddBinarySensor) { TestablePacketTransport transport; binary_sensor::BinarySensor bs; + transport.set_binary_sensor_count(1); transport.add_binary_sensor("motion", &bs); ASSERT_EQ(transport.binary_sensors_.size(), 1u); EXPECT_STREQ(transport.binary_sensors_[0].id, "motion"); @@ -24,6 +25,7 @@ TEST(PacketTransportBinarySensorTest, UnencryptedBinarySensorRoundTrip) { encoder.init_for_test("sender"); binary_sensor::BinarySensor local_bs; local_bs.state = true; + encoder.set_binary_sensor_count(1); encoder.add_binary_sensor("motion", &local_bs); encoder.send_data_(true); @@ -46,11 +48,13 @@ TEST(PacketTransportBinarySensorTest, MultipleSensorsRoundTrip) { sensor::Sensor s1, s2; s1.state = 10.0f; s2.state = 20.0f; + encoder.set_sensor_count(2); encoder.add_sensor("s1", &s1); encoder.add_sensor("s2", &s2); binary_sensor::BinarySensor bs1; bs1.state = true; + encoder.set_binary_sensor_count(1); encoder.add_binary_sensor("bs1", &bs1); encoder.send_data_(true); diff --git a/tests/components/packet_transport/sensor/sensor_test.cpp b/tests/components/packet_transport/sensor/sensor_test.cpp index 2f681aee58..5d1cfb4bc2 100644 --- a/tests/components/packet_transport/sensor/sensor_test.cpp +++ b/tests/components/packet_transport/sensor/sensor_test.cpp @@ -5,6 +5,7 @@ namespace esphome::packet_transport::testing { TEST(PacketTransportSensorTest, AddSensor) { TestablePacketTransport transport; sensor::Sensor s; + transport.set_sensor_count(1); transport.add_sensor("temp", &s); ASSERT_EQ(transport.sensors_.size(), 1u); EXPECT_STREQ(transport.sensors_[0].id, "temp"); @@ -26,6 +27,7 @@ TEST(PacketTransportSensorTest, UnencryptedSensorRoundTrip) { encoder.init_for_test("sender"); sensor::Sensor local_sensor; local_sensor.state = 42.5f; + encoder.set_sensor_count(1); encoder.add_sensor("temp", &local_sensor); encoder.send_data_(true); @@ -53,6 +55,7 @@ TEST(PacketTransportSensorTest, EncryptedSensorRoundTrip) { encoder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 99.9f; + encoder.set_sensor_count(1); encoder.add_sensor("temp", &local_sensor); encoder.send_data_(true); @@ -77,6 +80,7 @@ TEST(PacketTransportSensorTest, SendDataOnlyUpdated) { sensor::Sensor s1, s2; s1.state = 1.0f; s2.state = 2.0f; + encoder.set_sensor_count(2); encoder.add_sensor("s1", &s1); encoder.add_sensor("s2", &s2); @@ -111,6 +115,7 @@ TEST(PacketTransportSensorTest, PingKeyIncludedInTransmittedPacket) { responder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 77.7f; + responder.set_sensor_count(1); responder.add_sensor("temp", &local_sensor); // Requester sends a MAGIC_PING that the responder processes @@ -148,6 +153,7 @@ TEST(PacketTransportSensorTest, MissingPingKeyBlocksSensorData) { responder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 77.7f; + responder.set_sensor_count(1); responder.add_sensor("temp", &local_sensor); responder.send_data_(true); ASSERT_EQ(responder.sent_packets.size(), 1u);