#include "logger.h" #include #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/progmem.h" namespace esphome::logger { static const char *const TAG = "logger"; #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) // Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS) // Main thread/task always uses direct buffer access for console output and callbacks // // For non-main threads/tasks: // - WITH task log buffer: Prefer sending to ring buffer for async processing // - Avoids allocating stack memory for console output in normal operation // - 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 // // Optimized for the common case: 99.9% of logs come from the main thread 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; #if defined(USE_ESP32) || defined(USE_LIBRETINY) // Get task handle once - used for both main task check and passing to non-main thread handler TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); const bool is_main_task = (current_task == this->main_task_); #else // USE_HOST const bool is_main_task = pthread_equal(pthread_self(), this->main_thread_); #endif // Fast path: main thread, no recursion (99.9% of all logs) if (is_main_task && !this->main_task_recursion_guard_) [[likely]] { this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args); return; } // Main task with recursion - silently drop to prevent infinite loop if (is_main_task) { return; } // Non-main thread handling (~0.1% of logs) #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->log_vprintf_non_main_thread_(level, tag, line, format, args, current_task); #else // USE_HOST this->log_vprintf_non_main_thread_(level, tag, line, format, args); #endif } // Handles non-main thread logging only // Kept separate from hot path to improve instruction cache performance #if defined(USE_ESP32) || defined(USE_LIBRETINY) void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args, TaskHandle_t current_task) { #else // USE_HOST void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args) { #endif // Check if already in recursion for this non-main thread/task if (this->is_non_main_task_recursive_()) { return; } // RAII guard - automatically resets on any return path auto guard = this->make_non_main_task_guard_(); bool message_sent = false; #ifdef USE_ESPHOME_TASK_LOG_BUFFER // For non-main threads/tasks, queue the message for callbacks #if defined(USE_ESP32) || defined(USE_LIBRETINY) message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), current_task, format, args); #else // USE_HOST message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), format, args); #endif if (message_sent) { // Enable logger loop to process the buffered message // This is safe to call from any context including ISRs 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 #ifdef USE_HOST if (!message_sent) { // Host always has console output - no baud_rate check needed static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512; #else if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console // Maximum size for console log messages (includes null terminator) static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; #endif char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE}; this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); this->write_to_console_(buf); } // RAII guard automatically resets on return } #else // Implementation for all other platforms (single-task, no threading) 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) || global_recursion_guard_) return; this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); } #endif // USE_ESP32 / USE_HOST / USE_LIBRETINY #ifdef USE_STORE_LOG_STR_IN_FLASH // Implementation for ESP8266 with flash string support. // Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266. // // This function handles format strings stored in flash memory (PROGMEM) to save RAM. // Uses vsnprintf_P to read the format string directly from flash without copying to RAM. // void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { // NOLINT if (level > this->level_for(tag) || global_recursion_guard_) return; this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); } #endif // USE_STORE_LOG_STR_IN_FLASH inline uint8_t Logger::level_for(const char *tag) { #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS auto it = this->log_levels_.find(tag); if (it != this->log_levels_.end()) return it->second; #endif return this->current_level_; } Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) { // add 1 to buffer size for null terminator // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; #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(); #endif } #ifdef USE_ESPHOME_TASK_LOG_BUFFER void Logger::init_log_buffer(size_t total_buffer_size) { #ifdef USE_HOST // Host uses slot count instead of byte size // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->log_buffer_ = new logger::TaskLogBufferHost(total_buffer_size); #elif defined(USE_ESP32) // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); #elif defined(USE_LIBRETINY) // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->log_buffer_ = new logger::TaskLogBufferLibreTiny(total_buffer_size); #endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) // Start with loop disabled when using task buffer (unless using USB CDC on ESP32) // The loop will be enabled automatically when messages arrive this->disable_loop_when_buffer_empty_(); #endif } #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER void Logger::loop() { this->process_messages_(); } #endif void Logger::process_messages_() { #ifdef USE_ESPHOME_TASK_LOG_BUFFER // Process any buffered messages when available if (this->log_buffer_->has_messages()) { #ifdef USE_HOST logger::TaskLogBufferHost::LogMessage *message; while (this->log_buffer_->get_message_main_loop(&message)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text, message->text_length, buf); this->log_buffer_->release_message_main_loop(); this->write_log_buffer_to_console_(buf); } #elif defined(USE_ESP32) logger::TaskLogBuffer::LogMessage *message; const char *text; void *received_token; while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text, message->text_length, buf); // Release the message to allow other tasks to use it as soon as possible this->log_buffer_->release_message_main_loop(received_token); this->write_log_buffer_to_console_(buf); } #elif defined(USE_LIBRETINY) logger::TaskLogBufferLibreTiny::LogMessage *message; const char *text; while (this->log_buffer_->borrow_message_main_loop(&message, &text)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text, message->text_length, buf); // Release the message to allow other tasks to use it as soon as possible this->log_buffer_->release_message_main_loop(); this->write_log_buffer_to_console_(buf); } #endif } #if defined(USE_ESP32) || defined(USE_LIBRETINY) else { // No messages to process, disable loop if appropriate // This reduces overhead when there's no async logging activity this->disable_loop_when_buffer_empty_(); } #endif #endif // USE_ESPHOME_TASK_LOG_BUFFER } void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } #endif #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) UARTSelection Logger::get_uart() const { return this->uart_; } #endif float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } // Log level strings - packed into flash on ESP8266, indexed by log level (0-7) PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"); static const LogString *get_log_level_str(uint8_t level) { return LogLevelStrings::get_log_str(level, LogLevelStrings::LAST_INDEX); } void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:\n" " Max Level: %s\n" " Initial Level: %s", LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL)), LOG_STR_ARG(get_log_level_str(this->current_level_))); #ifndef USE_HOST ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32 "\n" " Hardware UART: %s", this->baud_rate_, LOG_STR_ARG(get_uart_selection_())); #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER if (this->log_buffer_) { #ifdef USE_HOST ESP_LOGCONFIG(TAG, " Task Log Buffer Slots: %u", static_cast(this->log_buffer_->size())); #else ESP_LOGCONFIG(TAG, " Task Log Buffer Size: %u bytes", static_cast(this->log_buffer_->size())); #endif } #endif #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS for (auto &it : this->log_levels_) { ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(get_log_level_str(it.second))); } #endif } void Logger::set_log_level(uint8_t level) { if (level > ESPHOME_LOG_LEVEL) { level = ESPHOME_LOG_LEVEL; ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL))); } this->current_level_ = level; #ifdef USE_LOGGER_LEVEL_LISTENERS for (auto *listener : this->level_listeners_) listener->on_log_level_change(level); #endif } Logger *global_logger = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esphome::logger