Compare commits

..
Author SHA1 Message Date
J. Nick Koston 0bb86f8094 Merge remote-tracking branch 'origin/dev' into HEAD 2026-11-14 08:37:40 -06:00
J. Nick Koston bf9e2b7a54 Merge branch 'dev' into binary-sensor-click-callback 2026-11-01 13:03:51 -10:00
J. Nick Koston fd000ead9f Merge dev 2026-11-01 11:52:49 -10:00
J. Nick Koston 42956e0173 [binary_sensor] Read the click configs once and share the test callback
Pair each click key with its detector class in one table, add the define
and create the detectors in one pass, register every test callback
through one helper, and point the skill at the in-tree example instead
of copying it.
2026-10-31 08:33:28 -10:00
J. Nick Koston 7edd1a8ea7 Merge branch 'binary-sensor-click-callback-tests' into binary-sensor-click-callback
# Conflicts:
#	tests/components/binary_sensor/click_trigger_test.cpp
2026-10-31 08:15:50 -10:00
J. Nick Koston 35e03009e6 [binary_sensor] Test that a double click resets its times
A short second press after a double click would otherwise pair with the
first release and the next gap.
2026-10-31 08:14:30 -10:00
J. Nick Koston df69028380 [binary_sensor] Test a press published by double click actions
Pin that the double click times reset before the actions run, so a
press an action publishes on the same sensor is kept.
2026-10-31 08:13:31 -10:00
J. Nick Koston 46c0cd859f Merge branch 'binary-sensor-click-callback-tests' into binary-sensor-click-callback
# Conflicts:
#	tests/components/binary_sensor/click_trigger_test.cpp
2026-10-31 07:54:21 -10:00
J. Nick Koston 419660bb67 [binary_sensor] Fix clang-tidy in the click test and hold the press after a double click 2026-10-31 07:54:11 -10:00
J. Nick Koston 22ba443107 [binary_sensor] Fix clang-tidy in the click test and hold the press after a double click 2026-10-31 07:54:08 -10:00
pre-commit-ci-lite[bot] 4015cea895 apply automatic formatting fixes 2026-10-31 17:51:49 +00:00
J. Nick Koston d4a8b149a9 [binary_sensor] Name the click detectors and build them as callback entries
Rename the state objects to ClickDetector and DoubleClickDetector with
check(), write the detector id as ::id through ApplyCall so a trigger id
named like the callback parameter cannot hide it, build both automations
from _CALLBACK_AUTOMATIONS, share one schema helper, and document the
stateful filter pattern in the automations skill.
2026-10-31 07:46:59 -10:00
J. Nick Koston 97732c9a25 [binary_sensor] Build on_click and on_double_click as callback automations
The click timing moves into small ClickState and DoubleClickState objects
that a generated state callback asks, so an automation whose actions are
all inline runs as one function with no Trigger or Automation object.
ClickTrigger and DoubleClickTrigger are removed; only codegen used them.
2026-10-31 07:40:40 -10:00
J. Nick Koston e3bb14be43 [binary_sensor] Add tests for the click and double click triggers
Pin the click and double click timing with a C++ unit test, and the
generated trigger code with a codegen test.
2026-10-31 07:38:50 -10:00
39 changed files with 689 additions and 693 deletions

No files matched your search

