Compare commits

..
Author SHA1 Message Date
J. Nick Koston 3c2c850cb4 Merge remote-tracking branch 'origin/dev' into HEAD 2026-11-14 08:38:58 -06:00
J. Nick Koston 95e4837b99 Merge branch 'dev' into api-client-synced-trigger 2026-11-01 12:32:34 -10:00
J. Nick Koston 16f14317a7 [api] Check the initial states arrive before on_client_synced fires 2026-10-29 20:04:02 -10:00
J. Nick Koston 08387b0fb0 [api] Don't fire on_client_synced after answering a disconnect request 2026-10-29 19:37:09 -10:00
J. Nick Koston e18413c2b7 [api] Catch an early on_client_synced in the integration test
Log unconditionally at the start of the trigger, so firing before the
subscription is seen even when the api.connected condition is false.
2026-10-29 19:15:17 -10:00
J. Nick Koston 086bfba8b3 [api] Fire on_client_synced once the deferred batch has drained
A direct send from the automation could overtake initial states still
queued under backpressure, and the trigger fired even when the final
flush killed the connection. The pending state rides in active_iterator_,
so it costs no RAM and the loop keeps its single iterator check.
2026-10-29 19:14:18 -10:00
J. Nick Koston 92e8a64669 Merge branch 'automation-owning-args' into api-client-synced-trigger 2026-10-29 17:09:42 -10:00
J. Nick Koston 32bde52dba [core] Move owning_args tests next to the automation tests 2026-10-29 17:09:39 -10:00
J. Nick Koston 32240fd69f Merge branch 'automation-owning-args' into api-client-synced-trigger 2026-10-29 17:06:59 -10:00
J. Nick Koston 8aa327264a Merge remote-tracking branch 'origin/dev' into automation-owning-args 2026-10-29 17:06:57 -10:00
J. Nick Koston b9a5ccea65 Merge branch 'automation-owning-args' into api-client-synced-trigger 2026-10-29 16:38:25 -10:00
J. Nick Koston aee3476828 [core] Move owning_args from nextion into automation
Picks std::string over StringRef for automation arguments when the
automation contains a deferring action. Shared so other components can
pass StringRef to their triggers without a lifetime issue.
2026-10-29 16:37:14 -10:00
J. Nick Koston 602c751c2f [api] Add on_client_synced trigger and pass client info as StringRef
on_client_connected fires during the hello, before the client subscribes
to states, so state_subscription_only is never true inside it. The new
on_client_synced trigger fires once the initial states have been sent,
when the subscriptions sent with SubscribeStates are active.

All three client triggers now pass StringRef; automations that defer get
std::string copies, chosen at compile time by automation.owning_args().
2026-10-29 16:30:08 -10:00
40 changed files with 736 additions and 609 deletions

No files matched your search

