diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 474eb9ec38..a6ee22ad03 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -73,6 +73,65 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch // Reset the recursion guard for this task this->reset_task_log_recursion_(is_main_task); } +#elif defined(USE_HOST) +// Implementation for host platform (multi-threaded with pthread support) +// Main thread always uses direct buffer access for console output and callbacks +// +// For non-main threads: +// - WITH task log buffer: Queue message to lock-free ring buffer for async processing +// - Prevents console corruption from concurrent writes by multiple threads +// - Messages are serialized through main loop for proper console output +// - Fallback to emergency console logging only if ring buffer is full +// - WITHOUT task log buffer: Only emergency console output, no callbacks +void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT + if (level > this->level_for(tag)) + return; + + pthread_t current_thread = pthread_self(); + bool is_main_thread = pthread_equal(current_thread, main_thread_); + + // Check and set recursion guard - uses pthread TLS for per-thread state + if (this->check_and_set_task_log_recursion_(is_main_thread)) { + return; // Recursion detected + } + + // Main thread uses the shared buffer for efficiency + if (is_main_thread) { + this->log_message_to_buffer_and_send_(level, tag, line, format, args); + this->reset_task_log_recursion_(is_main_thread); + return; + } + + bool message_sent = false; +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + // For non-main threads, queue the message for callbacks + message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), format, args); + if (message_sent) { + // Enable logger loop to process the buffered message + this->enable_loop_soon_any_context(); + } +#endif // USE_ESPHOME_TASK_LOG_BUFFER + + // Emergency console logging for non-main threads when ring buffer is full or disabled + // This is a fallback mechanism to ensure critical log messages are visible + // Note: This may cause interleaved/corrupted console output if multiple threads + // log simultaneously, but it's better than losing important messages entirely + if (!message_sent) { + // Host always has console output - no baud_rate check needed + // Use larger buffer for host since memory is plentiful + static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 1024; + char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety + uint16_t buffer_at = 0; // Initialize buffer position + this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, + MAX_CONSOLE_LOG_MSG_SIZE); + // Add newline before writing to console + this->add_newline_to_buffer_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); + this->write_msg_(console_buffer, buffer_at); + } + + // Reset the recursion guard for this thread + this->reset_task_log_recursion_(is_main_thread); +} #else // Implementation for all other platforms void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT @@ -86,7 +145,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch global_recursion_guard_ = false; } -#endif // !USE_ESP32 +#endif // USE_ESP32 / USE_HOST #ifdef USE_STORE_LOG_STR_IN_FLASH // Implementation for ESP8266 with flash string support. diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index ba8d4667b6..1e3f17a67c 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -2,7 +2,7 @@ #include #include -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_HOST) #include #endif #include "esphome/core/automation.h" @@ -12,7 +12,11 @@ #include "esphome/core/log.h" #ifdef USE_ESPHOME_TASK_LOG_BUFFER -#include "task_log_buffer.h" +#ifdef USE_HOST +#include "task_log_buffer_host.h" +#elif defined(USE_ESP32) +#include "task_log_buffer_esp32.h" +#endif #endif #ifdef USE_ARDUINO @@ -181,6 +185,9 @@ class Logger : public Component { 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. @@ -228,7 +235,7 @@ class Logger : public Component { inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, va_list args, char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST) this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); #elif defined(USE_ZEPHYR) char buff[MAX_POINTER_REPRESENTATION]; @@ -325,6 +332,9 @@ class Logger : public Component { #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) void *main_task_ = nullptr; // Only used for thread name identification #endif +#ifdef USE_HOST + pthread_t main_thread_{}; // Main thread for identification +#endif #ifdef USE_ESP32 // Task-specific recursion guards: // - Main task uses a dedicated member variable for efficiency @@ -332,6 +342,10 @@ class Logger : public Component { 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 @@ -342,7 +356,11 @@ class Logger : public Component { std::vector level_listeners_; // Log level change listeners #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER +#ifdef USE_HOST + std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer +#elif defined(USE_ESP32) std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer +#endif #endif // Group smaller types together at the end @@ -355,7 +373,7 @@ class Logger : public Component { #ifdef USE_LIBRETINY UARTSelection uart_{UART_SELECTION_DEFAULT}; #endif -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_HOST) bool main_task_recursion_guard_{false}; #else bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms @@ -392,7 +410,7 @@ class Logger : public Component { } #endif -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_HOST) inline bool HOT check_and_set_task_log_recursion_(bool is_main_task) { if (is_main_task) { const bool was_recursive = main_task_recursion_guard_; @@ -418,6 +436,22 @@ class Logger : public Component { } #endif +#ifdef USE_HOST + const char *HOT get_thread_name_() { + pthread_t current_thread = pthread_self(); + if (pthread_equal(current_thread, main_thread_)) { + return nullptr; // Main thread + } + // For non-main threads, return the thread name + // We store it in thread-local storage to avoid allocation + static thread_local char thread_name_buf[32]; + if (pthread_getname_np(current_thread, thread_name_buf, sizeof(thread_name_buf)) == 0) { + return thread_name_buf; + } + return nullptr; + } +#endif + static inline void copy_string(char *buffer, uint16_t &pos, const char *str) { const size_t len = strlen(str); // Intentionally no null terminator, building larger string @@ -475,7 +509,7 @@ class Logger : public Component { buffer[pos++] = '0' + (remainder - tens * 10); buffer[pos++] = ']'; -#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) if (thread_name != nullptr) { write_ansi_color_for_level(buffer, pos, 1); // Always use bold red for thread name buffer[pos++] = '['; diff --git a/esphome/components/logger/task_log_buffer.cpp b/esphome/components/logger/task_log_buffer_esp32.cpp similarity index 100% rename from esphome/components/logger/task_log_buffer.cpp rename to esphome/components/logger/task_log_buffer_esp32.cpp diff --git a/esphome/components/logger/task_log_buffer.h b/esphome/components/logger/task_log_buffer_esp32.h similarity index 100% rename from esphome/components/logger/task_log_buffer.h rename to esphome/components/logger/task_log_buffer_esp32.h diff --git a/esphome/components/logger/task_log_buffer_host.h b/esphome/components/logger/task_log_buffer_host.h new file mode 100644 index 0000000000..4db4a5dbc6 --- /dev/null +++ b/esphome/components/logger/task_log_buffer_host.h @@ -0,0 +1,108 @@ +#pragma once + +#ifdef USE_HOST + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + +#include +#include +#include +#include +#include +#include + +namespace esphome::logger { + +/** + * @brief Lock-free task log buffer for host platform. + * + * This implements a Multi-Producer Single-Consumer (MPSC) lock-free ring buffer + * for log messages on the host platform. It uses atomic operations for thread-safety + * without requiring mutexes in the hot path. + * + * Design: + * - Fixed number of pre-allocated message slots to avoid dynamic allocation + * - Each slot contains a header and fixed-size text buffer + * - Atomic indices for lock-free push/pop operations + * - Thread-safe for multiple producers, single consumer (main loop) + * + * Host platform has much more memory than embedded devices, so we use larger + * buffer sizes for better log message handling. + */ +class TaskLogBufferHost { + public: + // Default number of message slots - host has plenty of memory + static constexpr size_t DEFAULT_SLOT_COUNT = 64; + + // Structure for a log message (fixed size for lock-free operation) + struct LogMessage { + // Size constants - host has plenty of memory, so use larger sizes + static constexpr size_t MAX_THREAD_NAME_SIZE = 32; + static constexpr size_t MAX_TEXT_SIZE = 1024; + + const char *tag; // Pointer to static tag string + char thread_name[MAX_THREAD_NAME_SIZE]; // Thread name (copied) + char text[MAX_TEXT_SIZE + 1]; // Message text with null terminator + uint16_t text_length; // Actual length of text + uint16_t line; // Source line number + uint8_t level; // Log level + std::atomic ready; // Message is ready to be consumed + + LogMessage() : tag(nullptr), text_length(0), line(0), level(0), ready(false) { + thread_name[0] = '\0'; + text[0] = '\0'; + } + }; + + /// Constructor that takes the number of message slots + explicit TaskLogBufferHost(size_t slot_count); + ~TaskLogBufferHost(); + + // NOT thread-safe - get next message from buffer, only call from main loop + // Returns true if a message was retrieved, false if buffer is empty + bool get_message_main_loop(LogMessage **message); + + // NOT thread-safe - release the message after processing, only call from main loop + void release_message_main_loop(); + + // Thread-safe - send a message to the buffer from any thread + // Returns true if message was queued, false if buffer is full + bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, va_list args); + + // Check if there are messages ready to be processed + inline bool HOT has_messages() const { + return read_index_.load(std::memory_order_acquire) != write_index_.load(std::memory_order_acquire); + } + + // Get the buffer size (number of slots) + inline size_t size() const { return slot_count_; } + + private: + // Acquire a slot for writing (thread-safe) + // Returns slot index or -1 if buffer is full + int acquire_write_slot_(); + + // Commit a slot after writing (thread-safe) + void commit_write_slot_(int slot_index); + + std::unique_ptr slots_; // Pre-allocated message slots + size_t slot_count_; // Number of slots + + // Lock-free indices using atomics + // We use a simple approach: write_index_ is where the next write will go, + // read_index_ is where the next read will come from + std::atomic write_index_{0}; // Next slot to write to + std::atomic read_index_{0}; // Next slot to read from + std::atomic commit_index_{0}; // Last committed write + + // For thread-safe slot acquisition + std::atomic reserve_index_{0}; // Next slot to reserve for writing +}; + +} // namespace esphome::logger + +#endif // USE_ESPHOME_TASK_LOG_BUFFER +#endif // USE_HOST