+9 -2
View File
@@ -123,10 +123,17 @@ Both live in `esphome/core/helpers.h` and accept `std::function`, lambdas and fo
their templatized `add()` method. `LazyCallbackManager` is a drop-in replacement for `CallbackManager` that defers allocation until the first
callback is added. Prefer it for entity-level callbacks where most instances have no subscribers.
A filter that keeps state between calls (timing, edges) still needs no Trigger class: give each automation a
small object with constant initializers, created from its `CONF_TRIGGER_ID` before the automations are built,
and let `when` ask it, e.g. `ApplyCall("{}->check(state, {}, {})", ((CONF_TRIGGER_ID, ...), ...))`. An `ID`
value in an `ApplyCall` is written as `::id`, so a callback parameter cannot hide it. `on_click` in
`esphome/components/binary_sensor/__init__.py` is the in-tree example; once a second component needs this,
move the object creation into `CallbackAutomation`.
## Triggers: Trigger class method
Use `build_automation()` with a `Trigger<Ts...>` subclass only when the forwarder needs **mutable state
beyond a single `Automation*` pointer** (e.g. edge detection tracking previous state, timing logic).
Use `build_automation()` with a `Trigger<Ts...>` subclass only when the state must be set up from the
parent (in the constructor or in `setup()`), or the trigger needs its own timers.
**Python:**
```python
+49 -47
View File
@@ -134,10 +134,8 @@ BinarySensorInitiallyOff = binary_sensor_ns.class_(
BinarySensorPtr = BinarySensor.operator("ptr")
# Triggers
ClickTrigger = binary_sensor_ns.class_("ClickTrigger", automation.Trigger.template())
DoubleClickTrigger = binary_sensor_ns.class_(
"DoubleClickTrigger", automation.Trigger.template()
)
ClickDetector = binary_sensor_ns.class_("ClickDetector")
DoubleClickDetector = binary_sensor_ns.class_("DoubleClickDetector")
MultiClickTrigger = binary_sensor_ns.class_(
"MultiClickTrigger", automation.Trigger.template(), cg.Component
)
@@ -450,6 +448,23 @@ def validate_click_timing(value):
return value
def _click_automation_schema(detector: MockObjClass) -> cv.All:
return cv.All(
automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(detector),
cv.Optional(
CONF_MIN_LENGTH, default="50ms"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_MAX_LENGTH, default="350ms"
): cv.positive_time_period_milliseconds,
}
),
validate_click_timing,
)
def validate_publish_initial_state(value):
value = cv.boolean(value)
_LOGGER.warning(
@@ -482,35 +497,9 @@ _BINARY_SENSOR_SCHEMA = (
): validate_filters,
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}),
cv.Optional(CONF_ON_CLICK): cv.All(
automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger),
cv.Optional(
CONF_MIN_LENGTH, default="50ms"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_MAX_LENGTH, default="350ms"
): cv.positive_time_period_milliseconds,
}
),
validate_click_timing,
),
cv.Optional(CONF_ON_DOUBLE_CLICK): cv.All(
automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DoubleClickTrigger
),
cv.Optional(
CONF_MIN_LENGTH, default="50ms"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_MAX_LENGTH, default="350ms"
): cv.positive_time_period_milliseconds,
}
),
validate_click_timing,
cv.Optional(CONF_ON_CLICK): _click_automation_schema(ClickDetector),
cv.Optional(CONF_ON_DOUBLE_CLICK): _click_automation_schema(
DoubleClickDetector
),
cv.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation(
{
@@ -564,6 +553,11 @@ def binary_sensor_schema(
return _BINARY_SENSOR_SCHEMA.extend(schema)
_CLICK_DETECTORS = (
(CONF_ON_CLICK, ClickDetector),
(CONF_ON_DOUBLE_CLICK, DoubleClickDetector),
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_PRESS,
@@ -586,32 +580,40 @@ _CALLBACK_AUTOMATIONS = (
"add_full_state_callback",
[(cg.optional.template(bool), "x_previous"), (cg.optional.template(bool), "x")],
),
*(
automation.CallbackAutomation(
conf_key,
"add_on_state_callback",
params=[(bool, "state")],
forward=[],
when=automation.ApplyCall(
"{}->check(state, {}, {})",
(
(CONF_TRIGGER_ID, detector.operator("ptr")),
(CONF_MIN_LENGTH, cg.uint32),
(CONF_MAX_LENGTH, cg.uint32),
),
),
)
for conf_key, detector in _CLICK_DETECTORS
),
)
@coroutine_with_priority(CoroPriority.AUTOMATION)
async def _build_binary_sensor_automations(var, config):
# The detectors must exist before the automations that name them are built
if clicks := [c for key, _ in _CLICK_DETECTORS for c in config.get(key, ())]:
cg.add_define("USE_BINARY_SENSOR_CLICK_TRIGGER")
for conf in clicks:
cg.new_Pvariable(conf[CONF_TRIGGER_ID])
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
if config.get(CONF_ON_CLICK) or config.get(CONF_ON_DOUBLE_CLICK):
cg.add_define("USE_BINARY_SENSOR_CLICK_TRIGGER")
if config.get(CONF_ON_MULTI_CLICK):
cg.add_define("USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER")
if config.get(CONF_ON_RAW_VALUE):
cg.add_define("USE_BINARY_SENSOR_RAW_CALLBACK")
for conf in config.get(CONF_ON_CLICK, []):
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], var, conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH]
)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_DOUBLE_CLICK, []):
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], var, conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH]
)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_MULTI_CLICK, []):
timings = [
cg.StructInitializer(
@@ -136,6 +136,28 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
return length >= min_length && length <= max_length;
}
}
bool ClickDetector::check(bool state, uint32_t min_length, uint32_t max_length) {
if (state) {
this->start_time_ = millis();
return false;
}
return match_interval(min_length, max_length, millis() - this->start_time_);
}
bool DoubleClickDetector::check(bool state, uint32_t min_length, uint32_t max_length) {
const uint32_t now = millis();
if (state && this->start_time_ != 0 && this->end_time_ != 0 &&
match_interval(min_length, max_length, this->end_time_ - this->start_time_) &&
match_interval(min_length, max_length, now - this->end_time_)) {
this->start_time_ = 0;
this->end_time_ = 0;
return true;
}
this->start_time_ = this->end_time_;
this->end_time_ = now;
return false;
}
#endif // USE_BINARY_SENSOR_CLICK_TRIGGER
} // namespace esphome::binary_sensor
+11 -40
View File
@@ -24,54 +24,25 @@ static_assert(std::is_same_v<decltype(MultiClickTriggerEvent::state), uint32_t>
bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length);
class ClickTrigger final : public Trigger<> {
/// Times the presses of one on_click automation.
class ClickDetector {
public:
explicit ClickTrigger(BinarySensor *parent, uint32_t min_length, uint32_t max_length)
: min_length_(min_length), max_length_(max_length) {
parent->add_on_state_callback([this](bool state) {
if (state) {
this->start_time_ = millis();
} else {
const uint32_t length = millis() - this->start_time_;
if (match_interval(this->min_length_, this->max_length_, length))
this->trigger();
}
});
}
/// Whether `state` ends a press of min_length to max_length ms; a max_length of 0 means no maximum.
bool check(bool state, uint32_t min_length, uint32_t max_length);
protected:
uint32_t start_time_{0}; /// The millis() time when the click started.
uint32_t min_length_; /// Minimum length of click. 0 means no minimum.
uint32_t max_length_; /// Maximum length of click. 0 means no maximum.
uint32_t start_time_{0}; ///< The millis() time when the press started.
};
class DoubleClickTrigger final : public Trigger<> {
/// Times the presses of one on_double_click automation.
class DoubleClickDetector {
public:
explicit DoubleClickTrigger(BinarySensor *parent, uint32_t min_length, uint32_t max_length)
: min_length_(min_length), max_length_(max_length) {
parent->add_on_state_callback([this](bool state) {
const uint32_t now = millis();
if (state && this->start_time_ != 0 && this->end_time_ != 0) {
if (match_interval(this->min_length_, this->max_length_, this->end_time_ - this->start_time_) &&
match_interval(this->min_length_, this->max_length_, now - this->end_time_)) {
this->trigger();
this->start_time_ = 0;
this->end_time_ = 0;
return;
}
}
this->start_time_ = this->end_time_;
this->end_time_ = now;
});
}
/// Whether `state` is the second press, with the first press and the gap both in the window.
bool check(bool state, uint32_t min_length, uint32_t max_length);
protected:
uint32_t start_time_{0};
uint32_t end_time_{0};
uint32_t min_length_; /// Minimum length of click. 0 means no minimum.
uint32_t max_length_; /// Maximum length of click. 0 means no maximum.
uint32_t start_time_{0}; ///< The millis() time of the edge before the last one.
uint32_t end_time_{0}; ///< The millis() time of the last edge.
};
class MultiClickTrigger final : public Trigger<>, public Component {
+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
+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
@@ -8,18 +8,24 @@ from esphome.core import CORE
def test_click_triggers(generate_main: Callable[[str | Path], str]) -> None:
"""Each trigger gets its timing in milliseconds, defaults 50 and 350, and its own automation."""
"""Each trigger gets a detector and a callback with its timing in milliseconds, defaults 50 and 350."""
main_cpp = generate_main("tests/component_tests/binary_sensor/test_click.yaml")
triggers = re.findall(
r"new\((\w+)\) binary_sensor::(Click|DoubleClick)Trigger\(button_a, (\d+), (\d+)\);",
main_cpp,
kinds = dict(
re.findall(
r"new\((\w+)\) binary_sensor::(Click|DoubleClick)Detector\(\);", main_cpp
)
)
assert [trigger[1:] for trigger in triggers] == [
calls = re.findall(r"if \(!\(::(\w+)->check\(state, (\d+), (\d+)\)\)\)", main_cpp)
assert [(kinds[detector_id], *timing) for detector_id, *timing in calls] == [
("Click", "50", "350"),
("Click", "1000", "3000"),
("DoubleClick", "20", "400"),
]
for trigger_id, *_ in triggers:
assert f"Automation<>({trigger_id});" in main_cpp
assert main_cpp.count("button_a->add_on_state_callback([](") == 3
# Only the automation with a delay keeps an Automation object
assert re.findall(r"new\((\w+)\) Automation<>\(\);", main_cpp) == [
"automation_id_2"
]
assert "::automation_id_2->trigger();" in main_cpp
assert "USE_BINARY_SENSOR_CLICK_TRIGGER" in {define.name for define in CORE.defines}
@@ -21,7 +21,9 @@ binary_sensor:
- delay: 1s
- logger.log: a long click done
on_double_click:
- min_length: 20ms
# Named like the callback parameter, which must not hide it
- trigger_id: state
min_length: 20ms
max_length: 400ms
then:
- logger.log: a double click
+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,16 +1,15 @@
#include <gtest/gtest.h>
#include <memory>
#include <functional>
#include <utility>
#include "esphome/components/binary_sensor/automation.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/binary_sensor/filter.h"
#include "esphome/core/automation.h"
#include "esphome/core/base_automation.h"
#include "esphome/core/hal.h"
namespace esphome::binary_sensor::testing {
// The triggers read the real millis(), so every wait is at least 80 ms away from a window edge
// The click detectors read the real millis(), so every wait is at least 80 ms away from a window edge
static constexpr uint32_t MIN_MS = 100;
static constexpr uint32_t MAX_MS = 300;
static constexpr uint32_t SHORT_MS = 20;
@@ -24,18 +23,24 @@ class ClickTriggerTest : public ::testing::Test {
void SetUp() override {
fired = 0;
this->sensor_.publish_state(false);
this->automation_.add_action(&this->action_);
}
// As codegen builds on_click and on_double_click
void add_click_(uint32_t min_length = MIN_MS, uint32_t max_length = MAX_MS, BinarySensor *sensor = nullptr) {
this->click_ = std::make_unique<ClickTrigger>(sensor != nullptr ? sensor : &this->sensor_, min_length, max_length);
this->click_->set_automation_parent(&this->automation_);
this->watch_(this->click_, min_length, max_length, sensor != nullptr ? *sensor : this->sensor_);
}
void add_double_click_() {
this->double_click_ = std::make_unique<DoubleClickTrigger>(&this->sensor_, MIN_MS, MAX_MS);
this->double_click_->set_automation_parent(&this->automation_);
void add_double_click_(std::function<void()> on_fire = count_fire) {
this->watch_(this->double_click_, MIN_MS, MAX_MS, this->sensor_, std::move(on_fire));
}
template<typename D>
void watch_(D &detector, uint32_t min_length, uint32_t max_length, BinarySensor &sensor,
std::function<void()> on_fire = count_fire) {
sensor.add_on_state_callback([&detector, min_length, max_length, on_fire = std::move(on_fire)](bool state) {
if (detector.check(state, min_length, max_length))
on_fire();
});
}
static void count_fire() { fired++; }
void press_for_(uint32_t length) {
this->sensor_.publish_state(true);
@@ -44,10 +49,8 @@ class ClickTriggerTest : public ::testing::Test {
}
BinarySensor sensor_;
StatelessLambdaAction<> action_{[]() { fired++; }};
Automation<> automation_;
std::unique_ptr<ClickTrigger> click_;
std::unique_ptr<DoubleClickTrigger> double_click_;
ClickDetector click_;
DoubleClickDetector double_click_;
};
TEST(ClickMatchInterval, BoundsAreInclusive) {
@@ -198,4 +201,23 @@ TEST_F(ClickTriggerTest, DoubleClickDoesNotFireTwiceWithoutReset) {
EXPECT_EQ(fired, 1);
}
TEST_F(ClickTriggerTest, DoubleClickKeepsPressPublishedByItsActions) {
// The times reset before the actions run, so a press the first action publishes is kept
this->add_double_click_([this]() {
if (++fired == 1) {
this->sensor_.publish_state(false);
this->sensor_.publish_state(true);
}
});
this->press_for_(INSIDE_MS);
delay(INSIDE_MS);
this->sensor_.publish_state(true);
EXPECT_EQ(fired, 1);
delay(INSIDE_MS);
this->sensor_.publish_state(false);
delay(INSIDE_MS);
this->sensor_.publish_state(true);
EXPECT_EQ(fired, 2);
}
} // namespace esphome::binary_sensor::testing
@@ -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