mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 15:46:54 +00:00
[nrf52,logger] add support for task_log_buffer_size (#13862)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
@@ -231,9 +231,16 @@ CONFIG_SCHEMA = cv.All(
|
||||
bk72xx=768,
|
||||
ln882x=768,
|
||||
rtl87xx=768,
|
||||
nrf52=768,
|
||||
): cv.All(
|
||||
cv.only_on(
|
||||
[PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX]
|
||||
[
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RTL87XX,
|
||||
PLATFORM_NRF52,
|
||||
]
|
||||
),
|
||||
cv.validate_bytes,
|
||||
cv.Any(
|
||||
@@ -313,11 +320,13 @@ async def to_code(config):
|
||||
)
|
||||
if CORE.is_esp32:
|
||||
cg.add(log.create_pthread_key())
|
||||
if CORE.is_esp32 or CORE.is_libretiny:
|
||||
if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52:
|
||||
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
|
||||
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")
|
||||
@@ -417,6 +426,7 @@ async def to_code(config):
|
||||
pass
|
||||
|
||||
if CORE.is_nrf52:
|
||||
zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True)
|
||||
if config[CONF_HARDWARE_UART] == UART0:
|
||||
zephyr_add_overlay("""&uart0 { status = "okay";};""")
|
||||
if config[CONF_HARDWARE_UART] == UART1:
|
||||
|
||||
190
esphome/components/logger/log_buffer.h
Normal file
190
esphome/components/logger/log_buffer.h
Normal file
@@ -0,0 +1,190 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
|
||||
static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
||||
|
||||
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
|
||||
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
|
||||
'\0', // NONE
|
||||
'1', // ERROR (31 = red)
|
||||
'3', // WARNING (33 = yellow)
|
||||
'2', // INFO (32 = green)
|
||||
'5', // CONFIG (35 = magenta)
|
||||
'6', // DEBUG (36 = cyan)
|
||||
'7', // VERBOSE (37 = gray)
|
||||
'8', // VERY_VERBOSE (38 = white)
|
||||
};
|
||||
|
||||
static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
|
||||
'\0', // NONE
|
||||
'E', // ERROR
|
||||
'W', // WARNING
|
||||
'I', // INFO
|
||||
'C', // CONFIG
|
||||
'D', // DEBUG
|
||||
'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
|
||||
};
|
||||
|
||||
// Buffer wrapper for log formatting functions
|
||||
struct LogBuffer {
|
||||
char *data;
|
||||
uint16_t size;
|
||||
uint16_t pos{0};
|
||||
// Replaces the null terminator with a newline for console output.
|
||||
// Must be called after notify_listeners_() since listeners need null-terminated strings.
|
||||
// Console output uses length-based writes (buf.pos), so null terminator is not needed.
|
||||
void terminate_with_newline() {
|
||||
if (this->pos < this->size) {
|
||||
this->data[this->pos++] = '\n';
|
||||
} else if (this->size > 0) {
|
||||
// Buffer was full - replace last char with newline to ensure it's visible
|
||||
this->data[this->size - 1] = '\n';
|
||||
this->pos = this->size;
|
||||
}
|
||||
}
|
||||
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
|
||||
// Early return if insufficient space - intentionally don't update pos to prevent partial writes
|
||||
if (this->pos + MAX_HEADER_SIZE > this->size)
|
||||
return;
|
||||
|
||||
char *p = this->current_();
|
||||
|
||||
// Write ANSI color
|
||||
this->write_ansi_color_(p, level);
|
||||
|
||||
// Construct: [LEVEL][tag:line]
|
||||
*p++ = '[';
|
||||
if (level != 0) {
|
||||
if (level >= 7) {
|
||||
*p++ = 'V'; // VERY_VERBOSE = "VV"
|
||||
*p++ = 'V';
|
||||
} else {
|
||||
*p++ = LOG_LEVEL_LETTER_CHARS[level];
|
||||
}
|
||||
}
|
||||
*p++ = ']';
|
||||
*p++ = '[';
|
||||
|
||||
// Copy tag
|
||||
this->copy_string_(p, tag);
|
||||
|
||||
*p++ = ':';
|
||||
|
||||
// Format line number without modulo operations
|
||||
if (line > 999) [[unlikely]] {
|
||||
int thousands = line / 1000;
|
||||
*p++ = '0' + thousands;
|
||||
line -= thousands * 1000;
|
||||
}
|
||||
int hundreds = line / 100;
|
||||
int remainder = line - hundreds * 100;
|
||||
int tens = remainder / 10;
|
||||
*p++ = '0' + hundreds;
|
||||
*p++ = '0' + tens;
|
||||
*p++ = '0' + (remainder - tens * 10);
|
||||
*p++ = ']';
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
|
||||
// Write thread name with bold red color
|
||||
if (thread_name != nullptr) {
|
||||
this->write_ansi_color_(p, 1); // Bold red for thread name
|
||||
*p++ = '[';
|
||||
this->copy_string_(p, thread_name);
|
||||
*p++ = ']';
|
||||
this->write_ansi_color_(p, level); // Restore original color
|
||||
}
|
||||
#endif
|
||||
|
||||
*p++ = ':';
|
||||
*p++ = ' ';
|
||||
|
||||
this->pos = p - this->data;
|
||||
}
|
||||
void HOT format_body(const char *format, va_list args) {
|
||||
this->format_vsnprintf_(format, args);
|
||||
this->finalize_();
|
||||
}
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
void HOT format_body_P(PGM_P format, va_list args) {
|
||||
this->format_vsnprintf_P_(format, args);
|
||||
this->finalize_();
|
||||
}
|
||||
#endif
|
||||
void write_body(const char *text, uint16_t text_length) {
|
||||
this->write_(text, text_length);
|
||||
this->finalize_();
|
||||
}
|
||||
|
||||
private:
|
||||
bool full_() const { return this->pos >= this->size; }
|
||||
uint16_t remaining_() const { return this->size - this->pos; }
|
||||
char *current_() { return this->data + this->pos; }
|
||||
void write_(const char *value, uint16_t length) {
|
||||
const uint16_t available = this->remaining_();
|
||||
const uint16_t copy_len = (length < available) ? length : available;
|
||||
if (copy_len > 0) {
|
||||
memcpy(this->current_(), value, copy_len);
|
||||
this->pos += copy_len;
|
||||
}
|
||||
}
|
||||
void finalize_() {
|
||||
// Write color reset sequence
|
||||
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
|
||||
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
|
||||
// Null terminate
|
||||
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
|
||||
}
|
||||
void strip_trailing_newlines_() {
|
||||
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
|
||||
this->pos--;
|
||||
}
|
||||
void process_vsnprintf_result_(int ret) {
|
||||
if (ret < 0)
|
||||
return;
|
||||
const uint16_t rem = this->remaining_();
|
||||
this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
|
||||
this->strip_trailing_newlines_();
|
||||
}
|
||||
void format_vsnprintf_(const char *format, va_list args) {
|
||||
if (this->full_())
|
||||
return;
|
||||
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
|
||||
}
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
void format_vsnprintf_P_(PGM_P format, va_list args) {
|
||||
if (this->full_())
|
||||
return;
|
||||
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
|
||||
}
|
||||
#endif
|
||||
// Write ANSI color escape sequence to buffer, updates pointer in place
|
||||
// Caller is responsible for ensuring buffer has sufficient space
|
||||
void write_ansi_color_(char *&p, uint8_t level) {
|
||||
if (level == 0)
|
||||
return;
|
||||
// Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
|
||||
*p++ = '\033';
|
||||
*p++ = '[';
|
||||
*p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
|
||||
*p++ = ';';
|
||||
*p++ = '3';
|
||||
*p++ = LOG_LEVEL_COLOR_DIGIT[level];
|
||||
*p++ = 'm';
|
||||
}
|
||||
// Copy string without null terminator, updates pointer in place
|
||||
// Caller is responsible for ensuring buffer has sufficient space
|
||||
void copy_string_(char *&p, const char *str) {
|
||||
const size_t len = strlen(str);
|
||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
|
||||
// piece
|
||||
memcpy(p, str, len);
|
||||
p += len;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::logger
|
||||
@@ -10,9 +10,9 @@ 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
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS,
|
||||
// Zephyr) 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
|
||||
@@ -31,6 +31,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
// 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_);
|
||||
#elif (USE_ZEPHYR)
|
||||
k_tid_t current_task = k_current_get();
|
||||
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
|
||||
@@ -54,6 +57,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
// Host: pass a stack buffer for pthread_getname_np to write into.
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
const char *thread_name = get_thread_name_(current_task);
|
||||
#elif defined(USE_ZEPHYR)
|
||||
char thread_name_buf[MAX_POINTER_REPRESENTATION];
|
||||
const char *thread_name = get_thread_name_(thread_name_buf, current_task);
|
||||
#else // USE_HOST
|
||||
char thread_name_buf[THREAD_NAME_BUF_SIZE];
|
||||
const char *thread_name = this->get_thread_name_(thread_name_buf);
|
||||
@@ -83,18 +89,21 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
||||
// This is safe to call from any context including ISRs
|
||||
this->enable_loop_soon_any_context();
|
||||
}
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
#endif
|
||||
// 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) {
|
||||
if (!message_sent)
|
||||
#else
|
||||
if (!message_sent && this->baud_rate_ > 0) // If logging is enabled, write to console
|
||||
#endif
|
||||
{
|
||||
#ifdef USE_HOST
|
||||
// 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
|
||||
@@ -107,22 +116,16 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
||||
// RAII guard automatically resets on return
|
||||
}
|
||||
#else
|
||||
// Implementation for single-task platforms (ESP8266, RP2040, Zephyr)
|
||||
// TODO: Zephyr may have multiple threads (work queues, etc.) but uses this single-task path.
|
||||
// Implementation for single-task platforms (ESP8266, RP2040)
|
||||
// Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking.
|
||||
// Not a problem in practice yet since Zephyr has no API support (logs are console-only).
|
||||
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;
|
||||
#ifdef USE_ZEPHYR
|
||||
char tmp[MAX_POINTER_REPRESENTATION];
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args,
|
||||
this->get_thread_name_(tmp));
|
||||
#else // Other single-task platforms don't have thread names, so pass nullptr
|
||||
// Other single-task platforms don't have thread names, so pass nullptr
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
#endif
|
||||
}
|
||||
#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY
|
||||
#endif // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// Implementation for ESP8266 with flash string support.
|
||||
@@ -163,19 +166,12 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
|
||||
}
|
||||
#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)
|
||||
// Zephyr needs loop working to check when CDC port is open
|
||||
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||
// 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_();
|
||||
@@ -183,52 +179,33 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
void Logger::loop() { this->process_messages_(); }
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC))
|
||||
void Logger::loop() {
|
||||
this->process_messages_();
|
||||
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)
|
||||
this->cdc_loop_();
|
||||
#endif
|
||||
}
|
||||
#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)) {
|
||||
uint16_t text_length;
|
||||
while (this->log_buffer_->borrow_message_main_loop(message, text_length)) {
|
||||
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);
|
||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name,
|
||||
message->text_data(), 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)
|
||||
// Zephyr needs loop working to check when CDC port is open
|
||||
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||
else {
|
||||
// No messages to process, disable loop if appropriate
|
||||
// This reduces overhead when there's no async logging activity
|
||||
|
||||
@@ -13,15 +13,11 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#ifdef USE_HOST
|
||||
#include "log_buffer.h"
|
||||
#include "task_log_buffer_host.h"
|
||||
#elif defined(USE_ESP32)
|
||||
#include "task_log_buffer_esp32.h"
|
||||
#elif defined(USE_LIBRETINY)
|
||||
#include "task_log_buffer_libretiny.h"
|
||||
#endif
|
||||
#endif
|
||||
#include "task_log_buffer_zephyr.h"
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_ESP8266)
|
||||
@@ -97,195 +93,10 @@ struct CStrCompare {
|
||||
};
|
||||
#endif
|
||||
|
||||
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
|
||||
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
|
||||
'\0', // NONE
|
||||
'1', // ERROR (31 = red)
|
||||
'3', // WARNING (33 = yellow)
|
||||
'2', // INFO (32 = green)
|
||||
'5', // CONFIG (35 = magenta)
|
||||
'6', // DEBUG (36 = cyan)
|
||||
'7', // VERBOSE (37 = gray)
|
||||
'8', // VERY_VERBOSE (38 = white)
|
||||
};
|
||||
|
||||
static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
|
||||
'\0', // NONE
|
||||
'E', // ERROR
|
||||
'W', // WARNING
|
||||
'I', // INFO
|
||||
'C', // CONFIG
|
||||
'D', // DEBUG
|
||||
'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
|
||||
};
|
||||
|
||||
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
|
||||
static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
||||
|
||||
// "0x" + 2 hex digits per byte + '\0'
|
||||
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
||||
|
||||
// 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;
|
||||
|
||||
// Buffer wrapper for log formatting functions
|
||||
struct LogBuffer {
|
||||
char *data;
|
||||
uint16_t size;
|
||||
uint16_t pos{0};
|
||||
// Replaces the null terminator with a newline for console output.
|
||||
// Must be called after notify_listeners_() since listeners need null-terminated strings.
|
||||
// Console output uses length-based writes (buf.pos), so null terminator is not needed.
|
||||
void terminate_with_newline() {
|
||||
if (this->pos < this->size) {
|
||||
this->data[this->pos++] = '\n';
|
||||
} else if (this->size > 0) {
|
||||
// Buffer was full - replace last char with newline to ensure it's visible
|
||||
this->data[this->size - 1] = '\n';
|
||||
this->pos = this->size;
|
||||
}
|
||||
}
|
||||
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
|
||||
// Early return if insufficient space - intentionally don't update pos to prevent partial writes
|
||||
if (this->pos + MAX_HEADER_SIZE > this->size)
|
||||
return;
|
||||
|
||||
char *p = this->current_();
|
||||
|
||||
// Write ANSI color
|
||||
this->write_ansi_color_(p, level);
|
||||
|
||||
// Construct: [LEVEL][tag:line]
|
||||
*p++ = '[';
|
||||
if (level != 0) {
|
||||
if (level >= 7) {
|
||||
*p++ = 'V'; // VERY_VERBOSE = "VV"
|
||||
*p++ = 'V';
|
||||
} else {
|
||||
*p++ = LOG_LEVEL_LETTER_CHARS[level];
|
||||
}
|
||||
}
|
||||
*p++ = ']';
|
||||
*p++ = '[';
|
||||
|
||||
// Copy tag
|
||||
this->copy_string_(p, tag);
|
||||
|
||||
*p++ = ':';
|
||||
|
||||
// Format line number without modulo operations
|
||||
if (line > 999) [[unlikely]] {
|
||||
int thousands = line / 1000;
|
||||
*p++ = '0' + thousands;
|
||||
line -= thousands * 1000;
|
||||
}
|
||||
int hundreds = line / 100;
|
||||
int remainder = line - hundreds * 100;
|
||||
int tens = remainder / 10;
|
||||
*p++ = '0' + hundreds;
|
||||
*p++ = '0' + tens;
|
||||
*p++ = '0' + (remainder - tens * 10);
|
||||
*p++ = ']';
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
|
||||
// Write thread name with bold red color
|
||||
if (thread_name != nullptr) {
|
||||
this->write_ansi_color_(p, 1); // Bold red for thread name
|
||||
*p++ = '[';
|
||||
this->copy_string_(p, thread_name);
|
||||
*p++ = ']';
|
||||
this->write_ansi_color_(p, level); // Restore original color
|
||||
}
|
||||
#endif
|
||||
|
||||
*p++ = ':';
|
||||
*p++ = ' ';
|
||||
|
||||
this->pos = p - this->data;
|
||||
}
|
||||
void HOT format_body(const char *format, va_list args) {
|
||||
this->format_vsnprintf_(format, args);
|
||||
this->finalize_();
|
||||
}
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
void HOT format_body_P(PGM_P format, va_list args) {
|
||||
this->format_vsnprintf_P_(format, args);
|
||||
this->finalize_();
|
||||
}
|
||||
#endif
|
||||
void write_body(const char *text, uint16_t text_length) {
|
||||
this->write_(text, text_length);
|
||||
this->finalize_();
|
||||
}
|
||||
|
||||
private:
|
||||
bool full_() const { return this->pos >= this->size; }
|
||||
uint16_t remaining_() const { return this->size - this->pos; }
|
||||
char *current_() { return this->data + this->pos; }
|
||||
void write_(const char *value, uint16_t length) {
|
||||
const uint16_t available = this->remaining_();
|
||||
const uint16_t copy_len = (length < available) ? length : available;
|
||||
if (copy_len > 0) {
|
||||
memcpy(this->current_(), value, copy_len);
|
||||
this->pos += copy_len;
|
||||
}
|
||||
}
|
||||
void finalize_() {
|
||||
// Write color reset sequence
|
||||
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
|
||||
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
|
||||
// Null terminate
|
||||
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
|
||||
}
|
||||
void strip_trailing_newlines_() {
|
||||
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
|
||||
this->pos--;
|
||||
}
|
||||
void process_vsnprintf_result_(int ret) {
|
||||
if (ret < 0)
|
||||
return;
|
||||
const uint16_t rem = this->remaining_();
|
||||
this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
|
||||
this->strip_trailing_newlines_();
|
||||
}
|
||||
void format_vsnprintf_(const char *format, va_list args) {
|
||||
if (this->full_())
|
||||
return;
|
||||
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
|
||||
}
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
void format_vsnprintf_P_(PGM_P format, va_list args) {
|
||||
if (this->full_())
|
||||
return;
|
||||
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
|
||||
}
|
||||
#endif
|
||||
// Write ANSI color escape sequence to buffer, updates pointer in place
|
||||
// Caller is responsible for ensuring buffer has sufficient space
|
||||
void write_ansi_color_(char *&p, uint8_t level) {
|
||||
if (level == 0)
|
||||
return;
|
||||
// Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
|
||||
*p++ = '\033';
|
||||
*p++ = '[';
|
||||
*p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
|
||||
*p++ = ';';
|
||||
*p++ = '3';
|
||||
*p++ = LOG_LEVEL_COLOR_DIGIT[level];
|
||||
*p++ = 'm';
|
||||
}
|
||||
// Copy string without null terminator, updates pointer in place
|
||||
// Caller is responsible for ensuring buffer has sufficient space
|
||||
void copy_string_(char *&p, const char *str) {
|
||||
const size_t len = strlen(str);
|
||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
|
||||
// piece
|
||||
memcpy(p, str, len);
|
||||
p += len;
|
||||
}
|
||||
};
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
/** Enum for logging UART selection
|
||||
*
|
||||
@@ -411,11 +222,14 @@ class Logger : public Component {
|
||||
bool &flag_;
|
||||
};
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
||||
#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_USB_CDC)
|
||||
void cdc_loop_();
|
||||
#endif
|
||||
void process_messages_();
|
||||
void write_msg_(const char *msg, uint16_t len);
|
||||
@@ -534,13 +348,7 @@ class Logger : public Component {
|
||||
std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners
|
||||
#endif
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#ifdef USE_HOST
|
||||
logger::TaskLogBufferHost *log_buffer_{nullptr}; // Allocated once, never freed
|
||||
#elif defined(USE_ESP32)
|
||||
logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed
|
||||
#elif defined(USE_LIBRETINY)
|
||||
logger::TaskLogBufferLibreTiny *log_buffer_{nullptr}; // Allocated once, never freed
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Group smaller types together at the end
|
||||
@@ -552,7 +360,7 @@ class Logger : public Component {
|
||||
#ifdef USE_LIBRETINY
|
||||
UARTSelection uart_{UART_SELECTION_DEFAULT};
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
||||
#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
|
||||
@@ -595,8 +403,10 @@ class Logger : public Component {
|
||||
}
|
||||
|
||||
#elif defined(USE_ZEPHYR)
|
||||
const char *HOT get_thread_name_(std::span<char> buff) {
|
||||
k_tid_t current_task = k_current_get();
|
||||
const char *HOT get_thread_name_(std::span<char> 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
|
||||
}
|
||||
@@ -635,7 +445,7 @@ class Logger : public Component {
|
||||
// Create RAII guard for non-main task recursion
|
||||
inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); }
|
||||
|
||||
#elif defined(USE_LIBRETINY)
|
||||
#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
|
||||
@@ -643,6 +453,8 @@ class Logger : public Component {
|
||||
// - 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_; }
|
||||
@@ -651,7 +463,8 @@ class Logger : public Component {
|
||||
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Zephyr needs loop working to check when CDC port is open
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||
// Disable loop when task buffer is empty (with USB CDC check on ESP32)
|
||||
inline void disable_loop_when_buffer_empty_() {
|
||||
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context()
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace esphome::logger {
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
void Logger::loop() {
|
||||
void Logger::cdc_loop_() {
|
||||
if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ TaskLogBuffer::~TaskLogBuffer() {
|
||||
}
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **text, void **received_token) {
|
||||
if (message == nullptr || text == nullptr || received_token == nullptr) {
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||
if (this->current_token_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -43,18 +43,19 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **
|
||||
}
|
||||
|
||||
LogMessage *msg = static_cast<LogMessage *>(received_item);
|
||||
*message = msg;
|
||||
*text = msg->text_data();
|
||||
*received_token = received_item;
|
||||
message = msg;
|
||||
text_length = msg->text_length;
|
||||
this->current_token_ = received_item;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TaskLogBuffer::release_message_main_loop(void *token) {
|
||||
if (token == nullptr) {
|
||||
void TaskLogBuffer::release_message_main_loop() {
|
||||
if (this->current_token_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
vRingbufferReturnItem(ring_buffer_, token);
|
||||
vRingbufferReturnItem(ring_buffer_, this->current_token_);
|
||||
this->current_token_ = nullptr;
|
||||
// Update counter to mark all messages as processed
|
||||
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
@@ -52,10 +52,10 @@ class TaskLogBuffer {
|
||||
~TaskLogBuffer();
|
||||
|
||||
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
|
||||
bool borrow_message_main_loop(LogMessage **message, const char **text, void **received_token);
|
||||
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||
|
||||
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
|
||||
void release_message_main_loop(void *token);
|
||||
void release_message_main_loop();
|
||||
|
||||
// Thread-safe - send a message to the ring buffer from any thread
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
@@ -78,6 +78,7 @@ class TaskLogBuffer {
|
||||
// Atomic counter for message tracking (only differences matter)
|
||||
std::atomic<uint16_t> message_counter_{0}; // Incremented when messages are committed
|
||||
mutable uint16_t last_processed_counter_{0}; // Tracks last processed message
|
||||
void *current_token_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
TaskLogBufferHost::TaskLogBufferHost(size_t slot_count) : slot_count_(slot_count) {
|
||||
TaskLogBuffer::TaskLogBuffer(size_t slot_count) : slot_count_(slot_count) {
|
||||
// Allocate message slots
|
||||
this->slots_ = std::make_unique<LogMessage[]>(slot_count);
|
||||
}
|
||||
|
||||
TaskLogBufferHost::~TaskLogBufferHost() {
|
||||
TaskLogBuffer::~TaskLogBuffer() {
|
||||
// unique_ptr handles cleanup automatically
|
||||
}
|
||||
|
||||
int TaskLogBufferHost::acquire_write_slot_() {
|
||||
int TaskLogBuffer::acquire_write_slot_() {
|
||||
// Try to reserve a slot using compare-and-swap
|
||||
size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed);
|
||||
|
||||
@@ -43,7 +43,7 @@ int TaskLogBufferHost::acquire_write_slot_() {
|
||||
}
|
||||
}
|
||||
|
||||
void TaskLogBufferHost::commit_write_slot_(int slot_index) {
|
||||
void TaskLogBuffer::commit_write_slot_(int slot_index) {
|
||||
// Mark the slot as ready for reading
|
||||
this->slots_[slot_index].ready.store(true, std::memory_order_release);
|
||||
|
||||
@@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) {
|
||||
}
|
||||
}
|
||||
|
||||
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// Acquire a slot
|
||||
int slot_index = this->acquire_write_slot_();
|
||||
if (slot_index < 0) {
|
||||
@@ -115,11 +115,7 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
|
||||
if (message == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
|
||||
size_t current_write = this->write_index_.load(std::memory_order_acquire);
|
||||
|
||||
@@ -134,11 +130,12 @@ bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*message = &msg;
|
||||
message = &msg;
|
||||
text_length = msg.text_length;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TaskLogBufferHost::release_message_main_loop() {
|
||||
void TaskLogBuffer::release_message_main_loop() {
|
||||
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
|
||||
|
||||
// Clear the ready flag
|
||||
|
||||
@@ -21,12 +21,12 @@ namespace esphome::logger {
|
||||
*
|
||||
* Threading Model: Multi-Producer Single-Consumer (MPSC)
|
||||
* - Multiple threads can safely call send_message_thread_safe() concurrently
|
||||
* - Only the main loop thread calls get_message_main_loop() and release_message_main_loop()
|
||||
* - Only the main loop thread calls borrow_message_main_loop() and release_message_main_loop()
|
||||
*
|
||||
* Producers (multiple threads) Consumer (main loop only)
|
||||
* │ │
|
||||
* ▼ ▼
|
||||
* acquire_write_slot_() get_message_main_loop()
|
||||
* acquire_write_slot_() bool borrow_message_main_loop()
|
||||
* CAS on reserve_index_ read write_index_
|
||||
* │ check ready flag
|
||||
* ▼ │
|
||||
@@ -48,7 +48,7 @@ namespace esphome::logger {
|
||||
* - Atomic CAS for slot reservation allows multiple producers without locks
|
||||
* - Single consumer (main loop) processes messages in order
|
||||
*/
|
||||
class TaskLogBufferHost {
|
||||
class TaskLogBuffer {
|
||||
public:
|
||||
// Default number of message slots - host has plenty of memory
|
||||
static constexpr size_t DEFAULT_SLOT_COUNT = 64;
|
||||
@@ -71,15 +71,16 @@ class TaskLogBufferHost {
|
||||
thread_name[0] = '\0';
|
||||
text[0] = '\0';
|
||||
}
|
||||
inline char *text_data() { return this->text; }
|
||||
};
|
||||
|
||||
/// Constructor that takes the number of message slots
|
||||
explicit TaskLogBufferHost(size_t slot_count);
|
||||
~TaskLogBufferHost();
|
||||
explicit TaskLogBuffer(size_t slot_count);
|
||||
~TaskLogBuffer();
|
||||
|
||||
// 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);
|
||||
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||
|
||||
// NOT thread-safe - release the message after processing, only call from main loop
|
||||
void release_message_main_loop();
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) {
|
||||
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
|
||||
this->size_ = total_buffer_size;
|
||||
// Allocate memory for the circular buffer using ESPHome's RAM allocator
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
@@ -17,7 +17,7 @@ TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) {
|
||||
this->mutex_ = xSemaphoreCreateMutex();
|
||||
}
|
||||
|
||||
TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
|
||||
TaskLogBuffer::~TaskLogBuffer() {
|
||||
if (this->mutex_ != nullptr) {
|
||||
vSemaphoreDelete(this->mutex_);
|
||||
this->mutex_ = nullptr;
|
||||
@@ -29,7 +29,7 @@ TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
|
||||
}
|
||||
}
|
||||
|
||||
size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
|
||||
size_t TaskLogBuffer::available_contiguous_space() const {
|
||||
if (this->head_ >= this->tail_) {
|
||||
// head is ahead of or equal to tail
|
||||
// Available space is from head to end, plus from start to tail
|
||||
@@ -47,11 +47,7 @@ size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
|
||||
}
|
||||
}
|
||||
|
||||
bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, const char **text) {
|
||||
if (message == nullptr || text == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||
// Check if buffer was initialized successfully
|
||||
if (this->mutex_ == nullptr || this->storage_ == nullptr) {
|
||||
return false;
|
||||
@@ -77,15 +73,15 @@ bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, cons
|
||||
this->tail_ = 0;
|
||||
msg = reinterpret_cast<LogMessage *>(this->storage_);
|
||||
}
|
||||
*message = msg;
|
||||
*text = msg->text_data();
|
||||
message = msg;
|
||||
text_length = msg->text_length;
|
||||
this->current_message_size_ = message_total_size(msg->text_length);
|
||||
|
||||
// Keep mutex held until release_message_main_loop()
|
||||
return true;
|
||||
}
|
||||
|
||||
void TaskLogBufferLibreTiny::release_message_main_loop() {
|
||||
void TaskLogBuffer::release_message_main_loop() {
|
||||
// Advance tail past the current message
|
||||
this->tail_ += this->current_message_size_;
|
||||
|
||||
@@ -100,8 +96,8 @@ void TaskLogBufferLibreTiny::release_message_main_loop() {
|
||||
xSemaphoreGive(this->mutex_);
|
||||
}
|
||||
|
||||
bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line,
|
||||
const char *thread_name, const char *format, va_list args) {
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace esphome::logger {
|
||||
* - Volatile counter enables fast has_messages() without lock overhead
|
||||
* - If message doesn't fit at end, padding is added and message wraps to start
|
||||
*/
|
||||
class TaskLogBufferLibreTiny {
|
||||
class TaskLogBuffer {
|
||||
public:
|
||||
// Structure for a log message header (text data follows immediately after)
|
||||
struct LogMessage {
|
||||
@@ -60,11 +60,11 @@ class TaskLogBufferLibreTiny {
|
||||
static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF;
|
||||
|
||||
// Constructor that takes a total buffer size
|
||||
explicit TaskLogBufferLibreTiny(size_t total_buffer_size);
|
||||
~TaskLogBufferLibreTiny();
|
||||
explicit TaskLogBuffer(size_t total_buffer_size);
|
||||
~TaskLogBuffer();
|
||||
|
||||
// NOT thread-safe - borrow a message from the buffer, only call from main loop
|
||||
bool borrow_message_main_loop(LogMessage **message, const char **text);
|
||||
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||
|
||||
// NOT thread-safe - release a message buffer, only call from main loop
|
||||
void release_message_main_loop();
|
||||
|
||||
116
esphome/components/logger/task_log_buffer_zephyr.cpp
Normal file
116
esphome/components/logger/task_log_buffer_zephyr.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "task_log_buffer_zephyr.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
__thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
static inline uint32_t total_size_in_32bit_words(uint16_t text_length) {
|
||||
// Calculate total size in 32-bit words needed (header + text length + null terminator + 3(4 bytes alignment)
|
||||
return (sizeof(TaskLogBuffer::LogMessage) + text_length + 1 + 3) / sizeof(uint32_t);
|
||||
}
|
||||
|
||||
static inline uint32_t get_wlen(const mpsc_pbuf_generic *item) {
|
||||
return total_size_in_32bit_words(reinterpret_cast<const TaskLogBuffer::LogMessage *>(item)->text_length);
|
||||
}
|
||||
|
||||
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
|
||||
// alignment to 4 bytes
|
||||
total_buffer_size = (total_buffer_size + 3) / sizeof(uint32_t);
|
||||
this->mpsc_config_.buf = new uint32_t[total_buffer_size];
|
||||
this->mpsc_config_.size = total_buffer_size;
|
||||
this->mpsc_config_.flags = MPSC_PBUF_MODE_OVERWRITE;
|
||||
this->mpsc_config_.get_wlen = get_wlen,
|
||||
|
||||
mpsc_pbuf_init(&this->log_buffer_, &this->mpsc_config_);
|
||||
}
|
||||
|
||||
TaskLogBuffer::~TaskLogBuffer() { delete[] this->mpsc_config_.buf; }
|
||||
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
int ret = vsnprintf(nullptr, 0, format, args_copy);
|
||||
va_end(args_copy);
|
||||
|
||||
if (ret <= 0) {
|
||||
return false; // Formatting error or empty message
|
||||
}
|
||||
|
||||
// Calculate actual text length (capped to maximum size)
|
||||
static constexpr size_t MAX_TEXT_SIZE = 255;
|
||||
size_t text_length = (static_cast<size_t>(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret;
|
||||
size_t total_size = total_size_in_32bit_words(text_length);
|
||||
auto *msg = reinterpret_cast<LogMessage *>(mpsc_pbuf_alloc(&this->log_buffer_, total_size, K_NO_WAIT));
|
||||
if (msg == nullptr) {
|
||||
return false;
|
||||
}
|
||||
msg->level = level;
|
||||
msg->tag = tag;
|
||||
msg->line = line;
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
||||
|
||||
// Format the message text directly into the acquired memory
|
||||
// We add 1 to text_length to ensure space for null terminator during formatting
|
||||
char *text_area = msg->text_data();
|
||||
ret = vsnprintf(text_area, text_length + 1, format, args);
|
||||
|
||||
// Handle unexpected formatting error (ret < 0 is encoding error; ret == 0 is valid empty output)
|
||||
if (ret < 0) {
|
||||
// this should not happen, vsnprintf was called already once
|
||||
// fill with '\n' to not call mpsc_pbuf_free from producer
|
||||
// it will be trimmed anyway
|
||||
for (size_t i = 0; i < text_length; ++i) {
|
||||
text_area[i] = '\n';
|
||||
}
|
||||
text_area[text_length] = 0;
|
||||
// do not return false to free the buffer from main thread
|
||||
}
|
||||
|
||||
msg->text_length = text_length;
|
||||
|
||||
mpsc_pbuf_commit(&this->log_buffer_, reinterpret_cast<mpsc_pbuf_generic *>(msg));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||
if (this->current_token_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->current_token_ = mpsc_pbuf_claim(&this->log_buffer_);
|
||||
|
||||
if (this->current_token_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// we claimed buffer already, const_cast is safe here
|
||||
message = const_cast<LogMessage *>(reinterpret_cast<const LogMessage *>(this->current_token_));
|
||||
|
||||
text_length = message->text_length;
|
||||
// Remove trailing newlines
|
||||
while (text_length > 0 && message->text_data()[text_length - 1] == '\n') {
|
||||
text_length--;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TaskLogBuffer::release_message_main_loop() {
|
||||
if (this->current_token_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
mpsc_pbuf_free(&this->log_buffer_, this->current_token_);
|
||||
this->current_token_ = nullptr;
|
||||
}
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif // USE_ZEPHYR
|
||||
66
esphome/components/logger/task_log_buffer_zephyr.h
Normal file
66
esphome/components/logger/task_log_buffer_zephyr.h
Normal file
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <zephyr/sys/mpsc_pbuf.h>
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
// "0x" + 2 hex digits per byte + '\0'
|
||||
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
||||
|
||||
extern __thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
class TaskLogBuffer {
|
||||
public:
|
||||
// Structure for a log message header (text data follows immediately after)
|
||||
struct LogMessage {
|
||||
MPSC_PBUF_HDR; // this is only 2 bits but no more than 30 bits directly after
|
||||
uint16_t line; // Source code line number
|
||||
uint8_t level; // Log level (0-7)
|
||||
#if defined(CONFIG_THREAD_NAME)
|
||||
char thread_name[CONFIG_THREAD_MAX_NAME_LEN]; // Store thread name directly (only used for non-main threads)
|
||||
#else
|
||||
char thread_name[MAX_POINTER_REPRESENTATION]; // Store thread name directly (only used for non-main threads)
|
||||
#endif
|
||||
const char *tag; // We store the pointer, assuming tags are static
|
||||
uint16_t text_length; // Length of the message text (up to ~64KB)
|
||||
|
||||
// Methods for accessing message contents
|
||||
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); }
|
||||
};
|
||||
// Constructor that takes a total buffer size
|
||||
explicit TaskLogBuffer(size_t total_buffer_size);
|
||||
~TaskLogBuffer();
|
||||
|
||||
// Check if there are messages ready to be processed using an atomic counter for performance
|
||||
inline bool HOT has_messages() { return mpsc_pbuf_is_pending(&this->log_buffer_); }
|
||||
|
||||
// Get the total buffer size in bytes
|
||||
inline size_t size() const { return this->mpsc_config_.size * sizeof(uint32_t); }
|
||||
|
||||
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
|
||||
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||
|
||||
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
|
||||
void release_message_main_loop();
|
||||
|
||||
// Thread-safe - send a message to the ring buffer from any thread
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args);
|
||||
|
||||
protected:
|
||||
mpsc_pbuf_buffer_config mpsc_config_{};
|
||||
mpsc_pbuf_buffer log_buffer_{};
|
||||
const mpsc_pbuf_generic *current_token_{};
|
||||
};
|
||||
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif // USE_ZEPHYR
|
||||
@@ -320,6 +320,7 @@
|
||||
#endif
|
||||
|
||||
#ifdef USE_NRF52
|
||||
#define USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#define USE_NRF52_DFU
|
||||
#define USE_NRF52_REG0_VOUT 5
|
||||
#define USE_NRF52_UICR_ERASE
|
||||
|
||||
@@ -5,3 +5,4 @@ esphome:
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
task_log_buffer_size: 0
|
||||
|
||||
Reference in New Issue
Block a user