mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 08:55:36 +00:00
Merge branch 'kamilcuk/use-placement-new' of https://github.com/Kamilcuk/esphome into kamilcuk/use-placement-new
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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().
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user