Compare commits

..
35 changed files with 681 additions and 668 deletions

No files matched your search

+42 -21
View File
@@ -1,6 +1,7 @@
from collections.abc import Awaitable, Callable, Sequence
from contextvars import ContextVar
from dataclasses import dataclass, field
from functools import partial
import logging
import math
import re
@@ -1151,9 +1152,6 @@ IfAction = cg.esphome_ns.class_("IfAction", Action)
WhileAction = cg.esphome_ns.class_("WhileAction", Action)
RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
WaitUntilAction = cg.esphome_ns.class_("WaitUntilAction", Action, cg.Component)
UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action)
SuspendComponentAction = cg.esphome_ns.class_("SuspendComponentAction", Action)
ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
Automation = cg.esphome_ns.class_("Automation")
TriggerForwarder = cg.esphome_ns.class_("TriggerForwarder")
TriggerOnTrueForwarder = cg.esphome_ns.class_("TriggerOnTrueForwarder")
@@ -1771,25 +1769,44 @@ async def lambda_action_to_code(
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
register_simple_action(
"component.update",
UpdateComponentAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
synchronous=True,
_POLLING_COMPONENT_SCHEMA = maybe_simple_id(
{cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}
)
register_simple_action(
"component.suspend",
SuspendComponentAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
synchronous=True,
)
async def _polling_component_action(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
calls: list[str],
) -> MockObj:
"""Run ``calls`` on the component once it is ready, through ``PollingComponent``.
An if block, not an early return, so actions merged after this one still run.
"""
parent = await _apply_parent(config)
statements = [
f"{cg.PollingComponent} &esphome__component = *{parent};",
"if (esphome__component.is_ready()) {",
*(f" esphome__component.{call};" for call in calls),
"}",
]
return apply_statements_action(action_id, template_arg, args, statements)
for _name, _call in (
("component.update", "update()"),
("component.suspend", "stop_poller()"),
):
register_action(_name, ApplyAction, _POLLING_COMPONENT_SCHEMA, synchronous=True)(
partial(_polling_component_action, calls=[_call])
)
@register_action(
"component.resume",
ResumeComponentAction,
ApplyAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
@@ -1806,12 +1823,16 @@ async def component_resume_action_to_code(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, comp)
if CONF_UPDATE_INTERVAL in config:
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, cg.uint32)
cg.add(var.set_update_interval(template_))
return var
calls: list[str] = []
if (interval := config.get(CONF_UPDATE_INTERVAL)) is not None:
if isinstance(interval, Lambda):
# Built with the trigger's own argument types and always called, so the body
# may modify its by-value arguments
lamb = await cg.process_lambda(interval, args, return_type=cg.uint32)
interval = CallExpression(lamb, *(MockObj(name) for _, name in args))
calls.append(f"set_update_interval({cg.safe_exp(interval)})")
calls.append("start_poller()")
return await _polling_component_action(config, action_id, template_arg, args, calls)
async def build_action(
+55 -36
View File
@@ -37,7 +37,6 @@ from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@esphome/core"]
DOMAIN = "logger"
logger_ns = cg.esphome_ns.namespace("logger")
LOG_LEVELS = {
"NONE": cg.global_ns.ESPHOME_LOG_LEVEL_NONE,
@@ -94,7 +93,6 @@ UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1]
UART_SELECTION_NRF52 = [USB_CDC, UART0]
# Read by device-builder
HARDWARE_UART_TO_UART_SELECTION = {
UART0: logger_ns.UART_SELECTION_UART0,
UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP,
@@ -110,6 +108,8 @@ HARDWARE_UART_TO_SERIAL = {
UART0: cg.global_ns.Serial,
UART0_SWAP: cg.global_ns.Serial,
UART1: cg.global_ns.Serial1,
UART2: cg.global_ns.Serial2,
DEFAULT: cg.global_ns.Serial,
},
PLATFORM_RP2: {
UART0: cg.global_ns.Serial1,
@@ -503,10 +503,13 @@ async def to_code(config: ConfigType) -> None:
CORE.data.setdefault(DOMAIN, {})[CONF_LEVEL] = level
tx_buffer_size = config[CONF_TX_BUFFER_SIZE]
cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size)
# The host has a fixed 64 slots
task_log_buffer_size = (
64 if CORE.is_host else config.get(CONF_TASK_LOG_BUFFER_SIZE, 0)
)
# Determine task log buffer size. The buffer is a direct member of Logger
# (no separate heap allocation).
task_log_buffer_size = 0
if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52:
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
elif CORE.is_host:
task_log_buffer_size = 64 # Fixed 64 slots for host
if task_log_buffer_size > 0:
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
cg.add_define("ESPHOME_TASK_LOG_BUFFER_SIZE", task_log_buffer_size)
@@ -539,17 +542,17 @@ async def to_code(config: ConfigType) -> None:
cg.add(log.set_boot_log_level(tag, LOG_LEVELS[log_level]))
if CORE.is_esp32 or CORE.is_host:
cg.add(log.create_pthread_key())
if (hardware_uart := config.get(CONF_HARDWARE_UART)) is not None:
cg.add_define(f"USE_LOGGER_UART_SELECTION_{hardware_uart}")
cg.add_define(
"ESPHOME_LOGGER_UART_SELECTION",
cg.RawExpression(f"logger::UART_SELECTION_{hardware_uart}"),
)
# ESP8266 without serial logging never opens a port
cg.add_define(
"ESPHOME_LOGGER_UART_NAME",
"NONE" if CORE.is_esp8266 and baud_rate == 0 else hardware_uart,
)
# set_uart_selection() must be called before pre_setup() because
# pre_setup() switches on uart_ to decide which hardware to initialize
# (e.g. UART0 vs USB_SERIAL_JTAG). Without this, uart_ is still the
# default UART_SELECTION_UART0 and the wrong hardware gets initialized.
# uart_ is UART0 in C++ except on LibreTiny where it is DEFAULT; skip the
# setter when the config matches it.
cpp_default_uart = DEFAULT if CORE.is_libretiny else UART0
if (
hardware_uart := config.get(CONF_HARDWARE_UART)
) is not None and hardware_uart != cpp_default_uart:
cg.add(log.set_uart_selection(HARDWARE_UART_TO_UART_SELECTION[hardware_uart]))
# pre_setup() sets global_logger and must run before any other code
# that may call ESP_LOG* (e.g. setup_preferences contains ESP_LOGVV).
cg.add(log.pre_setup())
@@ -644,8 +647,10 @@ async def _late_logger_init(config: ConfigType) -> None:
if config[CONF_HARDWARE_UART] == USB_CDC:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True)
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG")
# Log V2 formats centrally in esp_log() (~4KB flash, ~180B RAM). 6.1
# adds CONSTRAINED_ENV_SAFE=n, keeping early/DRAM logs on
# esp_rom_printf instead of pulling ~1.2KB esp_rom_vprintf into IRAM.
@@ -662,13 +667,8 @@ async def _late_logger_init(config: ConfigType) -> None:
cg.add_define("USE_ESP32_LOG_V2")
# --wrap: a strong definition collides; see logger_esp32.cpp
cg.add_build_flag("-Wl,--wrap=esp_log_format")
# Zephyr polls the USB port only when it logs there
if (
CORE.using_zephyr
and has_serial_logging
and config[CONF_HARDWARE_UART] == USB_CDC
):
cg.add_define("USE_LOGGER_ZEPHYR_CDC_LOOP")
if CORE.is_rp2 and config[CONF_HARDWARE_UART] == USB_CDC:
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
# Define platform support flags for components that need auto-detection
try:
uart_selection(USB_SERIAL_JTAG)
@@ -718,6 +718,7 @@ async def _late_logger_init(config: ConfigType) -> None:
if config[CONF_HARDWARE_UART] == UART1:
zephyr_add_overlay("""&uart1 { status = "okay";};""")
if config[CONF_HARDWARE_UART] == USB_CDC:
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
zephyr_add_prj_conf("UART_LINE_CTRL", True)
# Register at end for safe mode
@@ -738,6 +739,8 @@ async def _late_logger_init(config: ConfigType) -> None:
conf,
)
CORE.add_job(final_step)
def validate_printf(value: ConfigType) -> ConfigType:
# https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python
@@ -872,6 +875,11 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
}
)
# Keys for CORE.data storage
DOMAIN = "logger"
KEY_LEVEL_LISTENERS = "level_listeners"
KEY_LOG_LISTENERS = "log_listeners"
def request_logger_level_listeners() -> None:
"""Request that logger level listeners be compiled in.
@@ -880,18 +888,29 @@ def request_logger_level_listeners() -> None:
function during their code generation. This enables the add_level_listener()
method and compiles in the listener vector.
"""
cg.add_define("USE_LOGGER_LEVEL_LISTENERS")
_request_log_listener_slot = cg.slot_counter("ESPHOME_LOG_MAX_LISTENERS")
def request_console() -> None:
"""Compile in Logger::console_read() and console_write(); call from to_code."""
cg.add_define("USE_LOGGER_CONSOLE")
CORE.data.setdefault(DOMAIN, {})[KEY_LEVEL_LISTENERS] = True
def request_log_listener() -> None:
"""Reserve a log listener slot; call once per listener from to_code."""
cg.add_define("USE_LOG_LISTENERS")
_request_log_listener_slot()
"""Request a log listener slot.
Components that need to receive log messages should call this function
during their code generation. This increments the listener count used
to size the StaticVector.
"""
data = CORE.data.setdefault(DOMAIN, {})
data[KEY_LOG_LISTENERS] = data.get(KEY_LOG_LISTENERS, 0) + 1
@coroutine_with_priority(CoroPriority.FINAL)
async def final_step() -> None:
"""Final code generation step to configure optional logger features."""
domain_data = CORE.data.get(DOMAIN, {})
if domain_data.get(KEY_LEVEL_LISTENERS, False):
cg.add_define("USE_LOGGER_LEVEL_LISTENERS")
# Only generate log listener code if any component needs it
log_listener_count = domain_data.get(KEY_LOG_LISTENERS, 0)
if log_listener_count > 0:
cg.add_define("USE_LOG_LISTENERS")
cg.add_define("ESPHOME_LOG_MAX_LISTENERS", log_listener_count)
+20 -37
View File
@@ -32,7 +32,7 @@ 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 defined(USE_ZEPHYR)
#elif (USE_ZEPHYR)
k_tid_t current_task = k_current_get();
const bool is_main_task = (current_task == this->main_task_);
#else // USE_HOST
@@ -57,10 +57,10 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
// (we already have the handle from the main task check above).
// Host: pass a stack buffer for pthread_getname_np to write into.
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
const char *thread_name = this->get_thread_name_(current_task);
const char *thread_name = get_thread_name_(current_task);
#elif defined(USE_ZEPHYR)
char thread_name_buf[MAX_POINTER_REPRESENTATION];
const char *thread_name = this->get_thread_name_(thread_name_buf, current_task);
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);
@@ -103,10 +103,10 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
{
#ifdef USE_HOST
// Host always has console output - no baud_rate check needed
static constexpr size_t MAX_CONSOLE_LOG_MSG_SIZE = 512;
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512;
#else
// Maximum size for console log messages (includes null terminator)
static constexpr size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
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};
@@ -118,11 +118,13 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
}
#else
// Implementation for single-task platforms (ESP8266, RP2040)
// Logging calls are NOT thread-safe: the recursion guard is a plain bool and tx_buffer_ has no locking.
// 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) || this->main_task_recursion_guard_)
if (level > this->level_for(tag) || global_recursion_guard_)
return;
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, 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 // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR
@@ -134,10 +136,10 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
//
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) || this->main_task_recursion_guard_)
if (level > this->level_for(tag) || global_recursion_guard_)
return;
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr);
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
}
#endif // USE_ESP8266
@@ -182,10 +184,10 @@ Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) {
#endif
}
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || defined(USE_LOGGER_ZEPHYR_CDC_LOOP)
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
void Logger::loop() {
this->process_messages_();
#ifdef USE_LOGGER_ZEPHYR_CDC_LOOP
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)
this->cdc_loop_();
#endif
}
@@ -211,11 +213,11 @@ void Logger::process_messages_() {
this->write_log_buffer_to_console_(buf);
}
}
// Zephyr with USB CDC keeps the loop running for cdc_loop_()
#ifndef USE_LOGGER_ZEPHYR_CDC_LOOP
#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
else {
// A concurrent enable_loop_soon_any_context() still wakes the next iteration
this->disable_loop();
// 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
@@ -263,7 +265,7 @@ void Logger::boot_log_capture_(uint8_t level, const char *tag, const LogBuffer &
void Logger::replay_boot_logs() {
// The replay shares tx_buffer_ with normal logging, so it is dropped like any other re-entrant log
bool &recursion_guard = this->main_task_recursion_guard_;
bool &recursion_guard = this->main_loop_recursion_guard_();
if (recursion_guard)
return;
this->boot_log_done_ = true; // Stops capture first, so the replay output is not buffered again
@@ -305,25 +307,6 @@ static const LogString *get_log_level_str(uint8_t level) {
return LogLevelStrings::get_log_str(level, LogLevelStrings::LAST_INDEX);
}
#ifndef USE_HOST
const LogString *Logger::get_uart_selection_() {
#ifdef USE_LOGGER_UART_SELECTION_DEFAULT
// LibreTiny resolves DEFAULT to the board's port only when serial logging is on
if (this->baud_rate_ > 0) {
switch (this->get_uart()) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
default:
return LOG_STR("UART2");
}
}
#endif
return LOG_STR(ESPHOME_LOGGER_UART_NAME);
}
#endif
void Logger::dump_config() {
ESP_LOGCONFIG(TAG,
"Logger:\n"
@@ -359,7 +342,7 @@ void Logger::dump_config() {
}
#endif
#ifdef USE_ZEPHYR
this->dump_crash_();
dump_crash_();
#ifdef USE_BOOTLOADER_ADAFRUIT
this->dump_bootloader_();
#endif
+84 -74
View File
@@ -42,6 +42,9 @@ struct device;
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():
@@ -70,7 +73,7 @@ struct LogCallback {
* public:
* void setup() override {
* if (logger::global_logger != nullptr)
* logger::global_logger->add_level_listener(this);
* logger::global_logger->add_logger_level_listener(this);
* }
* void on_log_level_change(uint8_t level) override {
* // Handle log level change
@@ -124,9 +127,6 @@ enum UARTSelection : uint8_t {
UART_SELECTION_UART0_SWAP,
#endif // USE_ESP8266
};
// The port chosen by hardware_uart, fixed at build time
static constexpr UARTSelection CONFIGURED_UART_SELECTION = ESPHOME_LOGGER_UART_SELECTION;
#endif // USE_ESP32 || USE_ESP8266 || USE_RP2 || USE_LIBRETINY || USE_ZEPHYR
/**
@@ -138,7 +138,7 @@ static constexpr UARTSelection CONFIGURED_UART_SELECTION = ESPHOME_LOGGER_UART_S
* - 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 single-task platforms: Uses the main task guard alone
* - 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:
@@ -149,7 +149,7 @@ static constexpr UARTSelection CONFIGURED_UART_SELECTION = ESPHOME_LOGGER_UART_S
class Logger final : public Component {
public:
explicit Logger(uint32_t baud_rate);
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || defined(USE_LOGGER_ZEPHYR_CDC_LOOP)
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
void loop() override;
#endif
#if defined(USE_RP2_CRASH_HANDLER) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)
@@ -157,52 +157,21 @@ class Logger final : public Component {
#endif
/// Manually set the baud rate for serial, set to 0 to disable.
void set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
uint32_t get_baud_rate() const { return this->baud_rate_; }
uint32_t get_baud_rate() const { return baud_rate_; }
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
Stream *get_hw_serial() const { return this->hw_serial_; }
Stream *get_hw_serial() const { return hw_serial_; }
#endif
#ifdef USE_ESP32
/// UART_NUM_0 when logging to USB
constexpr uart_port_t get_uart_num() const {
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
return UART_NUM_0;
#else
return static_cast<uart_port_t>(CONFIGURED_UART_SELECTION);
uart_port_t get_uart_num() const { return uart_num_; }
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
#endif
}
#endif
#if defined(USE_ESP32) || defined(USE_HOST)
void create_pthread_key() { pthread_key_create(&this->log_recursion_key_, nullptr); }
#ifdef USE_HOST
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
#endif
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
/// Get the UART used by the logger.
#ifdef USE_LIBRETINY
// DEFAULT resolves to the board's default port
constexpr UARTSelection get_uart() const {
return CONFIGURED_UART_SELECTION == UART_SELECTION_DEFAULT ? static_cast<UARTSelection>(LT_UART_DEFAULT_SERIAL + 1)
: CONFIGURED_UART_SELECTION;
}
#else
constexpr UARTSelection get_uart() const { return CONFIGURED_UART_SELECTION; }
#endif
#endif
#ifdef USE_LOGGER_CONSOLE
// Raw bytes on the console port, without log framing; request with logger.request_console()
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
size_t console_write(const uint8_t *data, size_t len) { return this->hw_serial_->write(data, len); }
bool console_read(uint8_t &byte) {
int c = this->hw_serial_->read();
if (c < 0)
return false;
byte = c;
return true;
}
#elif defined(USE_ESP32)
/// Blocking write; returns the bytes written.
size_t console_write(const uint8_t *data, size_t len);
/// Non-blocking read of one byte; false if none is waiting.
bool console_read(uint8_t &byte);
#endif
UARTSelection get_uart() const { return this->uart_; }
#endif
/// Set the default log level for this logger.
@@ -273,17 +242,14 @@ class Logger final : public Component {
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
#ifdef USE_LOGGER_ZEPHYR_CDC_LOOP
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)
void cdc_loop_();
#endif
void process_messages_();
// msg already ends in a newline
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
inline void HOT write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); }
#elif defined(USE_ESP32)
inline void write_msg_(const char *msg, uint16_t len); // Defined in logger_esp32.h
#else
#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
@@ -351,6 +317,7 @@ class Logger final : public Component {
#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) {
@@ -392,8 +359,15 @@ class Logger final : public Component {
#ifdef USE_HOST
pthread_t main_thread_{}; // Main thread for pthread_equal() comparison
#endif
#if defined(USE_ESP32) || defined(USE_HOST)
// Non-main task recursion guard (pthread TLS)
#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
@@ -418,10 +392,23 @@ class Logger final : public Component {
bool boot_log_done_{false}; // Set on first replay; stops further capture
#endif
uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE};
bool main_task_recursion_guard_{false}; // Guards tx_buffer_ on the main loop
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_ZEPHYR)
UARTSelection uart_{UART_SELECTION_UART0}; // Must match cpp_default_uart in __init__.py
#endif
#ifdef USE_LIBRETINY
UARTSelection uart_{UART_SELECTION_DEFAULT}; // Must match cpp_default_uart in __init__.py
#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
// The guard that protects tx_buffer_ on the main loop, for callers that run on any platform
bool &main_loop_recursion_guard_() { return this->main_task_recursion_guard_; }
#else
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
bool &main_loop_recursion_guard_() { return this->global_recursion_guard_; }
#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
@@ -490,9 +477,9 @@ class Logger final : public Component {
class NonMainTaskRecursionGuard {
public:
explicit NonMainTaskRecursionGuard(pthread_key_t key) : key_(key) {
pthread_setspecific(this->key_, reinterpret_cast<void *>(1));
pthread_setspecific(key_, reinterpret_cast<void *>(1));
}
~NonMainTaskRecursionGuard() { pthread_setspecific(this->key_, nullptr); }
~NonMainTaskRecursionGuard() { pthread_setspecific(key_, nullptr); }
NonMainTaskRecursionGuard(const NonMainTaskRecursionGuard &) = delete;
NonMainTaskRecursionGuard &operator=(const NonMainTaskRecursionGuard &) = delete;
NonMainTaskRecursionGuard(NonMainTaskRecursionGuard &&) = delete;
@@ -503,24 +490,40 @@ class Logger final : public Component {
};
// Check if non-main task is already in recursion (via TLS)
inline bool HOT is_non_main_task_recursive_() const {
return pthread_getspecific(this->log_recursion_key_) != nullptr;
}
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(this->log_recursion_key_);
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();
}
#elif defined(USE_LIBRETINY)
// No FreeRTOS TLS on LibreTiny: all non-main tasks share one guard
inline bool HOT is_non_main_task_recursive_() const { return this->non_main_task_recursion_guard_; }
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(this->non_main_task_recursion_guard_); }
#elif defined(USE_ZEPHYR)
// Thread-local guard (__thread)
inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard; }
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard); }
#endif
};
extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -560,7 +563,14 @@ void log_crash_to_client();
} // namespace esphome::logger
// ESP32 inline write_msg_(), included after the Logger class definition is complete
#ifdef USE_ESP32
// 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_RP2)
#include "logger_rp2.h"
#elif defined(USE_LIBRETINY)
#include "logger_libretiny.h"
#endif
+47 -43
View File
@@ -17,9 +17,6 @@
#include <driver/usb_serial_jtag_vfs.h>
#endif
#endif
#if defined(USE_LOGGER_CONSOLE) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
#include <esp_private/usb_console.h>
#endif
#if defined(CONFIG_PM_ENABLE) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) && \
(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0))
#include "esp_sleep.h"
@@ -76,8 +73,7 @@ static void init_usb_serial_jtag() {
}
#endif
#if !defined(USE_LOGGER_UART_SELECTION_USB_CDC) && !defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
static void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) {
void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) {
uart_config_t uart_config{};
uart_config.baud_rate = (int) baud_rate;
uart_config.data_bits = UART_DATA_8_BITS;
@@ -101,22 +97,37 @@ static void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_si
esp_sleep_set_console_uart_handling_mode(ESP_SLEEP_ALWAYS_FLUSH_UART);
#endif
}
#endif
// get_uart_num() and pre_setup() cast the selection straight to a port
static_assert(static_cast<int>(UART_SELECTION_UART0) == static_cast<int>(UART_NUM_0) &&
static_cast<int>(UART_SELECTION_UART1) == static_cast<int>(UART_NUM_1));
#ifdef USE_ESP32_VARIANT_ESP32
static_assert(static_cast<int>(UART_SELECTION_UART2) == static_cast<int>(UART_NUM_2));
#endif
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
#ifdef USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG
init_usb_serial_jtag();
#elif !defined(USE_LOGGER_UART_SELECTION_USB_CDC)
init_uart(this->get_uart_num(), this->baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE);
this->uart_num_ = UART_NUM_0;
switch (this->uart_) {
case UART_SELECTION_UART0:
this->uart_num_ = UART_NUM_0;
init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE);
break;
case UART_SELECTION_UART1:
this->uart_num_ = UART_NUM_1;
init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE);
break;
#ifdef USE_ESP32_VARIANT_ESP32
case UART_SELECTION_UART2:
this->uart_num_ = UART_NUM_2;
init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE);
break;
#endif
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
break;
#endif
#ifdef USE_LOGGER_USB_SERIAL_JTAG
case UART_SELECTION_USB_SERIAL_JTAG:
#ifdef USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG
init_usb_serial_jtag();
#endif
break;
#endif
}
}
global_logger = this;
@@ -128,36 +139,29 @@ void Logger::pre_setup() {
#endif
}
#ifdef USE_LOGGER_CONSOLE
size_t Logger::console_write(const uint8_t *data, size_t len) {
int written = 0;
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC)
#ifdef CONFIG_ESP_CONSOLE_USB_CDC
written = esp_usb_console_write_buf(reinterpret_cast<const char *>(data), len);
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
#ifdef USE_ESP32_VARIANT_ESP32
case UART_SELECTION_UART2:
return LOG_STR("UART2");
#endif
#elif defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
written = usb_serial_jtag_write_bytes(data, len, 20 / portTICK_PERIOD_MS);
#else
written = uart_write_bytes(this->get_uart_num(), data, len);
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
return LOG_STR("USB_CDC");
#endif
return written > 0 ? written : 0;
#ifdef USE_LOGGER_USB_SERIAL_JTAG
case UART_SELECTION_USB_SERIAL_JTAG:
return LOG_STR("USB_SERIAL_JTAG");
#endif
default:
return LOG_STR("UNKNOWN");
}
}
bool Logger::console_read(uint8_t &byte) {
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC)
#ifdef CONFIG_ESP_CONSOLE_USB_CDC
return esp_usb_console_available_for_read() && esp_usb_console_read_buf(reinterpret_cast<char *>(&byte), 1) == 1;
#else
return false;
#endif
#elif defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
return usb_serial_jtag_read_bytes(&byte, 1, 0) > 0;
#else
return uart_read_bytes(this->get_uart_num(), &byte, 1, 0) > 0;
#endif
}
#endif
#ifdef USE_CRASH_HANDLER
bool crash_log_pending() { return esp32::crash_handler_has_data(); }
void log_crash_to_client() {
+1 -1
View File
@@ -19,7 +19,7 @@ inline void HOT Logger::write_msg_(const char *msg, uint16_t len) {
fwrite(msg, 1, len, stdout);
#else
// Regular UART - single write including newline (already in buffer)
uart_write_bytes(this->get_uart_num(), msg, len);
uart_write_bytes(this->uart_num_, msg, len);
#endif
}
+16 -3
View File
@@ -14,9 +14,9 @@ void Logger::pre_setup() {
#if defined(USE_ESP8266_LOGGER_SERIAL)
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
#ifdef USE_LOGGER_UART_SELECTION_UART0_SWAP
Serial.swap();
#endif
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
Serial.swap();
}
Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
#elif defined(USE_ESP8266_LOGGER_SERIAL1)
this->hw_serial_ = &Serial1;
@@ -35,6 +35,19 @@ void Logger::pre_setup() {
#endif
}
const LogString *Logger::get_uart_selection_() {
#if defined(USE_ESP8266_LOGGER_SERIAL)
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
return LOG_STR("UART0_SWAP");
}
return LOG_STR("UART0");
#elif defined(USE_ESP8266_LOGGER_SERIAL1)
return LOG_STR("UART1");
#else
return LOG_STR("NONE");
#endif
}
#ifdef USE_CRASH_HANDLER
bool crash_log_pending() { return esp8266::crash_handler_has_data(); }
void log_crash_to_client() { esp8266::crash_handler_log(); }
@@ -0,0 +1,13 @@
#pragma once
#ifdef USE_ESP8266
#include "esphome/core/helpers.h"
namespace esphome::logger {
// Single write with newline already in buffer (added by caller)
inline void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); }
} // namespace esphome::logger
#endif
+51 -20
View File
@@ -12,28 +12,45 @@ ESPHOME_LOG_TAG(TAG, "logger");
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
#if defined(USE_LOGGER_UART_SELECTION_UART0) && LT_HW_UART0
this->hw_serial_ = &Serial0;
libretiny::ensure_serial_handle(0);
Serial0.begin(this->baud_rate_);
#elif defined(USE_LOGGER_UART_SELECTION_UART1) && LT_HW_UART1
this->hw_serial_ = &Serial1;
libretiny::ensure_serial_handle(1);
Serial1.begin(this->baud_rate_);
#elif defined(USE_LOGGER_UART_SELECTION_UART2) && LT_HW_UART2
this->hw_serial_ = &Serial2;
libretiny::ensure_serial_handle(2);
Serial2.begin(this->baud_rate_);
#else
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
if (CONFIGURED_UART_SELECTION != UART_SELECTION_DEFAULT) {
ESP_LOGW(TAG, " The chosen logger UART port is not available on this board."
"The default port was used instead.");
}
switch (this->uart_) {
#if LT_HW_UART0
case UART_SELECTION_UART0:
this->hw_serial_ = &Serial0;
libretiny::ensure_serial_handle(0);
Serial0.begin(this->baud_rate_);
break;
#endif
#if LT_HW_UART1
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial1;
libretiny::ensure_serial_handle(1);
Serial1.begin(this->baud_rate_);
break;
#endif
#if LT_HW_UART2
case UART_SELECTION_UART2:
this->hw_serial_ = &Serial2;
libretiny::ensure_serial_handle(2);
Serial2.begin(this->baud_rate_);
break;
#endif
default:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
if (this->uart_ != UART_SELECTION_DEFAULT) {
ESP_LOGW(TAG, " The chosen logger UART port is not available on this board."
"The default port was used instead.");
}
break;
}
// change lt_log() port to match default Serial
lt_log_set_port(this->get_uart() - 1);
if (this->uart_ == UART_SELECTION_DEFAULT) {
this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1);
lt_log_set_port(LT_UART_DEFAULT_SERIAL);
} else {
lt_log_set_port(this->uart_ - 1);
}
}
global_logger = this;
@@ -43,6 +60,20 @@ void Logger::pre_setup() {
#endif
}
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_DEFAULT:
return LOG_STR("DEFAULT");
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
case UART_SELECTION_UART2:
default:
return LOG_STR("UART2");
}
}
#ifdef USE_LIBRETINY_CRASH_HANDLER
bool crash_log_pending() { return libretiny::crash_handler_has_data(); }
void log_crash_to_client() { libretiny::crash_handler_log(); }
@@ -0,0 +1,13 @@
#pragma once
#ifdef USE_LIBRETINY
#include "esphome/core/helpers.h"
namespace esphome::logger {
// Single write with newline already in buffer (added by caller)
inline void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); }
} // namespace esphome::logger
#endif
+29 -10
View File
@@ -26,16 +26,20 @@ void Logger::setup() {
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC)
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
#elif defined(USE_LOGGER_UART_SELECTION_UART1)
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
#else
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
#endif
switch (this->uart_) {
case UART_SELECTION_UART0:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
break;
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
break;
case UART_SELECTION_USB_CDC:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
break;
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
@@ -45,6 +49,21 @@ void Logger::pre_setup() {
#endif
}
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
return LOG_STR("USB_CDC");
#endif
default:
return LOG_STR("UNKNOWN");
}
}
#ifdef USE_CRASH_HANDLER
bool crash_log_pending() { return rp2::crash_handler_has_data(); }
void log_crash_to_client() { rp2::crash_handler_log(); }
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#ifdef USE_RP2
#include "esphome/core/helpers.h"
namespace esphome::logger {
// Single write with newline already in buffer (added by caller)
inline void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); }
} // namespace esphome::logger
#endif
+42 -17
View File
@@ -42,9 +42,9 @@ __attribute__((section(".noinit"))) struct {
ESPHOME_LOG_TAG(TAG, "logger");
#ifdef USE_LOGGER_ZEPHYR_CDC_LOOP
#ifdef USE_LOGGER_UART_SELECTION_USB_CDC
void Logger::cdc_loop_() {
if (this->uart_dev_ == nullptr) {
if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) {
return;
}
static bool opened = false;
@@ -74,20 +74,26 @@ void Logger::cdc_loop_() {
void Logger::pre_setup() {
#ifdef CONFIG_SERIAL
if (this->baud_rate_ > 0) {
const struct device *uart_dev = nullptr;
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC)
static const struct device *uart_dev = nullptr;
switch (this->uart_) {
case UART_SELECTION_UART0: // NOLINT(bugprone-branch-clone)
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ESPHOME_LOGGER_UART0_NODE));
break;
case UART_SELECTION_UART1:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
break;
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
#if defined(CONFIG_USB_DEVICE_STACK) || defined(CONFIG_USB_DEVICE_STACK_NEXT)
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
if (device_is_ready(uart_dev)) {
// Nothing can log this early, and the logger has no output without USB anyway
zephyr::usb_device_enable();
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
if (device_is_ready(uart_dev)) {
// Nothing can log this early, and the logger has no output without USB anyway
zephyr::usb_device_enable();
}
#endif
break;
#endif
}
#endif
#elif defined(USE_LOGGER_UART_SELECTION_UART1)
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
#else
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ESPHOME_LOGGER_UART0_NODE));
#endif
if (device_is_ready(uart_dev)) {
this->uart_dev_ = uart_dev;
#if defined(USE_LOGGER_WAIT_FOR_CDC) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)
@@ -108,10 +114,14 @@ void Logger::pre_setup() {
char reason_buffer[zephyr::RESET_REASON_BUFFER_SIZE];
const char *reset_reason = zephyr::get_reset_reason(std::span<char, zephyr::RESET_REASON_BUFFER_SIZE>(reason_buffer));
ESP_LOGI(TAG, "Reset reason: %s", reset_reason);
this->dump_crash_();
dump_crash_();
#endif
#ifdef USE_CRASH_HANDLER
#ifdef USE_LOGGER_UART_SELECTION_USB_CDC
// Nobody reads a USB port this early; cdc_loop_() reports the dump once the port is opened
#if defined(USE_CRASH_HANDLER) && !defined(USE_LOGGER_ZEPHYR_CDC_LOOP)
if (this->uart_ == UART_SELECTION_USB_CDC)
return;
#endif
// Without a UART (baud_rate: 0, or one that did not come up) nothing reaches the console
if (this->uart_dev_ != nullptr)
log_crash_to_client();
@@ -133,7 +143,22 @@ void HOT Logger::write_msg_(const char *msg, uint16_t len) {
}
}
static constexpr uint8_t REASON_BUF_SIZE = 32;
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
return LOG_STR("USB_CDC");
#endif
default:
return LOG_STR("UNKNOWN");
}
}
static const uint8_t REASON_BUF_SIZE = 32;
static const char *reason_to_str(unsigned int reason, char *buf) {
switch (reason) {
@@ -44,15 +44,19 @@ void TaskLogBuffer::release_message_main_loop() {
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) {
// Measure on a copy; the caller's va_list formats the text later
// First, calculate the exact length needed using a null buffer (no actual writing)
va_list args_copy;
va_copy(args_copy, args);
int ret = task_log_cap_length(vsnprintf(nullptr, 0, format, args_copy));
int ret = vsnprintf(nullptr, 0, format, args_copy);
va_end(args_copy);
if (ret <= 0) {
return false; // Formatting error or empty message
}
uint16_t text_length = ret;
// 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;
// Calculate total size needed (header + text length + null terminator)
size_t total_size = sizeof(LogMessage) + text_length + 1;
@@ -65,10 +69,24 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
return false; // Failed to acquire memory
}
// Set up the message header in the acquired memory
LogMessage *msg = static_cast<LogMessage *>(acquired_memory);
fill_task_log_header(msg, level, tag, line, thread_name);
msg->level = level;
msg->tag = tag;
msg->line = line;
// Store the thread name now instead of waiting until main loop processing
// This avoids crashes if the task completes or is deleted between when this message
// is enqueued and when it's processed by the main loop
if (thread_name != nullptr) {
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
} else {
msg->thread_name[0] = '\0'; // Empty string if no thread name
}
// 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);
@@ -78,7 +96,12 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
return false;
}
msg->text_length = trim_trailing_newlines(text_area, text_length);
// Remove trailing newlines
while (text_length > 0 && text_area[text_length - 1] == '\n') {
text_length--;
}
msg->text_length = text_length;
// Complete the send operation with the acquired memory
result = xRingbufferSendComplete(this->handle_(), acquired_memory);
@@ -12,8 +12,6 @@
#include <freertos/FreeRTOS.h>
#include <freertos/ringbuf.h>
#include "task_log_message.h"
namespace esphome::logger {
/**
@@ -34,7 +32,19 @@ namespace esphome::logger {
*/
class TaskLogBuffer {
public:
using LogMessage = TaskLogMessage;
// Structure for a log message header (text data follows immediately after)
struct LogMessage {
const char *tag; // We store the pointer, assuming tags are static
char thread_name[16]; // Store thread name directly (only used for non-main threads)
uint16_t text_length; // Length of the message text (up to ~64KB)
uint16_t line; // Source code line number
uint8_t level; // Log level (0-7)
// Methods for accessing message contents
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); }
inline const char *text_data() const { return reinterpret_cast<const char *>(this) + sizeof(LogMessage); }
};
TaskLogBuffer();
// No destructor: Logger is never destroyed
@@ -71,8 +71,18 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
LogMessage &msg = this->slots_[slot_index];
// Copied now; the thread may exit before processing
fill_task_log_header(&msg, level, tag, line, thread_name);
// Fill in the message header
msg.level = level;
msg.tag = tag;
msg.line = line;
// Store the thread name now to avoid crashes if thread exits before processing
if (thread_name != nullptr) {
strncpy(msg.thread_name, thread_name, sizeof(msg.thread_name) - 1);
msg.thread_name[sizeof(msg.thread_name) - 1] = '\0';
} else {
msg.thread_name[0] = '\0';
}
// Format the message text
int ret = vsnprintf(msg.text, sizeof(msg.text), format, args);
@@ -84,7 +94,10 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
msg.text_length = static_cast<uint16_t>(std::min(static_cast<size_t>(ret), sizeof(msg.text) - 1));
}
msg.text_length = trim_trailing_newlines(msg.text, msg.text_length);
// Remove trailing newlines
while (msg.text_length > 0 && msg.text[msg.text_length - 1] == '\n') {
msg.text_length--;
}
msg.text[msg.text_length] = '\0';
// Commit the slot
@@ -13,8 +13,6 @@
#include <cstring>
#include <pthread.h>
#include "task_log_message.h"
namespace esphome::logger {
/**
@@ -13,6 +13,13 @@ TaskLogBuffer::TaskLogBuffer() {
this->mutex_ = xSemaphoreCreateMutex();
}
TaskLogBuffer::~TaskLogBuffer() {
if (this->mutex_ != nullptr) {
vSemaphoreDelete(this->mutex_);
this->mutex_ = nullptr;
}
}
size_t TaskLogBuffer::available_contiguous_space_() const {
if (this->head_ >= this->tail_) {
// head is ahead of or equal to tail
@@ -78,15 +85,19 @@ void TaskLogBuffer::release_message_main_loop() {
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) {
// Measure on a copy; the caller's va_list formats the text later
// First, calculate the exact length needed using a null buffer (no actual writing)
va_list args_copy;
va_copy(args_copy, args);
int ret = task_log_cap_length(vsnprintf(nullptr, 0, format, args_copy));
int ret = vsnprintf(nullptr, 0, format, args_copy);
va_end(args_copy);
if (ret <= 0) {
return false; // Formatting error or empty message
}
uint16_t text_length = ret;
// 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;
// Calculate total size needed (header + text length + null terminator)
size_t total_size = message_total_size(text_length);
@@ -117,7 +128,17 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
// Write message header
LogMessage *msg = reinterpret_cast<LogMessage *>(this->storage_ + write_pos);
fill_task_log_header(msg, level, tag, line, thread_name);
msg->level = level;
msg->tag = tag;
msg->line = line;
// Store the thread name now to avoid crashes if task is deleted before processing
if (thread_name != nullptr) {
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0';
} else {
msg->thread_name[0] = '\0';
}
// Format the message text directly into the buffer
char *text_area = msg->text_data();
@@ -129,7 +150,9 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
}
// Trim before storing so the next message reuses the freed bytes
text_length = trim_trailing_newlines(text_area, text_length);
while (text_length > 0 && text_area[text_length - 1] == '\n') {
text_length--;
}
text_area[text_length] = '\0';
msg->text_length = text_length;
@@ -13,8 +13,6 @@
#include <semphr.h>
#include <task.h>
#include "task_log_message.h"
namespace esphome::logger {
/**
@@ -44,14 +42,25 @@ namespace esphome::logger {
*/
class TaskLogBuffer {
public:
using LogMessage = TaskLogMessage;
// Structure for a log message header (text data follows immediately after)
struct LogMessage {
const char *tag; // We store the pointer, assuming tags are static
char thread_name[16]; // Store thread name directly (only used for non-main threads)
uint16_t text_length; // Length of the message text (up to ~64KB)
uint16_t line; // Source code line number
uint8_t level; // Log level (0-7)
// Methods for accessing message contents
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); }
inline const char *text_data() const { return reinterpret_cast<const char *>(this) + sizeof(LogMessage); }
};
// Padding marker level to indicate wrap-around point (stored in LogMessage.level field)
// Valid log levels are 0-7, so 0xFF cannot be a real message
static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF;
TaskLogBuffer();
// No destructor: Logger is never destroyed
~TaskLogBuffer();
// NOT thread-safe - borrow a message from the buffer, only call from main loop
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
@@ -4,7 +4,7 @@
namespace esphome::logger {
__thread bool non_main_task_recursion_guard; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
__thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
@@ -29,21 +29,29 @@ TaskLogBuffer::TaskLogBuffer() {
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) {
// Measure on a copy; the caller's va_list formats the text later
// First, calculate the exact length needed using a null buffer (no actual writing)
va_list args_copy;
va_copy(args_copy, args);
int ret = task_log_cap_length(vsnprintf(nullptr, 0, format, args_copy));
int ret = vsnprintf(nullptr, 0, format, args_copy);
va_end(args_copy);
if (ret <= 0) {
return false; // Formatting error or empty message
}
uint16_t text_length = ret;
// 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;
}
fill_task_log_header(msg, level, tag, line, thread_name);
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
@@ -82,7 +90,11 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &tex
// we claimed buffer already, const_cast is safe here
message = const_cast<LogMessage *>(reinterpret_cast<const LogMessage *>(this->current_token_));
text_length = trim_trailing_newlines(message->text_data(), message->text_length);
text_length = message->text_length;
// Remove trailing newlines
while (text_length > 0 && message->text_data()[text_length - 1] == '\n') {
text_length--;
}
return true;
}
@@ -6,14 +6,12 @@
#include "esphome/core/helpers.h"
#include <zephyr/sys/mpsc_pbuf.h>
#include "task_log_message.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)
extern __thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
@@ -36,6 +34,7 @@ class TaskLogBuffer {
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); }
};
TaskLogBuffer();
~TaskLogBuffer() = default;
// 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_); }
@@ -1,54 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
#include <cstdarg>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
namespace esphome::logger {
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Header of a variable-size task log message; the text follows it
struct TaskLogMessage {
const char *tag; // We store the pointer, assuming tags are static
char thread_name[16]; // Copied, the task may be gone before the main loop reads it
uint16_t text_length;
uint16_t line;
uint8_t level;
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(TaskLogMessage); }
};
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
static constexpr int TASK_LOG_MAX_TEXT_SIZE = 255;
inline int task_log_cap_length(int ret) { return ret > TASK_LOG_MAX_TEXT_SIZE ? TASK_LOG_MAX_TEXT_SIZE : ret; }
#endif
template<typename Message>
inline void fill_task_log_header(Message *msg, uint8_t level, const char *tag, uint16_t line, const char *thread_name) {
msg->level = level;
msg->tag = tag;
msg->line = line;
if (thread_name != nullptr) {
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0';
} else {
msg->thread_name[0] = '\0';
}
}
inline uint16_t trim_trailing_newlines(const char *text, uint16_t length) {
while (length > 0 && text[length - 1] == '\n')
length--;
return length;
}
} // namespace esphome::logger
#endif // USE_ESPHOME_TASK_LOG_BUFFER
-48
View File
@@ -595,52 +595,4 @@ template<typename... Ts> class WaitUntilAction : public Action<Ts...>, public Co
uint8_t stop_generation_{0};
};
template<typename... Ts> class UpdateComponentAction : public Action<Ts...> {
public:
UpdateComponentAction(PollingComponent *component) : component_(component) {}
void play(const Ts &...x) override {
if (!this->component_->is_ready())
return;
this->component_->update();
}
protected:
PollingComponent *component_;
};
template<typename... Ts> class SuspendComponentAction : public Action<Ts...> {
public:
SuspendComponentAction(PollingComponent *component) : component_(component) {}
void play(const Ts &...x) override {
if (!this->component_->is_ready())
return;
this->component_->stop_poller();
}
protected:
PollingComponent *component_;
};
template<typename... Ts> class ResumeComponentAction : public Action<Ts...> {
public:
ResumeComponentAction(PollingComponent *component) : component_(component) {}
TEMPLATABLE_VALUE(uint32_t, update_interval)
void play(const Ts &...x) override {
if (!this->component_->is_ready()) {
return;
}
optional<uint32_t> update_interval = this->update_interval_.optional_value(x...);
if (update_interval.has_value()) {
this->component_->set_update_interval(update_interval.value());
}
this->component_->start_poller();
}
protected:
PollingComponent *component_;
};
} // namespace esphome
+3 -25
View File
@@ -151,7 +151,6 @@
#define USE_LINE_READER
#define USE_LOCK
#define USE_LOGGER
#define USE_LOGGER_CONSOLE
#define USE_LOGGER_LEVEL_LISTENERS
#define USE_LOGGER_RUNTIME_TAG_LEVELS
#define USE_LVGL
@@ -759,26 +758,16 @@
#if defined(USE_ESP32_VARIANT_ESP32S2)
#define USE_LOGGER_USB_CDC
#define USE_LOGGER_UART_SELECTION_USB_CDC
#define ESPHOME_LOGGER_UART_NAME "USB_CDC"
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_USB_CDC
#elif defined(USE_ESP32_VARIANT_ESP32H21)
#define USE_LOGGER_USB_SERIAL_JTAG
#define USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG
#define ESPHOME_LOGGER_UART_NAME "USB_SERIAL_JTAG"
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_USB_SERIAL_JTAG
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \
defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \
defined(USE_ESP32_VARIANT_ESP32H4) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S3) || \
defined(USE_ESP32_VARIANT_ESP32S31)
defined(USE_ESP32_VARIANT_ESP32H21) || defined(USE_ESP32_VARIANT_ESP32H4) || defined(USE_ESP32_VARIANT_ESP32P4) || \
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32S31)
#define USE_LOGGER_USB_CDC
#define USE_LOGGER_UART_SELECTION_USB_CDC
#define USE_LOGGER_USB_SERIAL_JTAG
#define USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG
#define ESPHOME_LOGGER_UART_NAME "USB_SERIAL_JTAG"
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_USB_SERIAL_JTAG
#else
#define USE_LOGGER_UART_SELECTION_UART0
#define ESPHOME_LOGGER_UART_NAME "UART0"
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_UART0
#endif
#endif
@@ -792,9 +781,6 @@
#define USE_WIFI_SCAN_RESULTS_LOCK
#define USE_ESP8266_LOGGER_SERIAL
#define USE_ESP8266_LOGGER_SERIAL1
#define USE_LOGGER_UART_SELECTION_UART0
#define ESPHOME_LOGGER_UART_NAME "UART0"
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_UART0
#define USE_ESP8266_PREFERENCES_FLASH
#define ESP8266_PREFERENCES_FLASH_WORDS 128
#define ESP8266_PREFERENCES_RTC_EBOOT_WORDS 32
@@ -840,8 +826,6 @@
#define USE_I2C
#define USE_LOGGER_USB_CDC
#define USE_LOGGER_UART_SELECTION_USB_CDC
#define ESPHOME_LOGGER_UART_NAME "USB_CDC"
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_USB_CDC
#define USE_SOCKET_IMPL_LWIP_TCP
#define USE_RP2040_BLE
#define USE_RP2_BLE_TRACKER
@@ -872,9 +856,6 @@
#if defined(USE_BK72XX) || defined(USE_RTL87XX) || defined(USE_LN882X)
#define USE_LIBRETINY_CRASH_HANDLER
#endif
#define USE_LOGGER_UART_SELECTION_DEFAULT
#define ESPHOME_LOGGER_UART_NAME "DEFAULT"
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_DEFAULT
#ifdef USE_BK72XX
#define USE_REMOTE_RECEIVER_TIMER_SAMPLING
#endif
@@ -929,11 +910,8 @@
#define ESPHOME_TASK_LOG_BUFFER_SIZE 768
#define USE_LOGGER_EARLY_MESSAGE
#define USE_LOGGER_UART_SELECTION_USB_CDC
#define ESPHOME_LOGGER_UART_NAME "USB_CDC"
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_USB_CDC
#define USE_LOGGER_USB_CDC
#define USE_LOGGER_WAIT_FOR_CDC
#define USE_LOGGER_ZEPHYR_CDC_LOOP
#define USE_NRF52_DFU
#define USE_NRF52_REG0_VOUT 5
#define USE_NRF52_UICR_ERASE
+40 -93
View File
@@ -68,6 +68,38 @@ def test_logger_pre_setup_before_other_components(generate_main):
)
def test_default_uart_selection_is_not_emitted(generate_main):
"""UART0 is the C++ initializer on ESP8266, so the setter is skipped."""
main_cpp = generate_main("tests/component_tests/logger/test_logger.yaml")
assert "set_uart_selection(" not in main_cpp
def test_custom_uart_selection_is_emitted(generate_main):
"""A non default UART still reaches the setter before pre_setup()."""
main_cpp = generate_main("tests/component_tests/logger/test_logger_uart1.yaml")
assert "set_uart_selection(logger::UART_SELECTION_UART1);" in main_cpp
def test_libretiny_default_uart_selection_is_not_emitted(generate_main):
"""DEFAULT is the C++ initializer on LibreTiny, so the setter is skipped."""
main_cpp = generate_main(
"tests/component_tests/logger/test_logger_libretiny_default.yaml"
)
assert "set_uart_selection(" not in main_cpp
def test_libretiny_uart0_is_emitted(generate_main):
"""UART0 is not the LibreTiny initializer, so it must still be set."""
main_cpp = generate_main(
"tests/component_tests/logger/test_logger_libretiny_uart0.yaml"
)
assert "set_uart_selection(logger::UART_SELECTION_UART0);" in main_cpp
def test_ram_log_strings_is_deprecated(
generate_main: Callable[[str | Path], str],
caplog: pytest.LogCaptureFixture,
@@ -136,108 +168,23 @@ def test_log_v2_only_on_native_toolchain(
@pytest.mark.parametrize(
("config", "port", "name", "zephyr_cdc_loop"),
("config", "usb_cdc"),
[
pytest.param("test_logger.yaml", "UART0", "UART0", False, id="esp8266_default"),
pytest.param(
"test_logger_uart1.yaml", "UART1", "UART1", False, id="esp8266_uart1"
),
pytest.param(
"test_logger_uart0_swap.yaml",
"UART0_SWAP",
"UART0_SWAP",
False,
id="esp8266_uart0_swap",
),
# ESP8266 without serial logging never opens a port
pytest.param(
"test_logger_no_serial.yaml", "UART0", "NONE", False, id="esp8266_no_serial"
),
pytest.param(
"test_logger_libretiny_default.yaml",
"DEFAULT",
"DEFAULT",
False,
id="libretiny_default",
),
pytest.param(
"test_logger_libretiny_uart0.yaml",
"UART0",
"UART0",
False,
id="libretiny_uart0",
),
pytest.param(
"test_logger_rp2_usb_cdc.yaml",
"USB_CDC",
"USB_CDC",
False,
id="rp2_usb_cdc",
),
pytest.param(
"test_logger_rp2_uart0.yaml", "UART0", "UART0", False, id="rp2_uart0"
),
pytest.param(
"test_logger_esp32_usb_cdc.yaml",
"USB_CDC",
"USB_CDC",
False,
id="esp32_usb_cdc",
),
pytest.param(
"test_logger_esp32_usb_serial_jtag.yaml",
"USB_SERIAL_JTAG",
"USB_SERIAL_JTAG",
False,
id="esp32_usb_serial_jtag",
),
pytest.param(
"test_logger_nrf52_usb_cdc.yaml",
"USB_CDC",
"USB_CDC",
True,
id="nrf52_usb_cdc",
),
# Zephyr polls the USB port only when it logs there
pytest.param(
"test_logger_nrf52_usb_cdc_no_serial.yaml",
"USB_CDC",
"USB_CDC",
False,
id="nrf52_no_serial",
),
pytest.param("test_logger_rp2_usb_cdc.yaml", True, id="usb_cdc"),
pytest.param("test_logger_rp2_uart0.yaml", False, id="uart0"),
],
)
def test_uart_selection_defines(
def test_rp2_usb_cdc_selection_define(
generate_main: Callable[[str | Path], str],
config: str,
port: str,
name: str,
zephyr_cdc_loop: bool,
usb_cdc: bool,
) -> None:
"""The chosen port is fixed at build time: one flag, its enum value and its dump_config name."""
"""The USB CDC crash replay only compiles in when the logger uses USB CDC."""
generate_main(f"tests/component_tests/logger/{config}")
defines = {d.name: d.value for d in CORE.defines}
prefix = "USE_LOGGER_UART_SELECTION_"
assert {d.removeprefix(prefix) for d in defines if d.startswith(prefix)} == {port}
assert (
str(defines["ESPHOME_LOGGER_UART_SELECTION"])
== f"logger::UART_SELECTION_{port}"
)
assert str(defines["ESPHOME_LOGGER_UART_NAME"]) == f'"{name}"'
assert ("USE_LOGGER_ZEPHYR_CDC_LOOP" in defines) is zephyr_cdc_loop
def test_log_listener_slots_count_every_listener(
generate_main: Callable[[str | Path], str],
) -> None:
"""Two on_message triggers and the API each take a slot."""
generate_main("tests/component_tests/logger/test_logger_listeners.yaml")
assert (
str({d.name: d.value for d in CORE.defines}["ESPHOME_LOG_MAX_LISTENERS"]) == "3"
)
"USE_LOGGER_UART_SELECTION_USB_CDC" in {d.name for d in CORE.defines}
) is usb_cdc
@pytest.mark.parametrize(
@@ -1,11 +0,0 @@
---
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
logger:
hardware_uart: USB_CDC
@@ -1,11 +0,0 @@
---
esphome:
name: test
esp32:
variant: esp32c3
framework:
type: esp-idf
logger:
hardware_uart: USB_SERIAL_JTAG
@@ -1,23 +0,0 @@
---
esphome:
name: test
esp32:
variant: esp32
framework:
type: esp-idf
wifi:
ssid: test
password: testtesttest
api:
logger:
on_message:
- level: ERROR
then:
- lambda: "(void) message;"
- level: WARN
then:
- lambda: "(void) message;"
@@ -1,9 +0,0 @@
---
esphome:
name: test
esp8266:
board: d1_mini_lite
logger:
baud_rate: 0
@@ -1,10 +0,0 @@
---
esphome:
name: test
nrf52:
board: adafruit_itsybitsy_nrf52840
bootloader: adafruit_nrf52_sd140_v6
logger:
hardware_uart: USB_CDC
@@ -1,11 +0,0 @@
---
esphome:
name: test
nrf52:
board: adafruit_itsybitsy_nrf52840
bootloader: adafruit_nrf52_sd140_v6
logger:
hardware_uart: USB_CDC
baud_rate: 0
@@ -1,9 +0,0 @@
---
esphome:
name: test
esp8266:
board: d1_mini_lite
logger:
hardware_uart: UART0_SWAP
@@ -1,54 +0,0 @@
#include <gtest/gtest.h>
#include <cstring>
#include "esphome/core/defines.h"
// The cap and message header are compiled only for platforms with a task log buffer
#define USE_LIBRETINY
#ifndef USE_ESPHOME_TASK_LOG_BUFFER
#define USE_ESPHOME_TASK_LOG_BUFFER
#endif
#include "esphome/components/logger/task_log_message.h"
#undef USE_LIBRETINY
namespace esphome::logger::testing {
TEST(TaskLogMessageTest, CapLengthStopsAt255) {
EXPECT_EQ(task_log_cap_length(0), 0);
EXPECT_EQ(task_log_cap_length(255), 255);
EXPECT_EQ(task_log_cap_length(256), 255);
EXPECT_EQ(task_log_cap_length(1000), 255);
}
TEST(TaskLogMessageTest, HeaderCopiesFieldsAndThreadName) {
TaskLogMessage msg{};
fill_task_log_header(&msg, 3, "tag", 42, "worker");
EXPECT_EQ(msg.level, 3);
EXPECT_STREQ(msg.tag, "tag");
EXPECT_EQ(msg.line, 42);
EXPECT_STREQ(msg.thread_name, "worker");
}
TEST(TaskLogMessageTest, HeaderNullThreadNameIsEmpty) {
TaskLogMessage msg{};
std::memset(msg.thread_name, 'x', sizeof(msg.thread_name));
fill_task_log_header(&msg, 1, "tag", 1, nullptr);
EXPECT_STREQ(msg.thread_name, "");
}
TEST(TaskLogMessageTest, HeaderTruncatesLongThreadName) {
TaskLogMessage msg{};
fill_task_log_header(&msg, 1, "tag", 1, "a_thread_name_longer_than_sixteen");
EXPECT_EQ(std::strlen(msg.thread_name), sizeof(msg.thread_name) - 1);
EXPECT_EQ(std::strncmp(msg.thread_name, "a_thread_name_l", sizeof(msg.thread_name) - 1), 0);
}
TEST(TaskLogMessageTest, TrimTrailingNewlines) {
EXPECT_EQ(trim_trailing_newlines("", 0), 0);
EXPECT_EQ(trim_trailing_newlines("abc", 3), 3);
EXPECT_EQ(trim_trailing_newlines("abc\n\n", 5), 3);
EXPECT_EQ(trim_trailing_newlines("\n\n\n", 3), 0);
EXPECT_EQ(trim_trailing_newlines("a\nb", 3), 3);
}
} // namespace esphome::logger::testing
+1
View File
@@ -17,6 +17,7 @@ void setup() {
App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1);
auto *log = new logger::Logger(115200); // NOLINT
log->pre_setup();
log->set_uart_selection(logger::UART_SELECTION_UART0);
App.register_component_(log);
auto *wifi = new wifi::WiFiComponent(); // NOLINT
+93 -17
View File
@@ -2441,16 +2441,25 @@ def polling_component() -> None:
@pytest.mark.asyncio
@pytest.mark.usefixtures("polling_component")
@pytest.mark.parametrize(
"name",
["component.update", "component.suspend", "component.resume"],
("name", "call"),
[
("component.update", "update()"),
("component.suspend", "stop_poller()"),
("component.resume", "start_poller()"),
],
)
async def test_component_action_codegen(name: str) -> None:
"""Each action is an object constructed with the component, with no interval set."""
async def test_component_action_codegen(name: str, call: str) -> None:
"""The call goes through ``PollingComponent`` and only once the component is ready."""
await build_action(_component_action(name), TEMPLATE_ARG, [])
class_name = str(ACTION_REGISTRY[name].type_id)
assert f"new(act) {class_name}<>(comp);" in CORE.cpp_main_section
assert "set_update_interval" not in CORE.cpp_main_section
assert "new(act) ApplyAction<esphome__act__fn>();" in CORE.cpp_main_section
assert (
" PollingComponent &esphome__component = *::comp;\n"
" if (esphome__component.is_ready()) {\n"
f" esphome__component.{call};\n"
" }\n"
) in CORE.cpp_global_section
assert "set_update_interval" not in CORE.cpp_global_section
@pytest.mark.asyncio
@@ -2466,14 +2475,17 @@ async def test_component_resume_constant_interval_codegen() -> None:
)
assert (
"act->set_update_interval([]() -> uint32_t {\n return 250;\n});"
) in CORE.cpp_main_section
" if (esphome__component.is_ready()) {\n"
" esphome__component.set_update_interval(250);\n"
" esphome__component.start_poller();\n"
" }\n"
) in CORE.cpp_global_section
@pytest.mark.asyncio
@pytest.mark.usefixtures("polling_component")
async def test_component_resume_interval_lambda_codegen() -> None:
"""A templated interval is a lambda over the trigger args."""
"""A templated interval is computed from the trigger args, only once ready."""
await build_action(
_component_action(
"component.resume", {CONF_UPDATE_INTERVAL: Lambda("return x * 1000;")}
@@ -2482,27 +2494,91 @@ async def test_component_resume_interval_lambda_codegen() -> None:
[(cg.uint32, "x")],
)
main = CORE.cpp_main_section
assert "new(act) ResumeComponentAction<uint32_t>(comp);" in main
assert (
"act->set_update_interval([](uint32_t x) -> uint32_t {\n"
"static void esphome__act__fn(const std::remove_cvref_t<uint32_t> & x) {\n"
" PollingComponent &esphome__component = *::comp;\n"
" if (esphome__component.is_ready()) {\n"
" esphome__component.set_update_interval([](uint32_t x) -> uint32_t {\n"
" return x * 1000;\n"
"});"
) in main
" }(x));\n"
" esphome__component.start_poller();\n"
" }\n"
) in CORE.cpp_global_section
@pytest.mark.asyncio
@pytest.mark.usefixtures("apply_actions")
async def test_component_update_not_ready_still_runs_merged_actions() -> None:
"""The ready check skips only the update, never the actions merged after it."""
cg.new_Pvariable(ID("pcomp", is_declaration=True, type=cg.PollingComponent))
update = _component_action("component.update")
update["component.update"][CONF_ID] = ID("pcomp", type=cg.PollingComponent)
result = await build_action_list(
[update, _action("my.a", "act_a")], TEMPLATE_ARG, []
)
assert [str(action) for action in result] == ["act"]
text = CORE.cpp_global_section
assert text.index("esphome__component.update();") < text.index("::comp->do_a();")
# The only return is the stop check between the two actions
assert text.count("return;") == 1
@pytest.mark.asyncio
@pytest.mark.usefixtures("apply_actions")
async def test_callback_automation_if_with_component_update_is_direct() -> None:
"""An ``if`` whose branches update components becomes plain C++ in the callback."""
cg.new_Pvariable(ID("pcomp", is_declaration=True, type=cg.PollingComponent))
def update(id_: str) -> ConfigType:
return {
"component.update": {CONF_ID: ID("pcomp", type=cg.PollingComponent)},
CONF_TYPE_ID: ID(
id_, is_declaration=True, type=ApplyAction, is_manual=False
),
}
conf = _automation_conf(
{
"if": {
CONF_CONDITION: _inline_condition(),
CONF_THEN: [update("upd_then")],
CONF_ELSE: [update("upd_else")],
},
CONF_TYPE_ID: ID(
"act_if", is_declaration=True, type=IfAction, is_manual=False
),
}
)
await build_callback_automation(CALLBACK_PARENT, "add_cb", [], conf)
assert "Action" not in CORE.cpp_main_section
assert "Condition" not in CORE.cpp_main_section
text = CORE.cpp_global_section
assert "if (esphome__condition) {" in text
assert text.count("esphome__component.update();") == 2
@pytest.mark.asyncio
@pytest.mark.usefixtures("polling_component")
@pytest.mark.parametrize("body", ["x *= 1000; return x;", "return x *= 1000;"])
async def test_component_resume_interval_lambda_takes_args_by_value(body: str) -> None:
"""The user's interval lambda gets the trigger args by value, so it may modify them."""
"""The user's interval lambda gets the trigger args by value, so it may modify them.
It is called rather than reduced, so even a single return modifies only its own copy.
"""
await build_action(
_component_action("component.resume", {CONF_UPDATE_INTERVAL: Lambda(body)}),
cg.TemplateArguments(cg.uint32),
[(cg.uint32, "x")],
)
assert f"[](uint32_t x) -> uint32_t {{\n {body}\n}}" in CORE.cpp_main_section
assert (
f"set_update_interval([](uint32_t x) -> uint32_t {{\n {body}\n }}(x));"
in CORE.cpp_global_section
)
COMPONENT_ID = ID("comp", type=cg.Component)