Merge branch 'kamilcuk/use-placement-new' of https://github.com/Kamilcuk/esphome into kamilcuk/use-placement-new

This commit is contained in:
J. Nick Koston
2026-03-22 13:54:09 -10:00
12 changed files with 245 additions and 173 deletions
+2 -2
View File
@@ -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 <freertos/task.h>
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();
}
}
+36 -10
View File
@@ -9,11 +9,14 @@
#include <freertos/FreeRTOS.h>
#include <freertos/portmacro.h>
#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;
@@ -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)
@@ -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_) {
@@ -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<uint8_t> encryption_key_{};
#ifdef USE_SENSOR
std::vector<Sensor> sensors_{};
FixedVector<Sensor> sensors_{};
string_map_t<string_map_t<sensor::Sensor *>> remote_sensors_{};
#endif
#ifdef USE_BINARY_SENSOR
std::vector<BinarySensor> binary_sensors_{};
FixedVector<BinarySensor> binary_sensors_{};
string_map_t<string_map_t<binary_sensor::BinarySensor *>> remote_binary_sensors_{};
#endif
+1 -1
View File
@@ -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().
+3 -142
View File
@@ -12,21 +12,11 @@
#endif
#ifdef USE_LWIP_FAST_SELECT
#include "esphome/core/lwip_fast_select.h"
#ifdef USE_ESP32
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#else
#include <FreeRTOS.h>
#include <task.h>
#endif
#endif // USE_LWIP_FAST_SELECT
#include "esphome/core/version.h"
#include "esphome/core/hal.h"
#include <algorithm>
#include <ranges>
#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.
+150 -4
View File
@@ -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 <freertos/FreeRTOS.h>
#include <freertos/task.h>
#else
#include <FreeRTOS.h>
#include <task.h>
#endif
#else
#include <sys/select.h>
#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
+10 -1
View File
@@ -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) {
+4 -1
View File
@@ -1764,7 +1764,10 @@ template<typename... Ts> struct Callback<void(Ts...)> {
// 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<F>(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));
@@ -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);
@@ -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);