#pragma once #include #include #include #include #if defined(USE_ESP32) || defined(USE_HOST) #include #endif #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "log_buffer.h" #include "task_log_buffer_host.h" #include "task_log_buffer_esp32.h" #include "task_log_buffer_libretiny.h" #include "task_log_buffer_zephyr.h" #ifdef USE_ARDUINO #if defined(USE_ESP8266) #include #endif // USE_ESP8266 #ifdef USE_RP2040 #include #include #endif // USE_RP2040 #endif // USE_ARDUINO #ifdef USE_ESP32 #include #endif // USE_ESP32 #ifdef USE_ZEPHYR #include struct device; #endif namespace esphome::logger { /** Lightweight callback for receiving log messages without virtual dispatch overhead. * * Replaces the former LogListener virtual interface to eliminate per-implementer * vtable sub-tables and thunk code (~39 bytes saved per class that used LogListener). * * Usage: * // In your component's setup(): * if (logger::global_logger != nullptr) * logger::global_logger->add_log_callback( * this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { * static_cast(self)->on_log(level, tag, message, message_len); * }); */ struct LogCallback { void *instance; void (*fn)(void *, uint8_t, const char *, const char *, size_t); void invoke(uint8_t level, const char *tag, const char *message, size_t message_len) const { this->fn(this->instance, level, tag, message, message_len); } }; #ifdef USE_LOGGER_LEVEL_LISTENERS /** Interface for receiving log level changes without std::function overhead. * * Components can implement this interface instead of using lambdas with std::function * to reduce flash usage from std::function type erasure machinery. * * Usage: * class MyComponent : public Component, public LoggerLevelListener { * public: * void setup() override { * if (logger::global_logger != nullptr) * logger::global_logger->add_logger_level_listener(this); * } * void on_log_level_change(uint8_t level) override { * // Handle log level change * } * }; */ class LoggerLevelListener { public: virtual void on_log_level_change(uint8_t level) = 0; }; #endif #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS // Comparison function for const char* keys in log_levels_ map struct CStrCompare { bool operator()(const char *a, const char *b) const { return strcmp(a, b) < 0; } }; #endif // Stack buffer size for retrieving thread/task names from the OS // macOS allows up to 64 bytes, Linux up to 16 static constexpr size_t THREAD_NAME_BUF_SIZE = 64; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * * Advanced configuration (pin selection, etc) is not supported. */ enum UARTSelection : uint8_t { #ifdef USE_LIBRETINY UART_SELECTION_DEFAULT = 0, UART_SELECTION_UART0, #else UART_SELECTION_UART0 = 0, #endif UART_SELECTION_UART1, #if defined(USE_LIBRETINY) || defined(USE_ESP32_VARIANT_ESP32) UART_SELECTION_UART2, #endif #ifdef USE_LOGGER_USB_CDC UART_SELECTION_USB_CDC, #endif #ifdef USE_LOGGER_USB_SERIAL_JTAG UART_SELECTION_USB_SERIAL_JTAG, #endif #ifdef USE_ESP8266 UART_SELECTION_UART0_SWAP, #endif // USE_ESP8266 }; #endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY || USE_ZEPHYR /** * @brief Logger component for all ESPHome logging. * * This class implements a multi-platform logging system with protection against recursion. * * Recursion Protection Strategy: * - On ESP32: Uses task-specific recursion guards * * Main task: Uses a dedicated boolean member variable for efficiency * * Other tasks: Uses pthread TLS with a dynamically allocated key for task-specific state * - On other platforms: Uses a simple global recursion guard * * We use pthread TLS via pthread_key_create to create a unique key for storing * task-specific recursion state, which: * 1. Efficiently handles multiple tasks without locks or mutexes * 2. Works with ESP-IDF's pthread implementation that uses a linked list for TLS variables * 3. Avoids the limitations of the fixed FreeRTOS task local storage slots */ class Logger final : public Component { public: explicit Logger(uint32_t baud_rate); #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void loop() override; #endif /// Manually set the baud rate for serial, set to 0 to disable. void set_baud_rate(uint32_t baud_rate); uint32_t get_baud_rate() const { return baud_rate_; } #if defined(USE_ARDUINO) && !defined(USE_ESP32) Stream *get_hw_serial() const { return hw_serial_; } #endif #ifdef USE_ESP32 uart_port_t get_uart_num() const { return uart_num_; } void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } #endif #ifdef USE_HOST void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } #endif #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } /// Get the UART used by the logger. UARTSelection get_uart() const; #endif /// Set the default log level for this logger. void set_log_level(uint8_t level); #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS /// Set the log level of the specified tag. void set_log_level(const char *tag, uint8_t log_level); #endif uint8_t get_log_level() { return this->current_level_; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Set up this component. void pre_setup(); void dump_config() override; inline uint8_t level_for(const char *tag); #ifdef USE_LOG_LISTENERS /// Register a log callback to receive log messages void add_log_callback(void *instance, void (*fn)(void *, uint8_t, const char *, const char *, size_t)) { this->log_callbacks_.push_back(LogCallback{instance, fn}); } #else /// No-op when log listeners are disabled void add_log_callback(void *instance, void (*fn)(void *, uint8_t, const char *, const char *, size_t)) {} #endif #ifdef USE_LOGGER_LEVEL_LISTENERS /// Register a listener for log level changes void add_level_listener(LoggerLevelListener *listener) { this->level_listeners_.push_back(listener); } #endif float get_setup_priority() const override; void log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args); // NOLINT #ifdef USE_STORE_LOG_STR_IN_FLASH void log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, va_list args); // NOLINT #endif protected: // RAII guard for recursion flags - sets flag on construction, clears on destruction class RecursionGuard { public: explicit RecursionGuard(bool &flag) : flag_(flag) { flag_ = true; } ~RecursionGuard() { flag_ = false; } RecursionGuard(const RecursionGuard &) = delete; RecursionGuard &operator=(const RecursionGuard &) = delete; RecursionGuard(RecursionGuard &&) = delete; RecursionGuard &operator=(RecursionGuard &&) = delete; private: bool &flag_; }; #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) // Handles non-main thread logging only (~0.1% of calls) // thread_name is resolved by the caller from the task handle, avoiding redundant lookups void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args, const char *thread_name); #endif #if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) void cdc_loop_(); #endif void process_messages_(); #if defined(USE_HOST) || defined(USE_ZEPHYR) void write_msg_(const char *msg, uint16_t len); #else inline void write_msg_(const char *msg, uint16_t len); // Defined in platform-specific logger_*.h #endif // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // thread_name: name of the calling thread/task, or nullptr for main task (callers already know which task they're on) inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, va_list args, LogBuffer &buf, const char *thread_name) { buf.write_header(level, tag, line, thread_name); buf.format_body(format, args); } #ifdef USE_STORE_LOG_STR_IN_FLASH // Format a log message with flash string format and write it to a buffer with header, footer, and null terminator // ESP8266-only (single-task), thread_name is always nullptr inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, va_list args, LogBuffer &buf) { buf.write_header(level, tag, line, nullptr); buf.format_body_P(reinterpret_cast(format), args); } #endif // Helper to notify log callbacks inline void HOT notify_listeners_(uint8_t level, const char *tag, const LogBuffer &buf) { #ifdef USE_LOG_LISTENERS for (auto &cb : this->log_callbacks_) cb.invoke(level, tag, buf.data, buf.pos); #endif } // Helper to write log buffer to console (replaces null terminator with newline and writes) inline void HOT write_to_console_(LogBuffer &buf) { buf.terminate_with_newline(); this->write_msg_(buf.data, buf.pos); } // Helper to write log buffer to console if logging is enabled inline void HOT write_log_buffer_to_console_(LogBuffer &buf) { if (this->baud_rate_ > 0) this->write_to_console_(buf); } // Helper to format and send a log message to both console and listeners // Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings // thread_name: name of the calling thread/task, or nullptr for main task template inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line, FormatType format, va_list args, const char *thread_name) { RecursionGuard guard(recursion_guard); LogBuffer buf{this->tx_buffer_, ESPHOME_LOGGER_TX_BUFFER_SIZE}; #ifdef USE_STORE_LOG_STR_IN_FLASH if constexpr (std::is_same_v) { this->format_log_to_buffer_with_terminator_P_(level, tag, line, format, args, buf); } else #endif { this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name); } this->notify_listeners_(level, tag, buf); this->write_log_buffer_to_console_(buf); } #ifdef USE_ESPHOME_TASK_LOG_BUFFER // Helper to format a pre-formatted message from the task log buffer and notify listeners // Used by process_messages_ to avoid code duplication between ESP32 and host platforms inline void HOT format_buffered_message_and_notify_(uint8_t level, const char *tag, uint16_t line, const char *thread_name, const char *text, uint16_t text_length, LogBuffer &buf) { buf.write_header(level, tag, line, thread_name); buf.write_body(text, text_length); this->notify_listeners_(level, tag, buf); } #endif #ifndef USE_HOST const LogString *get_uart_selection_(); #endif // Group 4-byte aligned members first uint32_t baud_rate_; #if defined(USE_ARDUINO) && !defined(USE_ESP32) Stream *hw_serial_{nullptr}; #endif #if defined(USE_ZEPHYR) void dump_crash_(); const device *uart_dev_{nullptr}; #endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) void *main_task_{nullptr}; // Main thread/task for fast path comparison #endif #ifdef USE_HOST pthread_t main_thread_{}; // Main thread for pthread_equal() comparison #endif #ifdef USE_ESP32 // Task-specific recursion guards: // - Main task uses a dedicated member variable for efficiency // - Other tasks use pthread TLS with a dynamically created key via pthread_key_create pthread_key_t log_recursion_key_; // 4 bytes uart_port_t uart_num_; // 4 bytes (enum defaults to int size) #endif #ifdef USE_HOST // Thread-specific recursion guards using pthread TLS pthread_key_t log_recursion_key_; #endif // Large objects (internally aligned) #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS std::map log_levels_{}; #endif #ifdef USE_LOG_LISTENERS StaticVector log_callbacks_; // Log message callbacks (API, MQTT, syslog, etc.) #endif #ifdef USE_LOGGER_LEVEL_LISTENERS std::vector level_listeners_; // Log level change listeners #endif // Group smaller types together at the end uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) UARTSelection uart_{UART_SELECTION_UART0}; #endif #ifdef USE_LIBRETINY UARTSelection uart_{UART_SELECTION_DEFAULT}; #endif #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) bool main_task_recursion_guard_{false}; #ifdef USE_LIBRETINY bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny #endif #else bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif // Large buffers placed last to keep frequently-accessed member offsets small char tx_buffer_[ESPHOME_LOGGER_TX_BUFFER_SIZE + 1]; // +1 for null terminator #ifdef USE_ESPHOME_TASK_LOG_BUFFER logger::TaskLogBuffer log_buffer_; // Embedded in Logger (no separate heap allocation) #endif // --- get_thread_name_ overloads (per-platform) --- #if defined(USE_ESP32) || defined(USE_LIBRETINY) // Primary overload - takes a task handle directly to avoid redundant xTaskGetCurrentTaskHandle() calls // when the caller already has the handle (e.g. from the main task check in log_vprintf_) const char *get_thread_name_(TaskHandle_t task) { if (task == this->main_task_) { return nullptr; // Main task } #if defined(USE_ESP32) return pcTaskGetName(task); #elif defined(USE_LIBRETINY) return pcTaskGetTaskName(task); #endif } // Convenience overload - gets the current task handle and delegates const char *HOT get_thread_name_() { return this->get_thread_name_(xTaskGetCurrentTaskHandle()); } #elif defined(USE_HOST) // Takes a caller-provided buffer for the thread name (stack-allocated for thread safety) const char *HOT get_thread_name_(std::span buff) { pthread_t current_thread = pthread_self(); if (pthread_equal(current_thread, main_thread_)) { return nullptr; // Main thread } // For non-main threads, get the thread name into the caller-provided buffer if (pthread_getname_np(current_thread, buff.data(), buff.size()) == 0) { return buff.data(); } return nullptr; } #elif defined(USE_ZEPHYR) const char *HOT get_thread_name_(std::span buff, k_tid_t current_task = nullptr) { if (current_task == nullptr) { current_task = k_current_get(); } if (current_task == main_task_) { return nullptr; // Main task } const char *name = k_thread_name_get(current_task); if (name) { // zephyr print task names only if debug component is present return name; } std::snprintf(buff.data(), buff.size(), "%p", current_task); return buff.data(); } #endif // --- Non-main task recursion guards (per-platform) --- #if defined(USE_ESP32) || defined(USE_HOST) // RAII guard for non-main task recursion using pthread TLS class NonMainTaskRecursionGuard { public: explicit NonMainTaskRecursionGuard(pthread_key_t key) : key_(key) { pthread_setspecific(key_, reinterpret_cast(1)); } ~NonMainTaskRecursionGuard() { pthread_setspecific(key_, nullptr); } NonMainTaskRecursionGuard(const NonMainTaskRecursionGuard &) = delete; NonMainTaskRecursionGuard &operator=(const NonMainTaskRecursionGuard &) = delete; NonMainTaskRecursionGuard(NonMainTaskRecursionGuard &&) = delete; NonMainTaskRecursionGuard &operator=(NonMainTaskRecursionGuard &&) = delete; private: pthread_key_t key_; }; // Check if non-main task is already in recursion (via TLS) inline bool HOT is_non_main_task_recursive_() const { return pthread_getspecific(log_recursion_key_) != nullptr; } // Create RAII guard for non-main task recursion inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); } #elif defined(USE_LIBRETINY) || defined(USE_ZEPHYR) // LibreTiny doesn't have FreeRTOS TLS, so use a simple approach: // - Main task uses dedicated boolean (same as ESP32) // - Non-main tasks share a single recursion guard // This is safe because: // - Recursion from logging within logging is the main concern // - Cross-task "recursion" is prevented by the buffer mutex anyway // - Missing a recursive call from another task is acceptable (falls back to direct output) // // Zephyr use __thread as TLS // Check if non-main task is already in recursion inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard_; } // Create RAII guard for non-main task recursion (uses shared boolean for all non-main tasks) inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); } #endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) // Disable loop when task buffer is empty // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() inline void disable_loop_when_buffer_empty_() { // Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() // concurrently. If that happens between our check and disable_loop(), the enable request // will be processed on the next main loop iteration since: // - disable_loop() takes effect immediately // - enable_loop_soon_any_context() sets a pending flag that's checked at loop start this->disable_loop(); } #endif }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) class LoggerMessageTrigger final : public Trigger { public: explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { parent->add_log_callback(this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { auto *trigger = static_cast(self); if (level <= trigger->level_) { trigger->trigger(level, tag, message); } }); } protected: uint8_t level_; }; } // namespace esphome::logger // Platform-specific inline implementations of write_msg_() // Must be included after the Logger class definition is complete #if defined(USE_ESP32) #include "logger_esp32.h" #elif defined(USE_ESP8266) #include "logger_esp8266.h" #elif defined(USE_RP2040) #include "logger_rp2040.h" #elif defined(USE_LIBRETINY) #include "logger_libretiny.h" #endif