From 86ec218f75685454a3cc012cd632c350ba60ad5c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Mar 2026 13:15:35 -1000 Subject: [PATCH 01/10] [benchmark] Add plaintext API frame write benchmarks (#15036) --- .../components/api/bench_plaintext_frame.cpp | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 tests/benchmarks/components/api/bench_plaintext_frame.cpp diff --git a/tests/benchmarks/components/api/bench_plaintext_frame.cpp b/tests/benchmarks/components/api/bench_plaintext_frame.cpp new file mode 100644 index 0000000000..79bffaf953 --- /dev/null +++ b/tests/benchmarks/components/api/bench_plaintext_frame.cpp @@ -0,0 +1,162 @@ +#include "esphome/core/defines.h" +#ifdef USE_API_PLAINTEXT + +#include +#include +#include +#include +#include +#include + +#include "esphome/components/api/api_frame_helper_plaintext.h" +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_buffer.h" + +namespace esphome::api::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Helper to drain accumulated data from the read side of a socket +// to prevent the write side from blocking. +static void drain_socket(int fd) { + char buf[65536]; + while (::read(fd, buf, sizeof(buf)) > 0) { + } +} + +// Helper to create a TCP loopback connection with an APIPlaintextFrameHelper +// on the write end. Returns the helper and the read-side fd. +// Uses real TCP sockets so TCP_NODELAY succeeds during init(). +static std::pair, int> create_plaintext_helper() { + // Create a TCP listener on loopback + int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0); + int opt = 1; + ::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in addr {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; // OS-assigned port + ::bind(listen_fd, reinterpret_cast(&addr), sizeof(addr)); + ::listen(listen_fd, 1); + + // Get the assigned port + socklen_t addr_len = sizeof(addr); + ::getsockname(listen_fd, reinterpret_cast(&addr), &addr_len); + + // Connect from client side + int write_fd = ::socket(AF_INET, SOCK_STREAM, 0); + ::connect(write_fd, reinterpret_cast(&addr), sizeof(addr)); + + // Accept on server side (this is our read fd) + int read_fd = ::accept(listen_fd, nullptr, nullptr); + ::close(listen_fd); + + // Make both ends non-blocking + int flags = ::fcntl(write_fd, F_GETFL, 0); + ::fcntl(write_fd, F_SETFL, flags | O_NONBLOCK); + flags = ::fcntl(read_fd, F_GETFL, 0); + ::fcntl(read_fd, F_SETFL, flags | O_NONBLOCK); + + // Increase socket buffer sizes to reduce drain frequency + int bufsize = 1024 * 1024; + ::setsockopt(write_fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)); + ::setsockopt(read_fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)); + + auto sock = std::make_unique(write_fd); + auto helper = std::make_unique(std::move(sock)); + helper->init(); + + return {std::move(helper), read_fd}; +} + +// --- Write a single SensorStateResponse through plaintext framing --- +// Measures the full write path: header construction, varint encoding, +// iovec assembly, and socket write. + +static void PlaintextFrame_WriteSensorState(benchmark::State &state) { + auto [helper, read_fd] = create_plaintext_helper(); + uint8_t padding = helper->frame_header_padding(); + + // Pre-init buffer to typical TCP MSS size to avoid benchmarking + // heap allocation — in real use the buffer is reused across writes. + APIBuffer buffer; + buffer.reserve(1460); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + buffer.clear(); + SensorStateResponse msg; + msg.key = 0x12345678; + msg.state = 23.5f; + msg.missing_state = false; + + uint32_t size = msg.calculate_size(); + buffer.resize(padding + size); + ProtoWriteBuffer writer(&buffer, padding); + msg.encode(writer); + + helper->write_protobuf_packet(SensorStateResponse::MESSAGE_TYPE, writer); + + if ((i & 0xFF) == 0) + drain_socket(read_fd); + } + drain_socket(read_fd); + benchmark::DoNotOptimize(helper.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(PlaintextFrame_WriteSensorState); + +// --- Write a batch of 5 SensorStateResponses in one call --- +// Measures batched write: multiple messages assembled into one writev. + +static void PlaintextFrame_WriteBatch5(benchmark::State &state) { + auto [helper, read_fd] = create_plaintext_helper(); + uint8_t padding = helper->frame_header_padding(); + uint8_t footer = helper->frame_footer_size(); + + // Pre-init buffer to typical TCP MSS size to avoid benchmarking + // heap allocation — in real use the buffer is reused across writes. + APIBuffer buffer; + buffer.reserve(1460); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + buffer.clear(); + MessageInfo messages[5] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; + + for (int j = 0; j < 5; j++) { + uint16_t offset = buffer.size(); + SensorStateResponse msg; + msg.key = static_cast(j); + msg.state = 23.5f + static_cast(j); + msg.missing_state = false; + + uint32_t size = msg.calculate_size(); + buffer.resize(offset + padding + size + footer); + ProtoWriteBuffer writer(&buffer, offset + padding); + msg.encode(writer); + + messages[j] = MessageInfo(SensorStateResponse::MESSAGE_TYPE, offset, size); + } + + helper->write_protobuf_messages(ProtoWriteBuffer(&buffer, 0), std::span(messages, 5)); + + if ((i & 0xFF) == 0) + drain_socket(read_fd); + } + drain_socket(read_fd); + benchmark::DoNotOptimize(helper.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(PlaintextFrame_WriteBatch5); + +} // namespace esphome::api::benchmarks + +#endif // USE_API_PLAINTEXT From dd82a91d8fa8b3922e7e4b8177df5875c1d56d04 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:13:17 +1000 Subject: [PATCH 02/10] [lvgl] Don't animate page change when not requested (#15069) --- esphome/components/lvgl/defines.py | 2 +- esphome/components/lvgl/lvgl_esphome.cpp | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index d6d7a5e161..0a53d88669 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -280,7 +280,7 @@ SWIPE_TRIGGERS = tuple( LV_ANIM = LvConstant( - "LV_SCR_LOAD_ANIM_", + "LV_SCREEN_LOAD_ANIM_", "NONE", "OVER_LEFT", "OVER_RIGHT", diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index b1618f77c4..fb5e595713 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -176,7 +176,11 @@ void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t ti if (index >= this->pages_.size()) return; this->current_page_ = index; - lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); + if (anim == LV_SCREEN_LOAD_ANIM_NONE) { + lv_scr_load(this->pages_[this->current_page_]->obj); + } else { + lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); + } } void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { @@ -262,8 +266,8 @@ void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uin if (!this->is_paused()) { auto now = millis(); this->draw_buffer_(area, reinterpret_cast(color_p)); - ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), - lv_area_get_height(area), (int) (millis() - now)); + ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", (int) area->x1, (int) area->y1, + (int) lv_area_get_width(area), (int) lv_area_get_height(area), (int) (millis() - now)); } lv_display_flush_ready(disp_drv); } @@ -619,7 +623,7 @@ void LvglComponent::setup() { // Rotation will be handled by our drawing function, so reset the display rotation. for (auto *disp : this->displays_) disp->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); - this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0); + this->show_page(0, LV_SCREEN_LOAD_ANIM_NONE, 0); lv_display_trigger_activity(this->disp_); } From 8224da3460819a7a41fb302018f9ee3265fd46a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Mar 2026 15:32:24 -1000 Subject: [PATCH 03/10] [core] Inline Component::get_component_log_str() (#15068) --- esphome/core/component.cpp | 3 --- esphome/core/component.h | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 89ac0c7a2a..caaea89143 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -272,9 +272,6 @@ void Component::call() { break; } } -const LogString *Component::get_component_log_str() const { - return this->component_source_ == nullptr ? LOG_STR("") : this->component_source_; -} bool Component::should_warn_of_blocking(uint32_t blocking_time) { if (blocking_time > this->warn_if_blocking_over_) { // Prevent overflow when adding increment - if we're about to overflow, just max out diff --git a/esphome/core/component.h b/esphome/core/component.h index 119681f64c..46cd77b034 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -294,7 +294,9 @@ class Component { * * Returns LOG_STR("") if source not set */ - const LogString *get_component_log_str() const; + const LogString *get_component_log_str() const { + return this->component_source_ == nullptr ? LOG_STR("") : this->component_source_; + } bool should_warn_of_blocking(uint32_t blocking_time); From c48fd0738ba6ec1e88860758687c2fae06fae430 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Mar 2026 15:33:42 -1000 Subject: [PATCH 04/10] [mqtt] Rate-limit component resends to prevent task WDT on reconnect (#15061) --- esphome/components/mqtt/mqtt_client.cpp | 17 ++++++++++++++--- esphome/components/mqtt/mqtt_component.h | 3 +++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 38daf8f8f6..ab665e2579 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -28,6 +28,10 @@ namespace esphome::mqtt { static const char *const TAG = "mqtt"; +// Maximum number of MQTT component resends per loop iteration. +// Limits work to avoid triggering the task watchdog on reconnect. +static constexpr uint8_t MAX_RESENDS_PER_LOOP = 8; + // Disconnect reason strings indexed by MQTTClientDisconnectReason enum (0-8) PROGMEM_STRING_TABLE(MQTTDisconnectReasonStrings, "TCP disconnected", "Unacceptable Protocol Version", "Identifier Rejected", "Server Unavailable", "Malformed Credentials", "Not Authorized", @@ -396,9 +400,16 @@ void MQTTClientComponent::loop() { this->resubscribe_subscriptions_(); // Process pending resends for all MQTT components centrally - // This is more efficient than each component polling in its own loop - for (MQTTComponent *component : this->children_) { - component->process_resend(); + // Limit work per loop iteration to avoid triggering task WDT on reconnect + { + uint8_t resend_count = 0; + for (MQTTComponent *component : this->children_) { + if (component->is_resend_pending()) { + component->process_resend(); + if (++resend_count >= MAX_RESENDS_PER_LOOP) + break; + } + } } } break; diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 2403ef64ea..7983e04870 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -147,6 +147,9 @@ class MQTTComponent : public Component { /// Internal method for the MQTT client base to schedule a resend of the state on reconnect. void schedule_resend_state(); + /// Check if a resend is pending (called by MQTTClientComponent to rate-limit work) + bool is_resend_pending() const { return this->resend_state_; } + /// Process pending resend if needed (called by MQTTClientComponent) void process_resend(); From 1ccfd0b3cd19c44f17d8b99ae2e96866ddccebbe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Mar 2026 16:48:50 -1000 Subject: [PATCH 05/10] [logger] Fix race condition in task log buffer initialization Move TaskLogBuffer allocation from init_log_buffer() (called at DIAGNOSTICS priority) into the Logger constructor (called at EARLY_INIT priority). This ensures the buffer exists before global_logger is set, eliminating a window where another FreeRTOS task could dereference a null log_buffer_ pointer. Fixes crash: Guru Meditation Error: Core 0 panic'ed (Load access fault) in TaskLogBuffer::send_message_thread_safe when a task logs before init_log_buffer() is called. --- esphome/components/logger/__init__.py | 39 ++++++++++++++++----------- esphome/components/logger/logger.cpp | 9 ++++++- esphome/components/logger/logger.h | 6 +++-- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index a5601e6a8f..3da81e12a0 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -331,11 +331,27 @@ async def to_code(config: ConfigType) -> None: CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level tx_buffer_size = config[CONF_TX_BUFFER_SIZE] cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) - log = cg.new_Pvariable( - config[CONF_ID], - baud_rate, - ) - if CORE.is_esp32: + # Determine task log buffer size and define USE_ESPHOME_TASK_LOG_BUFFER early + # so the constructor can allocate the buffer immediately, preventing a race + # where another task logs before the buffer is initialized. + task_log_buffer_size = 0 + if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: + task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] + elif CORE.is_host: + task_log_buffer_size = 64 # Fixed 64 slots for host + if task_log_buffer_size > 0: + cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + task_log_buffer_size, + ) + else: + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + ) + if CORE.is_esp32 or CORE.is_host: cg.add(log.create_pthread_key()) # set_uart_selection() must be called before pre_setup() because # pre_setup() switches on uart_ to decide which hardware to initialize @@ -364,17 +380,10 @@ async def _late_logger_init(config: ConfigType) -> None: log = await cg.get_variable(config[CONF_ID]) level = config[CONF_LEVEL] baud_rate: int = config[CONF_BAUD_RATE] - if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: - task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] + if CORE.using_zephyr: + task_log_buffer_size = config.get(CONF_TASK_LOG_BUFFER_SIZE, 0) if task_log_buffer_size > 0: - cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - cg.add(log.init_log_buffer(task_log_buffer_size)) - if CORE.using_zephyr: - zephyr_add_prj_conf("MPSC_PBUF", True) - elif CORE.is_host: - cg.add(log.create_pthread_key()) - cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - cg.add(log.init_log_buffer(64)) # Fixed 64 slots for host + zephyr_add_prj_conf("MPSC_PBUF", True) # Enable runtime tag levels if logs are configured or explicitly enabled logs_config = config[CONF_LOGS] diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 497809cd2e..598af8117a 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -152,13 +152,20 @@ inline uint8_t Logger::level_for(const char *tag) { return this->current_level_; } +#ifdef USE_ESPHOME_TASK_LOG_BUFFER +Logger::Logger(uint32_t baud_rate, size_t task_log_buffer_size) : baud_rate_(baud_rate) { +#else Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { +#endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); #elif defined(USE_ZEPHYR) this->main_task_ = k_current_get(); #elif defined(USE_HOST) - this->main_thread_ = pthread_self(); +this->main_thread_ = pthread_self(); +#endif +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + this->init_log_buffer(task_log_buffer_size); #endif } #ifdef USE_ESPHOME_TASK_LOG_BUFFER diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8c38cadcbc..05b4cc21a7 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -143,9 +143,10 @@ enum UARTSelection : uint8_t { */ class Logger final : public Component { public: - explicit Logger(uint32_t baud_rate); #ifdef USE_ESPHOME_TASK_LOG_BUFFER - void init_log_buffer(size_t total_buffer_size); + explicit Logger(uint32_t baud_rate, size_t task_log_buffer_size); +#else + explicit Logger(uint32_t baud_rate); #endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void loop() override; @@ -353,6 +354,7 @@ class Logger final : public Component { std::vector level_listeners_; // Log level change listeners #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER + void init_log_buffer(size_t total_buffer_size); logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed #endif From 6576e88df0504f10e1e1365e88f5d16b086a325e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Mar 2026 16:53:25 -1000 Subject: [PATCH 06/10] Rename init_log_buffer to init_log_buffer_ for clang-tidy naming --- esphome/components/logger/logger.cpp | 4 ++-- esphome/components/logger/logger.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 598af8117a..9f9989691f 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -165,11 +165,11 @@ Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { this->main_thread_ = pthread_self(); #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER - this->init_log_buffer(task_log_buffer_size); + this->init_log_buffer_(task_log_buffer_size); #endif } #ifdef USE_ESPHOME_TASK_LOG_BUFFER -void Logger::init_log_buffer(size_t total_buffer_size) { +void Logger::init_log_buffer_(size_t total_buffer_size) { // Host uses slot count instead of byte size // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 05b4cc21a7..0697d6886f 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -354,7 +354,7 @@ class Logger final : public Component { std::vector level_listeners_; // Log level change listeners #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER - void init_log_buffer(size_t total_buffer_size); + void init_log_buffer_(size_t total_buffer_size); logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed #endif From 44ea4a22354a9a536c03f4a537359f87cd5dc6fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Mar 2026 16:55:15 -1000 Subject: [PATCH 07/10] Update esphome/components/logger/logger.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/logger/logger.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 9f9989691f..4a69990e40 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -162,7 +162,7 @@ Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { #elif defined(USE_ZEPHYR) this->main_task_ = k_current_get(); #elif defined(USE_HOST) -this->main_thread_ = pthread_self(); + this->main_thread_ = pthread_self(); #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER this->init_log_buffer_(task_log_buffer_size); From cbea4e0ccfc14ce029f7b07a3956ddf07f3dde8e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 02:56:50 +0000 Subject: [PATCH 08/10] [pre-commit.ci lite] apply automatic fixes --- esphome/components/logger/logger.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 4a69990e40..9f9989691f 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -162,7 +162,7 @@ Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { #elif defined(USE_ZEPHYR) this->main_task_ = k_current_get(); #elif defined(USE_HOST) - this->main_thread_ = pthread_self(); +this->main_thread_ = pthread_self(); #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER this->init_log_buffer_(task_log_buffer_size); From b0e6f57bc2c75bce260e436cff8920ffa953f0f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Mar 2026 16:58:49 -1000 Subject: [PATCH 09/10] Remove disable_loop from init_log_buffer_ to avoid ESP_LOGVV before global_logger is set Since init_log_buffer_ is now called from the constructor (before pre_setup sets global_logger), calling disable_loop() would trigger ESP_LOGVV which dereferences the null global_logger pointer. The loop self-disables on its first iteration when no messages are found, so the explicit disable in init_log_buffer_ was unnecessary. --- esphome/components/logger/logger.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 9f9989691f..3e8988b193 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -170,16 +170,11 @@ this->main_thread_ = pthread_self(); } #ifdef USE_ESPHOME_TASK_LOG_BUFFER void Logger::init_log_buffer_(size_t total_buffer_size) { - // Host uses slot count instead of byte size // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); - -#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) - // Start with loop disabled when using task buffer - // The loop will be enabled automatically when messages arrive - // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() - this->disable_loop_when_buffer_empty_(); -#endif + // Note: we don't call disable_loop_when_buffer_empty_() here because this is called + // from the constructor before the component is registered with App. The loop will + // self-disable on its first iteration when it finds no messages to process. } #endif From e32565b36abed5b7b78857df52f2e624c6b127f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Mar 2026 17:00:26 -1000 Subject: [PATCH 10/10] Inline init_log_buffer_ into constructor --- esphome/components/logger/logger.cpp | 14 ++++---------- esphome/components/logger/logger.h | 1 - 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 3e8988b193..ceacded775 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -165,18 +165,12 @@ Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { this->main_thread_ = pthread_self(); #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER - this->init_log_buffer_(task_log_buffer_size); -#endif -} -#ifdef USE_ESPHOME_TASK_LOG_BUFFER -void Logger::init_log_buffer_(size_t total_buffer_size) { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); - // Note: we don't call disable_loop_when_buffer_empty_() here because this is called - // from the constructor before the component is registered with App. The loop will - // self-disable on its first iteration when it finds no messages to process. -} + this->log_buffer_ = new logger::TaskLogBuffer(task_log_buffer_size); + // Note: we don't disable loop here because the component isn't registered with App yet. + // The loop self-disables on its first iteration when it finds no messages to process. #endif +} #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void Logger::loop() { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 0697d6886f..c81b8e4e94 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -354,7 +354,6 @@ class Logger final : public Component { std::vector level_listeners_; // Log level change listeners #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER - void init_log_buffer_(size_t total_buffer_size); logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed #endif