+32 -15
View File
@@ -144,6 +144,7 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_SEND_QUEUE = "max_send_queue"
CONF_ON_CLIENT_SYNCED = "on_client_synced"
CONF_OUTGOING_CONNECTION = "outgoing_connection"
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
@@ -453,6 +454,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_CLIENT_SYNCED): automation.validate_automation(
single=True
),
# Connection limits to prevent memory exhaustion on resource-constrained devices
# Each connection uses ~500-1000 bytes of RAM plus system resources
# Platform defaults based on available RAM and network stack implementation:
@@ -706,21 +710,34 @@ async def to_code(config: ConfigType) -> None:
if (wizard_config := config.get(wizard.CONF_WIZARD)) is not None:
await wizard.to_code(wizard_config)
if CONF_ON_CLIENT_CONNECTED in config:
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
await automation.build_automation(
var.get_client_connected_trigger(),
[(cg.std_string, "client_info"), (cg.std_string, "client_address")],
config[CONF_ON_CLIENT_CONNECTED],
)
if CONF_ON_CLIENT_DISCONNECTED in config:
cg.add_define("USE_API_CLIENT_DISCONNECTED_TRIGGER")
await automation.build_automation(
var.get_client_disconnected_trigger(),
[(cg.std_string, "client_info"), (cg.std_string, "client_address")],
config[CONF_ON_CLIENT_DISCONNECTED],
)
for conf_key, define, callback_method in (
(
CONF_ON_CLIENT_CONNECTED,
"USE_API_CLIENT_CONNECTED_TRIGGER",
"set_on_client_connected_callback",
),
(
CONF_ON_CLIENT_DISCONNECTED,
"USE_API_CLIENT_DISCONNECTED_TRIGGER",
"set_on_client_disconnected_callback",
),
(
CONF_ON_CLIENT_SYNCED,
"USE_API_CLIENT_SYNCED_TRIGGER",
"set_on_client_synced_callback",
),
):
if (automation_conf := config.get(conf_key)) is not None:
cg.add_define(define)
await automation.build_callback_automation(
var,
callback_method,
automation.owning_args(
[(cg.StringRef, "client_info"), (cg.StringRef, "client_address")],
automation_conf,
),
automation_conf,
)
if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
if key := encryption_config.get(CONF_KEY):
+20 -2
View File
@@ -251,6 +251,9 @@ void APIConnection::destroy_active_iterator_() {
this->iterator_storage_.initial_state.~InitialStateIterator();
break;
case ActiveIterator::NONE:
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
case ActiveIterator::CLIENT_SYNCED_PENDING:
#endif
break;
}
this->active_iterator_ = ActiveIterator::NONE;
@@ -415,10 +418,25 @@ void APIConnection::process_active_iterator_() {
} else {
this->process_iterator_batch_(this->iterator_storage_.list_entities);
}
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
} else if (this->active_iterator_ == ActiveIterator::CLIENT_SYNCED_PENDING) {
// Fired after the sync, not on SubscribeStates, so the subscriptions sent with it are active.
// Waits for the batch to drain so a direct send from the automation cannot overtake queued states.
// next_close: a DisconnectRequest this pass already answered; remove is only set next loop.
if (this->deferred_batch_.empty() && !this->flags_.remove && !this->flags_.next_close) {
this->active_iterator_ = ActiveIterator::NONE;
char peername[socket::SOCKADDR_STR_LEN];
this->parent_->get_client_synced_callback().call(StringRef(this->helper_->get_client_name()),
StringRef(this->helper_->get_peername_to(peername)));
}
#endif
} else { // INITIAL_STATE
if (this->iterator_storage_.initial_state.completed()) {
this->destroy_active_iterator_();
this->finalize_iterator_sync_();
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
this->active_iterator_ = ActiveIterator::CLIENT_SYNCED_PENDING;
#endif
} else {
this->process_iterator_batch_(this->iterator_storage_.initial_state);
}
@@ -1936,8 +1954,8 @@ void APIConnection::complete_authentication_() {
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
{
char peername[socket::SOCKADDR_STR_LEN];
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_peername_to(peername)));
this->parent_->get_client_connected_callback().call(StringRef(this->helper_->get_client_name()),
StringRef(this->helper_->get_peername_to(peername)));
}
#endif
#ifdef USE_HOMEASSISTANT_TIME
+14 -2
View File
@@ -296,7 +296,11 @@ class APIConnection final : public APIServerConnectionBase {
this->flags_.state_subscription = true;
// Start initial state iterator only if no iterator is active
// If list_entities is running, we'll start initial_state when it completes
if (this->active_iterator_ == ActiveIterator::NONE) {
if (this->active_iterator_ == ActiveIterator::NONE
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
|| this->active_iterator_ == ActiveIterator::CLIENT_SYNCED_PENDING
#endif
) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
}
@@ -642,7 +646,15 @@ class APIConnection final : public APIServerConnectionBase {
// Group 2: Iterator union (saves ~16 bytes vs separate iterators)
// These iterators are never active simultaneously - list_entities runs to completion
// before initial_state begins, so we use a union with explicit construction/destruction.
enum class ActiveIterator : uint8_t { NONE, LIST_ENTITIES, INITIAL_STATE };
enum class ActiveIterator : uint8_t {
NONE,
LIST_ENTITIES,
INITIAL_STATE,
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
// Initial states sent, on_client_synced not fired yet; uses no iterator storage
CLIENT_SYNCED_PENDING,
#endif
};
union IteratorUnion {
ListEntitiesIterator list_entities;
+6 -4
View File
@@ -257,9 +257,11 @@ void APIServer::remove_client_(uint8_t client_index) {
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before closing socket and removal for the trigger
char peername_buf[socket::SOCKADDR_STR_LEN];
std::string client_name(client->get_name());
std::string client_peername(client->get_peername_to(peername_buf));
char client_name[CLIENT_INFO_NAME_MAX_LEN];
strncpy(client_name, client->get_name(), sizeof(client_name) - 1);
client_name[sizeof(client_name) - 1] = '\0';
char peername[socket::SOCKADDR_STR_LEN];
client->get_peername_to(peername);
#endif
// Read before the swap-and-reset below destroys the connection
@@ -302,7 +304,7 @@ void APIServer::remove_client_(uint8_t client_index) {
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_.trigger(client_name, client_peername);
this->client_disconnected_callback_.call(StringRef(client_name), StringRef(peername));
#endif
}
+23 -4
View File
@@ -277,11 +277,27 @@ class APIServer final : public Component
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
#endif
#if defined(USE_API_CLIENT_CONNECTED_TRIGGER) || defined(USE_API_CLIENT_DISCONNECTED_TRIGGER) || \
defined(USE_API_CLIENT_SYNCED_TRIGGER)
/// Called with the client name and address, which are only valid during the call
using ClientCallback = Callback<void(StringRef, StringRef)>;
#endif
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
Trigger<std::string, std::string> *get_client_connected_trigger() { return &this->client_connected_trigger_; }
template<typename F> void set_on_client_connected_callback(F &&callback) {
this->client_connected_callback_ = ClientCallback::create(std::forward<F>(callback));
}
const ClientCallback &get_client_connected_callback() const { return this->client_connected_callback_; }
#endif
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
Trigger<std::string, std::string> *get_client_disconnected_trigger() { return &this->client_disconnected_trigger_; }
template<typename F> void set_on_client_disconnected_callback(F &&callback) {
this->client_disconnected_callback_ = ClientCallback::create(std::forward<F>(callback));
}
#endif
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
template<typename F> void set_on_client_synced_callback(F &&callback) {
this->client_synced_callback_ = ClientCallback::create(std::forward<F>(callback));
}
const ClientCallback &get_client_synced_callback() const { return this->client_synced_callback_; }
#endif
protected:
@@ -343,10 +359,13 @@ class APIServer final : public Component
// Pointers and pointer-like types first (4 bytes each)
socket::ListenSocket *socket_{nullptr};
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
Trigger<std::string, std::string> client_connected_trigger_;
ClientCallback client_connected_callback_;
#endif
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
Trigger<std::string, std::string> client_disconnected_trigger_;
ClientCallback client_disconnected_callback_;
#endif
#ifdef USE_API_CLIENT_SYNCED_TRIGGER
ClientCallback client_synced_callback_;
#endif
// 4-byte aligned types
+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
+4 -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
@@ -402,6 +401,7 @@
#define USE_API
#define USE_API_CLIENT_CONNECTED_TRIGGER
#define USE_API_CLIENT_DISCONNECTED_TRIGGER
#define USE_API_CLIENT_SYNCED_TRIGGER
#define USE_API_HOMEASSISTANT_ACTION_RESPONSES
#define USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#define USE_API_HOMEASSISTANT_SERVICES
@@ -759,26 +759,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 +782,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 +827,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 +857,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 +911,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
+10
View File
@@ -98,6 +98,16 @@ esphome:
api:
port: 8000
reboot_timeout: 0min
on_client_connected:
# Deferring actions get owned copies of the arguments
- delay: 1s
- logger.log:
format: "Client %s connected from %s"
args: [client_info.c_str(), client_address.c_str()]
on_client_synced:
- logger.log:
format: "Client %s synced from %s"
args: [client_info.c_str(), client_address.c_str()]
actions:
- action: hello_world
description: Log a greeting
@@ -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
@@ -0,0 +1,28 @@
esphome:
name: api-client-synced
host:
api:
on_client_synced:
- logger.log: "Synced trigger entered"
- if:
condition:
api.connected:
state_subscription_only: true
then:
- logger.log: "Synced trigger fired"
# Home Assistant subscribes to actions in the same packet as states
- homeassistant.event:
event: esphome.client_synced
switch:
# A known state at boot, so the test can check it is sent before the trigger
- platform: template
id: ready_switch
name: "Ready switch"
optimistic: true
restore_mode: ALWAYS_ON
logger:
level: DEBUG
@@ -0,0 +1,56 @@
"""Integration test for the api on_client_synced trigger."""
from __future__ import annotations
import asyncio
from aioesphomeapi import EntityState, HomeassistantServiceCall, SwitchState
import pytest
from .log_utils import LineWaiter
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_api_client_synced(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""The trigger fires after the initial states, with the action subscription active."""
waiter = LineWaiter()
calls: asyncio.Queue[HomeassistantServiceCall] = asyncio.Queue()
# Everything the client receives, in order
received: list[EntityState | HomeassistantServiceCall] = []
def on_service_call(call: HomeassistantServiceCall) -> None:
received.append(call)
calls.put_nowait(call)
async with (
run_compiled(yaml_config, line_callback=waiter.callback),
api_client_connected() as client,
):
await client.device_info()
assert waiter.count("Synced trigger entered") == 0
client.subscribe_home_assistant_states_and_services(
on_state=received.append,
on_service_call=on_service_call,
on_state_sub=lambda *_: None,
)
await waiter.wait_for_each(
"Synced trigger entered", "Synced trigger fired", timeout=5.0
)
call = await asyncio.wait_for(calls.get(), timeout=5.0)
assert call.service == "esphome.client_synced"
assert call.is_event
# The initial switch state arrives before the trigger's event
synced_at = received.index(call)
switch_states = [
i
for i, item in enumerate(received[:synced_at])
if isinstance(item, SwitchState)
]
assert switch_states, received
assert received[switch_states[0]].state is True