mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 09:36:04 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b310c985e0 | ||
|
|
7bc87b9b00 | ||
|
|
a5c1b4f532 | ||
|
|
8fc6fcf724 | ||
|
|
22a2efd8fd | ||
|
|
e0354ae794 | ||
|
|
f9299c1c58 | ||
|
|
09c40438e2 | ||
|
|
63df68acc4 | ||
|
|
8936e4f54b | ||
|
|
1c1cf21b16 | ||
|
|
71a0b9cfca | ||
|
|
ebc4dd2cc9 | ||
|
|
8f705e7cdb | ||
|
|
c2067f4452 | ||
|
|
e1b38f5171 | ||
|
|
ef10ea99b9 |
No files matched your search
@@ -37,6 +37,7 @@ 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,
|
||||
@@ -93,6 +94,7 @@ 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,
|
||||
@@ -108,8 +110,6 @@ 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,13 +503,10 @@ 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)
|
||||
# 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
|
||||
# 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)
|
||||
)
|
||||
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)
|
||||
@@ -542,17 +539,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())
|
||||
# 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]))
|
||||
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,
|
||||
)
|
||||
# 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())
|
||||
@@ -647,10 +644,8 @@ 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.
|
||||
@@ -667,8 +662,13 @@ 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")
|
||||
if CORE.is_rp2 and config[CONF_HARDWARE_UART] == USB_CDC:
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
|
||||
# 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")
|
||||
# Define platform support flags for components that need auto-detection
|
||||
try:
|
||||
uart_selection(USB_SERIAL_JTAG)
|
||||
@@ -718,7 +718,6 @@ 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
|
||||
@@ -739,8 +738,6 @@ 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
|
||||
@@ -875,11 +872,6 @@ 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.
|
||||
@@ -888,29 +880,18 @@ def request_logger_level_listeners() -> None:
|
||||
function during their code generation. This enables the add_level_listener()
|
||||
method and compiles in the listener vector.
|
||||
"""
|
||||
CORE.data.setdefault(DOMAIN, {})[KEY_LEVEL_LISTENERS] = True
|
||||
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")
|
||||
|
||||
|
||||
def request_log_listener() -> None:
|
||||
"""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)
|
||||
"""Reserve a log listener slot; call once per listener from to_code."""
|
||||
cg.add_define("USE_LOG_LISTENERS")
|
||||
_request_log_listener_slot()
|
||||
@@ -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 (USE_ZEPHYR)
|
||||
#elif defined(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 = get_thread_name_(current_task);
|
||||
const char *thread_name = this->get_thread_name_(current_task);
|
||||
#elif defined(USE_ZEPHYR)
|
||||
char thread_name_buf[MAX_POINTER_REPRESENTATION];
|
||||
const char *thread_name = get_thread_name_(thread_name_buf, current_task);
|
||||
const char *thread_name = this->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 const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512;
|
||||
static constexpr size_t MAX_CONSOLE_LOG_MSG_SIZE = 512;
|
||||
#else
|
||||
// Maximum size for console log messages (includes null terminator)
|
||||
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
|
||||
static constexpr 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,13 +118,11 @@ 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: 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).
|
||||
// Logging calls are NOT thread-safe: the recursion guard is a plain bool and tx_buffer_ has no locking.
|
||||
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||
if (level > this->level_for(tag) || this->main_task_recursion_guard_)
|
||||
return;
|
||||
// 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);
|
||||
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
}
|
||||
#endif // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR
|
||||
|
||||
@@ -136,10 +134,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) || global_recursion_guard_)
|
||||
if (level > this->level_for(tag) || this->main_task_recursion_guard_)
|
||||
return;
|
||||
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
}
|
||||
#endif // USE_ESP8266
|
||||
|
||||
@@ -184,10 +182,10 @@ Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || defined(USE_LOGGER_ZEPHYR_CDC_LOOP)
|
||||
void Logger::loop() {
|
||||
this->process_messages_();
|
||||
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)
|
||||
#ifdef USE_LOGGER_ZEPHYR_CDC_LOOP
|
||||
this->cdc_loop_();
|
||||
#endif
|
||||
}
|
||||
@@ -213,11 +211,11 @@ void Logger::process_messages_() {
|
||||
this->write_log_buffer_to_console_(buf);
|
||||
}
|
||||
}
|
||||
#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
|
||||
// Zephyr with USB CDC keeps the loop running for cdc_loop_()
|
||||
#ifndef USE_LOGGER_ZEPHYR_CDC_LOOP
|
||||
else {
|
||||
// No messages to process, disable loop if appropriate
|
||||
// This reduces overhead when there's no async logging activity
|
||||
this->disable_loop_when_buffer_empty_();
|
||||
// A concurrent enable_loop_soon_any_context() still wakes the next iteration
|
||||
this->disable_loop();
|
||||
}
|
||||
#endif
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
@@ -265,7 +263,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_loop_recursion_guard_();
|
||||
bool &recursion_guard = this->main_task_recursion_guard_;
|
||||
if (recursion_guard)
|
||||
return;
|
||||
this->boot_log_done_ = true; // Stops capture first, so the replay output is not buffered again
|
||||
@@ -307,6 +305,25 @@ 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"
|
||||
@@ -342,7 +359,7 @@ void Logger::dump_config() {
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZEPHYR
|
||||
dump_crash_();
|
||||
this->dump_crash_();
|
||||
#ifdef USE_BOOTLOADER_ADAFRUIT
|
||||
this->dump_bootloader_();
|
||||
#endif
|
||||
|
||||
@@ -42,9 +42,6 @@ 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():
|
||||
@@ -73,7 +70,7 @@ struct LogCallback {
|
||||
* public:
|
||||
* void setup() override {
|
||||
* if (logger::global_logger != nullptr)
|
||||
* logger::global_logger->add_logger_level_listener(this);
|
||||
* logger::global_logger->add_level_listener(this);
|
||||
* }
|
||||
* void on_log_level_change(uint8_t level) override {
|
||||
* // Handle log level change
|
||||
@@ -127,6 +124,9 @@ 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 @@ enum UARTSelection : uint8_t {
|
||||
* - 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 other platforms: Uses a simple global recursion guard
|
||||
* - On single-task platforms: Uses the main task guard alone
|
||||
*
|
||||
* We use pthread TLS via pthread_key_create to create a unique key for storing
|
||||
* task-specific recursion state, which:
|
||||
@@ -149,7 +149,7 @@ enum UARTSelection : uint8_t {
|
||||
class Logger final : public Component {
|
||||
public:
|
||||
explicit Logger(uint32_t baud_rate);
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || defined(USE_LOGGER_ZEPHYR_CDC_LOOP)
|
||||
void loop() override;
|
||||
#endif
|
||||
#if defined(USE_RP2_CRASH_HANDLER) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)
|
||||
@@ -157,21 +157,52 @@ 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 baud_rate_; }
|
||||
uint32_t get_baud_rate() const { return this->baud_rate_; }
|
||||
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
Stream *get_hw_serial() const { return hw_serial_; }
|
||||
Stream *get_hw_serial() const { return this->hw_serial_; }
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
uart_port_t get_uart_num() const { return uart_num_; }
|
||||
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
|
||||
/// 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);
|
||||
#endif
|
||||
#ifdef USE_HOST
|
||||
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
void create_pthread_key() { pthread_key_create(&this->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.
|
||||
UARTSelection get_uart() const { return this->uart_; }
|
||||
#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
|
||||
#endif
|
||||
|
||||
/// Set the default log level for this logger.
|
||||
@@ -242,14 +273,17 @@ 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
|
||||
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)
|
||||
#ifdef USE_LOGGER_ZEPHYR_CDC_LOOP
|
||||
void cdc_loop_();
|
||||
#endif
|
||||
void process_messages_();
|
||||
#if defined(USE_HOST) || defined(USE_ZEPHYR)
|
||||
void write_msg_(const char *msg, uint16_t len);
|
||||
// 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
|
||||
inline void write_msg_(const char *msg, uint16_t len); // Defined in platform-specific logger_*.h
|
||||
void write_msg_(const char *msg, uint16_t len);
|
||||
#endif
|
||||
|
||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
||||
@@ -317,7 +351,6 @@ 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) {
|
||||
@@ -359,15 +392,8 @@ class Logger final : public Component {
|
||||
#ifdef USE_HOST
|
||||
pthread_t main_thread_{}; // Main thread for pthread_equal() comparison
|
||||
#endif
|
||||
#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
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
// Non-main task recursion guard (pthread TLS)
|
||||
pthread_key_t log_recursion_key_;
|
||||
#endif
|
||||
|
||||
@@ -392,23 +418,10 @@ 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};
|
||||
#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};
|
||||
bool main_task_recursion_guard_{false}; // Guards tx_buffer_ on the main loop
|
||||
#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
|
||||
@@ -477,9 +490,9 @@ class Logger final : public Component {
|
||||
class NonMainTaskRecursionGuard {
|
||||
public:
|
||||
explicit NonMainTaskRecursionGuard(pthread_key_t key) : key_(key) {
|
||||
pthread_setspecific(key_, reinterpret_cast<void *>(1));
|
||||
pthread_setspecific(this->key_, reinterpret_cast<void *>(1));
|
||||
}
|
||||
~NonMainTaskRecursionGuard() { pthread_setspecific(key_, nullptr); }
|
||||
~NonMainTaskRecursionGuard() { pthread_setspecific(this->key_, nullptr); }
|
||||
NonMainTaskRecursionGuard(const NonMainTaskRecursionGuard &) = delete;
|
||||
NonMainTaskRecursionGuard &operator=(const NonMainTaskRecursionGuard &) = delete;
|
||||
NonMainTaskRecursionGuard(NonMainTaskRecursionGuard &&) = delete;
|
||||
@@ -490,40 +503,24 @@ 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(log_recursion_key_) != nullptr; }
|
||||
inline bool HOT is_non_main_task_recursive_() const {
|
||||
return pthread_getspecific(this->log_recursion_key_) != nullptr;
|
||||
}
|
||||
|
||||
// Create RAII guard for non-main task recursion
|
||||
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();
|
||||
inline NonMainTaskRecursionGuard make_non_main_task_guard_() {
|
||||
return NonMainTaskRecursionGuard(this->log_recursion_key_);
|
||||
}
|
||||
|
||||
#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)
|
||||
@@ -563,14 +560,7 @@ void log_crash_to_client();
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
// Platform-specific inline implementations of write_msg_()
|
||||
// Must be included after the Logger class definition is complete
|
||||
#if defined(USE_ESP32)
|
||||
// ESP32 inline write_msg_(), included after the Logger class definition is complete
|
||||
#ifdef 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
|
||||
@@ -17,6 +17,9 @@
|
||||
#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"
|
||||
@@ -73,7 +76,8 @@ static void init_usb_serial_jtag() {
|
||||
}
|
||||
#endif
|
||||
|
||||
void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) {
|
||||
#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) {
|
||||
uart_config_t uart_config{};
|
||||
uart_config.baud_rate = (int) baud_rate;
|
||||
uart_config.data_bits = UART_DATA_8_BITS;
|
||||
@@ -97,37 +101,22 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) {
|
||||
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) {
|
||||
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();
|
||||
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);
|
||||
#endif
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
global_logger = this;
|
||||
@@ -139,29 +128,36 @@ 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_ESP32_VARIANT_ESP32
|
||||
case UART_SELECTION_UART2:
|
||||
return LOG_STR("UART2");
|
||||
#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);
|
||||
#endif
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
return LOG_STR("USB_CDC");
|
||||
#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);
|
||||
#endif
|
||||
#ifdef USE_LOGGER_USB_SERIAL_JTAG
|
||||
case UART_SELECTION_USB_SERIAL_JTAG:
|
||||
return LOG_STR("USB_SERIAL_JTAG");
|
||||
#endif
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return written > 0 ? written : 0;
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -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->uart_num_, msg, len);
|
||||
uart_write_bytes(this->get_uart_num(), msg, len);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ void Logger::pre_setup() {
|
||||
#if defined(USE_ESP8266_LOGGER_SERIAL)
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
|
||||
Serial.swap();
|
||||
}
|
||||
#ifdef USE_LOGGER_UART_SELECTION_UART0_SWAP
|
||||
Serial.swap();
|
||||
#endif
|
||||
Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
#elif defined(USE_ESP8266_LOGGER_SERIAL1)
|
||||
this->hw_serial_ = &Serial1;
|
||||
@@ -35,19 +35,6 @@ 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(); }
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#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
|
||||
@@ -12,45 +12,28 @@ ESPHOME_LOG_TAG(TAG, "logger");
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
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;
|
||||
#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.");
|
||||
}
|
||||
|
||||
#endif
|
||||
// change lt_log() port to match default Serial
|
||||
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);
|
||||
}
|
||||
lt_log_set_port(this->get_uart() - 1);
|
||||
}
|
||||
|
||||
global_logger = this;
|
||||
@@ -60,20 +43,6 @@ 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(); }
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#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
|
||||
@@ -26,20 +26,16 @@ void Logger::setup() {
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
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;
|
||||
}
|
||||
#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
|
||||
}
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
@@ -49,21 +45,6 @@ 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(); }
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#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,9 +42,9 @@ __attribute__((section(".noinit"))) struct {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "logger");
|
||||
|
||||
#ifdef USE_LOGGER_UART_SELECTION_USB_CDC
|
||||
#ifdef USE_LOGGER_ZEPHYR_CDC_LOOP
|
||||
void Logger::cdc_loop_() {
|
||||
if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) {
|
||||
if (this->uart_dev_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
static bool opened = false;
|
||||
@@ -74,26 +74,20 @@ void Logger::cdc_loop_() {
|
||||
void Logger::pre_setup() {
|
||||
#ifdef CONFIG_SERIAL
|
||||
if (this->baud_rate_ > 0) {
|
||||
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:
|
||||
const struct device *uart_dev = nullptr;
|
||||
#if defined(USE_LOGGER_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();
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
#endif
|
||||
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
|
||||
#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)
|
||||
@@ -114,14 +108,10 @@ 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);
|
||||
dump_crash_();
|
||||
this->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 (this->uart_ == UART_SELECTION_USB_CDC)
|
||||
return;
|
||||
#endif
|
||||
#if defined(USE_CRASH_HANDLER) && !defined(USE_LOGGER_ZEPHYR_CDC_LOOP)
|
||||
// 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();
|
||||
@@ -143,22 +133,7 @@ void HOT Logger::write_msg_(const char *msg, uint16_t 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_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 constexpr uint8_t REASON_BUF_SIZE = 32;
|
||||
|
||||
static const char *reason_to_str(unsigned int reason, char *buf) {
|
||||
switch (reason) {
|
||||
|
||||
@@ -44,19 +44,15 @@ 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) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
// Measure on a copy; the caller's va_list formats the text later
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
int ret = vsnprintf(nullptr, 0, format, args_copy);
|
||||
int ret = task_log_cap_length(vsnprintf(nullptr, 0, format, args_copy));
|
||||
va_end(args_copy);
|
||||
|
||||
if (ret <= 0) {
|
||||
return false; // Formatting error or empty message
|
||||
}
|
||||
|
||||
// Calculate actual text length (capped to maximum size)
|
||||
static constexpr size_t MAX_TEXT_SIZE = 255;
|
||||
size_t text_length = (static_cast<size_t>(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret;
|
||||
uint16_t text_length = ret;
|
||||
|
||||
// Calculate total size needed (header + text length + null terminator)
|
||||
size_t total_size = sizeof(LogMessage) + text_length + 1;
|
||||
@@ -69,24 +65,10 @@ 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);
|
||||
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
|
||||
}
|
||||
fill_task_log_header(msg, level, tag, line, 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);
|
||||
|
||||
@@ -96,12 +78,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove trailing newlines
|
||||
while (text_length > 0 && text_area[text_length - 1] == '\n') {
|
||||
text_length--;
|
||||
}
|
||||
|
||||
msg->text_length = text_length;
|
||||
msg->text_length = trim_trailing_newlines(text_area, text_length);
|
||||
// Complete the send operation with the acquired memory
|
||||
result = xRingbufferSendComplete(this->handle_(), acquired_memory);
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/ringbuf.h>
|
||||
|
||||
#include "task_log_message.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
/**
|
||||
@@ -32,19 +34,7 @@ namespace esphome::logger {
|
||||
*/
|
||||
class TaskLogBuffer {
|
||||
public:
|
||||
// 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); }
|
||||
};
|
||||
using LogMessage = TaskLogMessage;
|
||||
|
||||
TaskLogBuffer();
|
||||
// No destructor: Logger is never destroyed
|
||||
|
||||
@@ -71,18 +71,8 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
|
||||
LogMessage &msg = this->slots_[slot_index];
|
||||
|
||||
// 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';
|
||||
}
|
||||
// Copied now; the thread may exit before processing
|
||||
fill_task_log_header(&msg, level, tag, line, thread_name);
|
||||
|
||||
// Format the message text
|
||||
int ret = vsnprintf(msg.text, sizeof(msg.text), format, args);
|
||||
@@ -94,10 +84,7 @@ 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));
|
||||
}
|
||||
|
||||
// Remove trailing newlines
|
||||
while (msg.text_length > 0 && msg.text[msg.text_length - 1] == '\n') {
|
||||
msg.text_length--;
|
||||
}
|
||||
msg.text_length = trim_trailing_newlines(msg.text, msg.text_length);
|
||||
msg.text[msg.text_length] = '\0';
|
||||
|
||||
// Commit the slot
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <cstring>
|
||||
#include <pthread.h>
|
||||
|
||||
#include "task_log_message.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,13 +13,6 @@ 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
|
||||
@@ -85,19 +78,15 @@ 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) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
// Measure on a copy; the caller's va_list formats the text later
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
int ret = vsnprintf(nullptr, 0, format, args_copy);
|
||||
int ret = task_log_cap_length(vsnprintf(nullptr, 0, format, args_copy));
|
||||
va_end(args_copy);
|
||||
|
||||
if (ret <= 0) {
|
||||
return false; // Formatting error or empty message
|
||||
}
|
||||
|
||||
// Calculate actual text length (capped to maximum size)
|
||||
static constexpr size_t MAX_TEXT_SIZE = 255;
|
||||
size_t text_length = (static_cast<size_t>(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret;
|
||||
uint16_t text_length = ret;
|
||||
|
||||
// Calculate total size needed (header + text length + null terminator)
|
||||
size_t total_size = message_total_size(text_length);
|
||||
@@ -128,17 +117,7 @@ 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);
|
||||
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';
|
||||
}
|
||||
fill_task_log_header(msg, level, tag, line, thread_name);
|
||||
|
||||
// Format the message text directly into the buffer
|
||||
char *text_area = msg->text_data();
|
||||
@@ -150,9 +129,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
}
|
||||
|
||||
// Trim before storing so the next message reuses the freed bytes
|
||||
while (text_length > 0 && text_area[text_length - 1] == '\n') {
|
||||
text_length--;
|
||||
}
|
||||
text_length = trim_trailing_newlines(text_area, text_length);
|
||||
text_area[text_length] = '\0';
|
||||
msg->text_length = text_length;
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <semphr.h>
|
||||
#include <task.h>
|
||||
|
||||
#include "task_log_message.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
/**
|
||||
@@ -42,25 +44,14 @@ namespace esphome::logger {
|
||||
*/
|
||||
class TaskLogBuffer {
|
||||
public:
|
||||
// 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); }
|
||||
};
|
||||
using LogMessage = TaskLogMessage;
|
||||
|
||||
// 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();
|
||||
~TaskLogBuffer();
|
||||
// No destructor: Logger is never destroyed
|
||||
|
||||
// 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,29 +29,21 @@ 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) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
// Measure on a copy; the caller's va_list formats the text later
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
int ret = vsnprintf(nullptr, 0, format, args_copy);
|
||||
int ret = task_log_cap_length(vsnprintf(nullptr, 0, format, args_copy));
|
||||
va_end(args_copy);
|
||||
|
||||
if (ret <= 0) {
|
||||
return false; // Formatting error or empty message
|
||||
}
|
||||
|
||||
// Calculate actual text length (capped to maximum size)
|
||||
static constexpr size_t MAX_TEXT_SIZE = 255;
|
||||
size_t text_length = (static_cast<size_t>(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret;
|
||||
uint16_t text_length = ret;
|
||||
size_t total_size = total_size_in_32bit_words(text_length);
|
||||
auto *msg = reinterpret_cast<LogMessage *>(mpsc_pbuf_alloc(&this->log_buffer_, total_size, K_NO_WAIT));
|
||||
if (msg == nullptr) {
|
||||
return false;
|
||||
}
|
||||
msg->level = level;
|
||||
msg->tag = tag;
|
||||
msg->line = line;
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
||||
fill_task_log_header(msg, level, tag, line, 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
|
||||
@@ -90,11 +82,7 @@ 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 = message->text_length;
|
||||
// Remove trailing newlines
|
||||
while (text_length > 0 && message->text_data()[text_length - 1] == '\n') {
|
||||
text_length--;
|
||||
}
|
||||
text_length = trim_trailing_newlines(message->text_data(), message->text_length);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
#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
|
||||
|
||||
@@ -34,7 +36,6 @@ 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_); }
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#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
|
||||
@@ -614,25 +614,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
|
||||
def FILTER_SOURCE_FILES() -> list[str]:
|
||||
"""Filter out web_server_v1.cpp when version is not 1, the ESP32 event stream elsewhere, and the server on host."""
|
||||
"""Filter out web_server_v1.cpp when version is not 1."""
|
||||
files_to_filter: list[str] = []
|
||||
|
||||
# The host has no web server; it builds only sse_chunk.cpp, for its unit tests
|
||||
if CORE.is_host:
|
||||
files_to_filter.extend(
|
||||
(
|
||||
"event_source.cpp",
|
||||
"list_entities.cpp",
|
||||
"web_server.cpp",
|
||||
"web_server_v1.cpp",
|
||||
)
|
||||
)
|
||||
return files_to_filter
|
||||
|
||||
# The /events implementation is ESP32 only
|
||||
if not CORE.is_esp32:
|
||||
files_to_filter.extend(("event_source.cpp", "sse_chunk.cpp"))
|
||||
|
||||
# web_server_v1.cpp is only needed when version is 1
|
||||
config = CORE.config.get(DOMAIN, {})
|
||||
if config.get(CONF_VERSION, 2) != 1:
|
||||
|
||||
@@ -1,491 +0,0 @@
|
||||
#include "event_source.h"
|
||||
#if defined(USE_ESP32) && defined(USE_WEBSERVER)
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "web_server.h"
|
||||
|
||||
namespace esphome::web_server {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "web_server_idf");
|
||||
|
||||
// Only prepare_json_() may hold the JSON arena: every other frame in this file is capped below one
|
||||
// arena. Measured at -Os on GCC 14; newer toolchains stay checked on purpose, older ones skip it.
|
||||
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
|
||||
#pragma GCC diagnostic error "-Wstack-usage=2048"
|
||||
#endif
|
||||
|
||||
AsyncEventSource::~AsyncEventSource() {
|
||||
LockGuard guard{this->pending_mutex_};
|
||||
for (auto *vec : {&this->sessions_, &this->pending_sessions_}) {
|
||||
for (auto *ses : *vec) {
|
||||
delete ses; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) {
|
||||
// Httpd task: set up the live httpd_req_t and park the session; main loop does the rest.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory,clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
auto *rsp = new AsyncEventSourceResponse(request, this, this->web_server_);
|
||||
{
|
||||
LockGuard guard{this->pending_mutex_};
|
||||
this->pending_sessions_.push_back(rsp);
|
||||
this->has_pending_sessions_.store(true, std::memory_order_release);
|
||||
}
|
||||
this->web_server_->enable_loop_soon_any_context();
|
||||
}
|
||||
|
||||
// clang-analyzer traces a false-positive leak path from loop() through
|
||||
// adopt_pending_sessions_main_loop_() into start_session_main_loop_() and
|
||||
// finally ArduinoJson. Suppress along the entire in-our-code call chain.
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
bool AsyncEventSource::loop() {
|
||||
// Fast path: one atomic load per tick. Slow path is out-of-line on connect.
|
||||
if (this->has_pending_sessions_.load(std::memory_order_acquire)) {
|
||||
this->adopt_pending_sessions_main_loop_();
|
||||
}
|
||||
|
||||
// Clean up dead sessions safely
|
||||
// This follows the ESP-IDF pattern where free_ctx marks resources as dead
|
||||
// and the main loop handles the actual cleanup to avoid race conditions
|
||||
for (size_t i = 0; i < this->sessions_.size();) {
|
||||
auto *ses = this->sessions_[i];
|
||||
// If the session has a dead socket (marked by destroy callback)
|
||||
if (ses->safe_to_delete()) {
|
||||
// destroy() already logged the close with the fd; don't double-log here.
|
||||
delete ses; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
// Remove by swapping with last element (O(1) removal, order doesn't matter for sessions)
|
||||
this->sessions_[i] = this->sessions_.back();
|
||||
this->sessions_.pop_back();
|
||||
} else {
|
||||
ses->loop();
|
||||
++i;
|
||||
}
|
||||
}
|
||||
// A session still waiting for httpd to commit its context keeps the loop alive too
|
||||
return !this->sessions_.empty() || this->has_pending_sessions_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void AsyncEventSource::adopt_pending_sessions_main_loop_() {
|
||||
std::vector<AsyncEventSourceResponse *> incoming;
|
||||
{
|
||||
LockGuard guard{this->pending_mutex_};
|
||||
incoming.swap(this->pending_sessions_);
|
||||
this->has_pending_sessions_.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
for (auto *rsp : incoming) {
|
||||
// Already disconnected? Drop it; skip session start on a dead session.
|
||||
if (rsp->safe_to_delete()) {
|
||||
delete rsp; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
continue;
|
||||
}
|
||||
// httpd commits the session context only after the creating handler returns, so stay
|
||||
// pending until then; httpd_req_cleanup() always commits it or calls destroy()
|
||||
if (!rsp->is_attached()) {
|
||||
LockGuard guard{this->pending_mutex_};
|
||||
this->pending_sessions_.push_back(rsp);
|
||||
this->has_pending_sessions_.store(true, std::memory_order_release);
|
||||
continue;
|
||||
}
|
||||
this->sessions_.push_back(rsp);
|
||||
rsp->start_session_main_loop_();
|
||||
}
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
void AsyncEventSource::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
|
||||
uint32_t reconnect) {
|
||||
for (auto *ses : this->sessions_) {
|
||||
if (ses->is_connected()) {
|
||||
ses->try_send_nodefer(message, message_len, event, id, reconnect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncEventSource::deferrable_send_state(void *source, bool detail_all, message_generator_t *message_generator) {
|
||||
// Skip if no connected clients to avoid unnecessary processing
|
||||
if (this->empty())
|
||||
return;
|
||||
for (auto *ses : this->sessions_) {
|
||||
if (ses->is_connected()) {
|
||||
ses->deferrable_send_state(source, detail_all, message_generator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncWebServerRequest *request, AsyncEventSource *server,
|
||||
WebServer *ws)
|
||||
: server_(server), web_server_(ws), entities_iterator_(ws, this) {
|
||||
// Httpd task only. start_session_main_loop_() sends the greeting and starts the iterator.
|
||||
this->begin(request, SSE_STATUS, SSE_CONTENT_TYPE, SSE_HEADERS, SSE_FIRST_CHUNK);
|
||||
}
|
||||
static_assert(web_server_base::stream_head_iov_count(std::size(SSE_HEADERS) + WEB_SERVER_DEFAULT_HEADERS_COUNT) <=
|
||||
web_server_base::STREAM_HEAD_MAX_IOV,
|
||||
"the event stream head must go out in one write");
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
void AsyncEventSourceResponse::start_session_main_loop_() {
|
||||
auto *ws = this->web_server_;
|
||||
|
||||
// The tcp send buffer is empty on connect. A refusal is a closing session or a failed tail
|
||||
// allocation; nothing retries the greeting, so close and let the client reconnect.
|
||||
auto message = ws->get_config_json();
|
||||
if (!this->try_send_nodefer(message.c_str(), message.size(), "ping", millis(), 30000)) {
|
||||
ESP_LOGW(TAG, "Config not sent to fd %d", this->log_id());
|
||||
this->close();
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_WEBSERVER_SORTING
|
||||
for (auto &group : ws->sorting_groups_) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
root["name"] = json::linked(group.second.name);
|
||||
root["sorting_weight"] = group.second.weight;
|
||||
message = builder.serialize();
|
||||
|
||||
// a (very) large number of these should be able to be queued initially without defer
|
||||
// since the only thing in the send buffer at this point is the initial ping/config.
|
||||
// A refusal means the socket is full or closing; the remaining groups are not sent. The
|
||||
// session stays up on purpose: partial grouping beats a reconnect loop on a slow link.
|
||||
if (!this->try_send_nodefer(message.c_str(), message.size(), "sorting_group")) {
|
||||
ESP_LOGW(TAG, "Sorting groups not sent to fd %d", this->log_id());
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
this->entities_iterator_.begin(ws->include_internal_);
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
// helper for allowing only unique entries in the queue
|
||||
void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) {
|
||||
DeferredEvent item(source, message_generator);
|
||||
|
||||
// Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
|
||||
for (auto &event : this->deferred_queue_) {
|
||||
if (event == item) {
|
||||
return; // Already in queue, no need to update since items are equal
|
||||
}
|
||||
}
|
||||
this->deferred_queue_.push_back(item);
|
||||
}
|
||||
|
||||
void AsyncEventSourceResponse::process_deferred_queue_() {
|
||||
if (this->is_closing()) {
|
||||
return;
|
||||
}
|
||||
while (!deferred_queue_.empty()) {
|
||||
DeferredEvent &de = deferred_queue_.front();
|
||||
if (this->send_json_(de.source_, de.message_generator_)) {
|
||||
if (this->is_closing() || deferred_queue_.empty()) {
|
||||
return;
|
||||
}
|
||||
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
|
||||
deferred_queue_.erase(deferred_queue_.begin());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncEventSourceResponse::release_buffers_() {
|
||||
std::vector<DeferredEvent>().swap(this->deferred_queue_);
|
||||
this->tail_.reset();
|
||||
this->tail_cap_ = 0;
|
||||
this->tail_len_ = 0;
|
||||
this->tail_sent_ = 0;
|
||||
}
|
||||
|
||||
void AsyncEventSourceResponse::drain_tail_() {
|
||||
if (this->sending_ || this->is_closing() || this->tail_len_ == 0) {
|
||||
return;
|
||||
}
|
||||
SendGuard guard{*this};
|
||||
|
||||
const size_t remaining = this->tail_len_ - this->tail_sent_;
|
||||
const ssize_t sent = this->write(this->tail_.get() + this->tail_sent_, remaining);
|
||||
if (sent <= 0) {
|
||||
return; // write() owns the stall clock and the close
|
||||
}
|
||||
this->no_memory_logged_ = false;
|
||||
this->tail_sent_ += sent;
|
||||
if (this->tail_sent_ < this->tail_len_) {
|
||||
ESP_LOGV(TAG, "Partial send: %zd/%zu bytes (total: %u/%u)", sent, remaining, this->tail_sent_, this->tail_len_);
|
||||
return;
|
||||
}
|
||||
// Fully sent; the storage stays for the next stall
|
||||
this->tail_len_ = 0;
|
||||
}
|
||||
|
||||
bool AsyncEventSourceResponse::reserve_tail_(size_t len) {
|
||||
if (this->tail_cap_ >= len) {
|
||||
return true;
|
||||
}
|
||||
if (len > TAIL_MAX_SIZE) {
|
||||
return false;
|
||||
}
|
||||
// Nothing is pending while the tail grows, so free the old block first. PREFER_INTERNAL keeps
|
||||
// the tail where plain new put it.
|
||||
this->tail_.reset();
|
||||
this->tail_ = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::PREFER_INTERNAL).make_unique_array_for_overwrite(len);
|
||||
this->tail_cap_ = this->tail_ ? len : 0;
|
||||
return this->tail_cap_ != 0;
|
||||
}
|
||||
|
||||
bool AsyncEventSourceResponse::stash_chunk_(const char *prefix, size_t prefix_len, const char *message,
|
||||
size_t message_len, size_t total, size_t sent) {
|
||||
// A log event of nothing but line breaks is the largest chunk; reserve_tail_() must never
|
||||
// refuse it for size, only for memory
|
||||
static_assert(SSE_SEP_LEN * LOG_EVENT_MAX_LEN + PREFIX_BUF_SIZE + SSE_SUFFIX_LEN <= TAIL_MAX_SIZE,
|
||||
"the log cut in web_server.h must keep a worst case log event inside the tail ceiling");
|
||||
if (!this->reserve_tail_(total)) {
|
||||
if (sent != 0) {
|
||||
// Part of the chunk is on the wire, so the stream is broken and the client has to go
|
||||
ESP_LOGW(TAG, "Cannot buffer a %zu byte chunk, closing", total);
|
||||
this->close();
|
||||
} else {
|
||||
this->tail_alloc_failed_(total); // nothing on the wire, the caller retries on the stall clock
|
||||
}
|
||||
return false;
|
||||
}
|
||||
uint8_t *dst = this->tail_.get();
|
||||
std::memcpy(dst, prefix, prefix_len);
|
||||
dst += prefix_len;
|
||||
for_each_chunk_piece(
|
||||
message, message_len,
|
||||
[](void *ctx, const char *piece, size_t len) {
|
||||
auto &out = *static_cast<uint8_t **>(ctx);
|
||||
std::memcpy(out, piece, len);
|
||||
out += len;
|
||||
},
|
||||
&dst);
|
||||
this->tail_len_ = total;
|
||||
this->tail_sent_ = sent;
|
||||
return true;
|
||||
}
|
||||
|
||||
void AsyncEventSourceResponse::loop() {
|
||||
AsyncStreamResponse::loop();
|
||||
if (this->is_closing()) {
|
||||
if (this->tail_cap_ != 0 || this->deferred_queue_.capacity() != 0) {
|
||||
this->release_buffers_();
|
||||
}
|
||||
return;
|
||||
}
|
||||
drain_tail_();
|
||||
process_deferred_queue_();
|
||||
if (this->is_closing())
|
||||
return;
|
||||
// One step per loop; refusals retry next pass
|
||||
this->entities_iterator_.try_advance(1);
|
||||
}
|
||||
|
||||
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wstack-usage=" // the one frame that holds the JSON arena
|
||||
#endif
|
||||
AsyncEventSourceResponse::JsonPrepareResult AsyncEventSourceResponse::prepare_json_(void *source,
|
||||
message_generator_t *generator,
|
||||
char *buffer, size_t *size_out) {
|
||||
// Log lines emitted while the generator runs are dropped for this session: on the large path they would fill
|
||||
// tail_ before it is overwritten, and on the small path they would go out ahead of the event being built.
|
||||
SendGuard guard{*this};
|
||||
json::JsonArena<json::JSON_ARENA_SIZE> arena;
|
||||
json::JsonBuilder builder(&arena);
|
||||
generator(this->web_server_, source, builder);
|
||||
*size_out = builder.serialize_to(buffer, JSON_BUF_SIZE);
|
||||
if (*size_out < JSON_BUF_SIZE) {
|
||||
return JsonPrepareResult::JSON_PREPARE_RESULT_BUFFER;
|
||||
}
|
||||
|
||||
char prefix[PREFIX_BUF_SIZE];
|
||||
const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), "state", 0, 0, true);
|
||||
|
||||
// Nothing reaches the wire until the complete document and its framing are owned by the caller.
|
||||
size_t json_len = 0;
|
||||
size_t cap = std::max<size_t>(JSON_BUF_SIZE * 2, this->tail_cap_);
|
||||
for (;;) {
|
||||
if (!this->reserve_tail_(cap)) {
|
||||
*size_out = cap;
|
||||
return JsonPrepareResult::JSON_PREPARE_RESULT_NO_MEMORY;
|
||||
}
|
||||
const size_t room = cap - prefix_len - SSE_SUFFIX_LEN;
|
||||
json_len = builder.serialize_to(reinterpret_cast<char *>(this->tail_.get()) + prefix_len, room);
|
||||
if (json_len < room) {
|
||||
break;
|
||||
}
|
||||
if (cap >= TAIL_MAX_SIZE) {
|
||||
*size_out = room;
|
||||
return JsonPrepareResult::JSON_PREPARE_RESULT_TOO_LARGE;
|
||||
}
|
||||
cap = std::min<size_t>(cap * 2, TAIL_MAX_SIZE);
|
||||
}
|
||||
|
||||
const size_t total = prefix_len + json_len + SSE_SUFFIX_LEN;
|
||||
write_chunk_header(prefix, total - CHUNK_HDR_LEN - CHUNK_END_LEN);
|
||||
uint8_t *dst = this->tail_.get();
|
||||
std::memcpy(dst, prefix, prefix_len);
|
||||
std::memcpy(dst + prefix_len + json_len, SSE_SUFFIX, SSE_SUFFIX_LEN);
|
||||
this->tail_len_ = total;
|
||||
this->tail_sent_ = 0;
|
||||
return JsonPrepareResult::JSON_PREPARE_RESULT_TAIL;
|
||||
}
|
||||
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
bool AsyncEventSourceResponse::send_json_(void *source, message_generator_t *generator) {
|
||||
if (!this->ready_to_send_()) {
|
||||
return false;
|
||||
}
|
||||
char buffer[JSON_BUF_SIZE];
|
||||
size_t size;
|
||||
const auto result = this->prepare_json_(source, generator, buffer, &size);
|
||||
switch (result) {
|
||||
case JsonPrepareResult::JSON_PREPARE_RESULT_BUFFER:
|
||||
return this->try_send_nodefer(buffer, size, "state");
|
||||
case JsonPrepareResult::JSON_PREPARE_RESULT_TAIL:
|
||||
this->drain_tail_();
|
||||
return true;
|
||||
case JsonPrepareResult::JSON_PREPARE_RESULT_NO_MEMORY: {
|
||||
SendGuard guard{*this};
|
||||
this->tail_alloc_failed_(size);
|
||||
return false;
|
||||
}
|
||||
case JsonPrepareResult::JSON_PREPARE_RESULT_TOO_LARGE: {
|
||||
SendGuard guard{*this};
|
||||
ESP_LOGW(TAG, "State event does not fit %zu bytes, dropped", size);
|
||||
this->tail_.reset();
|
||||
this->tail_cap_ = 0;
|
||||
this->stall_.clear();
|
||||
this->no_memory_logged_ = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AsyncEventSourceResponse::tail_alloc_failed_(size_t cap) {
|
||||
// Same stall clock as a socket that stops draining, so a session cannot retry forever. A refused write may
|
||||
// already have started it, so the warning keeps its own flag.
|
||||
if (!this->no_memory_logged_) {
|
||||
this->no_memory_logged_ = true;
|
||||
ESP_LOGW(TAG, "No memory for a %zu byte chunk", cap);
|
||||
}
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (!this->stall_.is_running()) {
|
||||
this->stall_.start(now);
|
||||
return;
|
||||
}
|
||||
if (this->stall_.is_expired(now, SEND_STALL_TIMEOUT_MS)) {
|
||||
ESP_LOGW(TAG, "Closing EventSource after %" PRIu32 " ms without memory", this->stall_.elapsed(now));
|
||||
this->close();
|
||||
}
|
||||
}
|
||||
|
||||
bool AsyncEventSourceResponse::ready_to_send_() {
|
||||
if (this->sending_ || !this->is_connected() || this->is_closing()) {
|
||||
return false;
|
||||
}
|
||||
drain_tail_();
|
||||
return !this->is_closing() && this->tail_len_ == 0;
|
||||
}
|
||||
|
||||
bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
|
||||
uint32_t reconnect) {
|
||||
if (!this->ready_to_send_()) {
|
||||
return false;
|
||||
}
|
||||
SendGuard guard{*this};
|
||||
|
||||
// Everything after the prefix goes out straight from the caller's buffer
|
||||
char prefix[PREFIX_BUF_SIZE];
|
||||
const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), event, id, reconnect, message != nullptr);
|
||||
if (message == nullptr && prefix_len == CHUNK_HDR_LEN) {
|
||||
return true; // Match ESPAsyncWebServer: nothing to send
|
||||
}
|
||||
if (prefix_len >= PREFIX_BUF_SIZE - 1) {
|
||||
// The appenders truncate silently, which would put a malformed event on the wire
|
||||
ESP_LOGW(TAG, "Event name too long, dropped");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Gather list: the prefix, then the data lines and their separators from the message
|
||||
struct Gather {
|
||||
web_server_base::StreamIovec iov[MAX_SEND_IOV]; // left uninitialized on purpose
|
||||
int iovcnt{1};
|
||||
size_t total{0};
|
||||
bool fits{true};
|
||||
} g;
|
||||
g.total = prefix_len;
|
||||
for_each_chunk_piece(
|
||||
message, message_len,
|
||||
[](void *ctx, const char *piece, size_t len) {
|
||||
auto &g = *static_cast<Gather *>(ctx);
|
||||
g.total += len;
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
if (g.iovcnt == MAX_SEND_IOV) {
|
||||
g.fits = false;
|
||||
return;
|
||||
}
|
||||
g.iov[g.iovcnt++] = {const_cast<char *>(piece), len};
|
||||
},
|
||||
&g);
|
||||
// The header and the terminator are not part of the chunk length
|
||||
write_chunk_header(prefix, g.total - CHUNK_HDR_LEN - CHUNK_END_LEN);
|
||||
g.iov[0] = {prefix, prefix_len};
|
||||
|
||||
// A message with more lines than the list holds skips straight to the tail
|
||||
const ssize_t sent = g.fits ? this->write(g.iov, g.iovcnt) : 0;
|
||||
if (sent < 0) {
|
||||
return false;
|
||||
}
|
||||
if (sent > 0) {
|
||||
this->no_memory_logged_ = false;
|
||||
}
|
||||
if (static_cast<size_t>(sent) == g.total) {
|
||||
return true;
|
||||
}
|
||||
// The caller's buffers do not outlive this call, so keep the chunk and continue from loop()
|
||||
return this->stash_chunk_(prefix, prefix_len, message, message_len, g.total, sent);
|
||||
}
|
||||
|
||||
void AsyncEventSourceResponse::deferrable_send_state(void *source, bool detail_all,
|
||||
message_generator_t *message_generator) {
|
||||
// Let the detail_all listing finish before bare state events, so the web UI never shows unnamed entries and
|
||||
// initial connect sends less
|
||||
if (!detail_all && !this->entities_iterator_.completed())
|
||||
return;
|
||||
|
||||
drain_tail_();
|
||||
process_deferred_queue_();
|
||||
|
||||
if (this->is_closing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->tail_len_ != 0 || !deferred_queue_.empty()) {
|
||||
// outgoing event buffer or deferred queue still not empty which means downstream tcp send buffer full, no point
|
||||
// trying to send first
|
||||
deq_push_back_with_dedup_(source, message_generator);
|
||||
} else {
|
||||
// A send error closes the session and clears the queue; nothing is queued after that
|
||||
if (!this->send_json_(source, message_generator) && !this->is_closing()) {
|
||||
deq_push_back_with_dedup_(source, message_generator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::web_server
|
||||
|
||||
#endif // USE_ESP32 && USE_WEBSERVER
|
||||
@@ -1,176 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_ESP32) && defined(USE_WEBSERVER)
|
||||
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/web_server_base/stream_response.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "list_entities.h"
|
||||
#include "sse_chunk.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::web_server {
|
||||
|
||||
class WebServer;
|
||||
class AsyncEventSource;
|
||||
class AsyncEventSourceResponse;
|
||||
|
||||
using message_generator_t = void(WebServer *, void *, json::JsonBuilder &);
|
||||
|
||||
/*
|
||||
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
|
||||
that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for
|
||||
the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a
|
||||
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two pointers
|
||||
per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
|
||||
because of dedup) would take up only 0.8 kB.
|
||||
*/
|
||||
struct DeferredEvent {
|
||||
friend class AsyncEventSourceResponse;
|
||||
|
||||
protected:
|
||||
void *source_;
|
||||
message_generator_t *message_generator_;
|
||||
|
||||
public:
|
||||
DeferredEvent(void *source, message_generator_t *message_generator)
|
||||
: source_(source), message_generator_(message_generator) {}
|
||||
bool operator==(const DeferredEvent &test) const {
|
||||
return (source_ == test.source_ && message_generator_ == test.message_generator_);
|
||||
}
|
||||
};
|
||||
static_assert(sizeof(DeferredEvent) == sizeof(void *) + sizeof(message_generator_t *),
|
||||
"DeferredEvent should have no padding");
|
||||
|
||||
class AsyncEventSourceResponse : public web_server_base::AsyncStreamResponse {
|
||||
friend class AsyncEventSource;
|
||||
|
||||
public:
|
||||
bool try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0,
|
||||
uint32_t reconnect = 0);
|
||||
// detail_all events go out while entities are still being listed; state events wait until listing is done
|
||||
void deferrable_send_state(void *source, bool detail_all, message_generator_t *message_generator);
|
||||
// The old event name argument would otherwise convert to true
|
||||
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator) = delete;
|
||||
void loop();
|
||||
|
||||
protected:
|
||||
AsyncEventSourceResponse(AsyncWebServerRequest *request, AsyncEventSource *server, WebServer *ws);
|
||||
|
||||
// Main-loop only: sends initial ping/config/sorting_groups, starts entity iterator.
|
||||
void start_session_main_loop_();
|
||||
|
||||
void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator);
|
||||
void process_deferred_queue_();
|
||||
// A new chunk may go out: not re-entered from a log line, session alive, tail empty
|
||||
bool ready_to_send_();
|
||||
// Push what is left of the chunk in tail_ to the socket
|
||||
void drain_tail_();
|
||||
// Grow tail_ to hold len bytes, kept at its high-water mark. False on OOM.
|
||||
bool reserve_tail_(size_t len);
|
||||
// Keep the whole chunk in tail_ and continue from sent; false when the tail cannot be allocated.
|
||||
bool stash_chunk_(const char *prefix, size_t prefix_len, const char *message, size_t message_len, size_t total,
|
||||
size_t sent);
|
||||
// Send a state event; JSON too large for the stack buffer is serialized into tail_ instead
|
||||
bool send_json_(void *source, message_generator_t *generator);
|
||||
enum class JsonPrepareResult : uint8_t {
|
||||
JSON_PREPARE_RESULT_BUFFER,
|
||||
JSON_PREPARE_RESULT_TAIL,
|
||||
JSON_PREPARE_RESULT_NO_MEMORY,
|
||||
JSON_PREPARE_RESULT_TOO_LARGE,
|
||||
};
|
||||
// Keep the arena out of the caller's frame so it is gone before the state send.
|
||||
// size_out: BUFFER = JSON length, NO_MEMORY = requested tail capacity, TOO_LARGE = available JSON room;
|
||||
// unused for TAIL.
|
||||
JsonPrepareResult __attribute__((noinline)) prepare_json_(void *source, message_generator_t *generator, char *buffer,
|
||||
size_t *size_out);
|
||||
// Warn once per stall, and close the session once the stall timeout passes with no memory for the tail
|
||||
void tail_alloc_failed_(size_t cap);
|
||||
|
||||
// A log line emitted inside a send re-enters try_send_nodefer on this session; refuse it
|
||||
struct SendGuard {
|
||||
AsyncEventSourceResponse &owner;
|
||||
explicit SendGuard(AsyncEventSourceResponse &owner) : owner(owner) { owner.sending_ = true; }
|
||||
~SendGuard() { this->owner.sending_ = false; }
|
||||
};
|
||||
// Frees the tail and the queue's storage once the session is closing
|
||||
void release_buffers_();
|
||||
|
||||
AsyncEventSource *server_;
|
||||
std::vector<DeferredEvent> deferred_queue_;
|
||||
WebServer *web_server_;
|
||||
ListEntitiesIterator entities_iterator_;
|
||||
// One chunk the socket did not take whole, allocated on the first stall; the only heap use
|
||||
// on the send path
|
||||
RAMUniquePtr<uint8_t[]> tail_;
|
||||
uint16_t tail_cap_{0};
|
||||
uint16_t tail_len_{0}; // Zero means nothing pending
|
||||
uint16_t tail_sent_{0};
|
||||
bool sending_{false};
|
||||
// The no-memory warning went out during the current stall; cleared on send progress
|
||||
bool no_memory_logged_{false};
|
||||
// The longest multi line log message in the tree (a climate dump_config) has 22 lines; a
|
||||
// longer one goes through the tail
|
||||
static constexpr size_t MAX_SEND_LINES = 22;
|
||||
static constexpr size_t MAX_SEND_IOV = 1 + 2 * MAX_SEND_LINES;
|
||||
// Chunk header, retry/id/event lines and the first "data: "
|
||||
static constexpr size_t PREFIX_BUF_SIZE = 128;
|
||||
|
||||
// Stack buffer for a state event's JSON; a larger document is serialized into the tail
|
||||
static constexpr size_t JSON_BUF_SIZE = 1024;
|
||||
// Same ceiling JsonBuilder::serialize() applies (max_heap_size in json_util.cpp); a larger
|
||||
// document is dropped before anything is on the wire
|
||||
static constexpr size_t JSON_MAX_SIZE = 5120;
|
||||
// Most RAM a stalled session keeps: the largest state document plus any accepted framing
|
||||
static constexpr size_t TAIL_MAX_SIZE = JSON_MAX_SIZE + PREFIX_BUF_SIZE + SSE_SUFFIX_LEN;
|
||||
};
|
||||
|
||||
class AsyncEventSource : public AsyncWebHandler {
|
||||
friend class AsyncEventSourceResponse;
|
||||
|
||||
public:
|
||||
AsyncEventSource(StringRef url, WebServer *ws) : url_(url), web_server_(ws) {}
|
||||
~AsyncEventSource() override;
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
bool canHandle(AsyncWebServerRequest *request) const override {
|
||||
if (request->method() != HTTP_GET)
|
||||
return false;
|
||||
char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
|
||||
return request->url_to(url_buf) == this->url_;
|
||||
}
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
void handleRequest(AsyncWebServerRequest *request) override;
|
||||
void try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0,
|
||||
uint32_t reconnect = 0);
|
||||
void deferrable_send_state(void *source, bool detail_all, message_generator_t *message_generator);
|
||||
// The old event name argument would otherwise convert to true
|
||||
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator) = delete;
|
||||
/// Returns true if there are sessions remaining (including pending cleanup).
|
||||
bool loop();
|
||||
bool empty() { return this->count() == 0; }
|
||||
|
||||
size_t count() const { return this->sessions_.size(); }
|
||||
|
||||
protected:
|
||||
// Cold path: move sessions from pending_sessions_ into sessions_ and greet each one.
|
||||
void __attribute__((noinline, cold)) adopt_pending_sessions_main_loop_();
|
||||
|
||||
StringRef url_; // Must outlive this object (string literal)
|
||||
// Main-loop only. Vector: SSE sessions are 1-5 connections, linear search beats set.
|
||||
std::vector<AsyncEventSourceResponse *> sessions_;
|
||||
// Httpd-task intake; guarded by pending_mutex_, gated by has_pending_sessions_.
|
||||
std::vector<AsyncEventSourceResponse *> pending_sessions_;
|
||||
Mutex pending_mutex_;
|
||||
WebServer *web_server_;
|
||||
std::atomic<bool> has_pending_sessions_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::web_server
|
||||
|
||||
#endif // USE_ESP32 && USE_WEBSERVER
|
||||
@@ -8,8 +8,14 @@
|
||||
|
||||
namespace esphome::web_server {
|
||||
|
||||
#ifdef USE_ESP32
|
||||
ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource *es) : web_server_(ws), events_(es) {}
|
||||
#elif USE_ARDUINO
|
||||
ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es)
|
||||
: web_server_(ws), events_(es) {}
|
||||
#endif
|
||||
void ListEntitiesIterator::send_all_(void *source, message_generator_t *generator) {
|
||||
this->session_->deferrable_send_state(source, true, generator);
|
||||
this->events_->deferrable_send_state(source, true, generator);
|
||||
}
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_WEBSERVER) && (defined(USE_ESP32) || defined(USE_ARDUINO))
|
||||
#ifdef USE_WEBSERVER
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/component_iterator.h"
|
||||
namespace esphome::json {
|
||||
class JsonBuilder;
|
||||
} // namespace esphome::json
|
||||
|
||||
namespace esphome::web_server_idf {
|
||||
#ifdef USE_ESP32
|
||||
class AsyncEventSource;
|
||||
#endif
|
||||
} // namespace esphome::web_server_idf
|
||||
|
||||
namespace esphome::web_server {
|
||||
|
||||
// The /events session the iterator lists entities to
|
||||
#ifdef USE_ESP32
|
||||
class AsyncEventSourceResponse;
|
||||
using EventSession = AsyncEventSourceResponse;
|
||||
#elif defined(USE_ARDUINO)
|
||||
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
|
||||
class DeferredUpdateEventSource;
|
||||
using EventSession = DeferredUpdateEventSource;
|
||||
#endif
|
||||
class WebServer;
|
||||
|
||||
class ListEntitiesIterator final : public ComponentIterator {
|
||||
public:
|
||||
ListEntitiesIterator(const WebServer *ws, EventSession *session) : web_server_(ws), session_(session) {}
|
||||
#ifdef USE_ESP32
|
||||
ListEntitiesIterator(const WebServer *ws, esphome::web_server_idf::AsyncEventSource *es);
|
||||
#elif defined(USE_ARDUINO)
|
||||
ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es);
|
||||
#endif
|
||||
|
||||
// Entity overrides (generated from entity_types.h).
|
||||
// Implementations live in list_entities.cpp.
|
||||
@@ -40,7 +45,11 @@ class ListEntitiesIterator final : public ComponentIterator {
|
||||
void send_all_(void *source, void (*generator)(WebServer *, void *, json::JsonBuilder &));
|
||||
|
||||
const WebServer *web_server_;
|
||||
EventSession *session_;
|
||||
#ifdef USE_ESP32
|
||||
esphome::web_server_idf::AsyncEventSource *events_;
|
||||
#elif USE_ARDUINO
|
||||
DeferredUpdateEventSource *events_;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::web_server
|
||||
|
||||
@@ -245,7 +245,7 @@ void DeferredUpdateEventSource::process_deferred_queue_() {
|
||||
deferred_queue_.erase(deferred_queue_.begin());
|
||||
this->consecutive_send_failures_ = 0; // Reset failure count on successful send
|
||||
} else {
|
||||
// NOTE: Similar logic exists in web_server_idf/web_server_idf.cpp in AsyncStreamResponse::write().
|
||||
// NOTE: Similar logic exists in web_server_idf/web_server_idf.cpp in AsyncEventSourceResponse::drain_tail_().
|
||||
// The close mechanisms are platform-specific (this path calls close() directly; the IDF path is time-based and
|
||||
// closes through HTTPD to preserve session ownership), but both drop a client after roughly 20 seconds without
|
||||
// send progress. Keep that stall policy in sync when changing either side.
|
||||
|
||||
@@ -5,11 +5,7 @@
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/web_server_base/web_server_base.h"
|
||||
#include "esphome/components/web_server_base/captive_dns.h"
|
||||
// The host only builds sse_chunk.cpp for unit tests, but other tests can define USE_WEBSERVER there
|
||||
#if defined(USE_WEBSERVER) && (defined(USE_ESP32) || defined(USE_ARDUINO))
|
||||
#ifdef USE_ESP32
|
||||
#include "event_source.h"
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/entity_includes.h"
|
||||
|
||||
@@ -105,9 +105,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(cg.RawExpression(f"{web_server_base_ns}::global_web_server_base = {var}"))
|
||||
|
||||
# Default headers added in init(): web_server_idf's StaticVector, or the library's DefaultHeaders
|
||||
cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1)
|
||||
if CORE.is_esp32:
|
||||
# Count for StaticVector in web_server_idf - matches headers added in init()
|
||||
cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1)
|
||||
return
|
||||
|
||||
# ESP32 uses IDF web server (early return above), so this is for other Arduino platforms
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
|
||||
#include <strings.h>
|
||||
|
||||
// Pure helpers of AsyncStreamResponse, kept free of httpd and lwIP so they build on the host.
|
||||
// The iovec type is a template parameter: lwIP defines its own on the device, POSIX on the host.
|
||||
|
||||
namespace esphome::web_server_base {
|
||||
|
||||
struct HttpHeader {
|
||||
const char *name;
|
||||
const char *value;
|
||||
};
|
||||
|
||||
/// Drops the first sent bytes from a gather list after a write; a short write resumes inside the
|
||||
/// element it stopped in. count reaches 0 once everything went out.
|
||||
template<typename Iovec> void advance_iovecs(Iovec *&iov, int &count, size_t sent) {
|
||||
while (count > 0 && sent >= iov->iov_len) {
|
||||
sent -= iov->iov_len;
|
||||
++iov;
|
||||
--count;
|
||||
}
|
||||
if (count > 0) {
|
||||
iov->iov_base = static_cast<char *>(iov->iov_base) + sent;
|
||||
iov->iov_len -= sent;
|
||||
}
|
||||
}
|
||||
|
||||
/// Pieces in a head: 4 for the status line and Content-Type, 4 per header, the blank line and the body
|
||||
constexpr size_t stream_head_iov_count(size_t header_count) { return 4 + 4 * header_count + 2; }
|
||||
|
||||
/// Pieces per batch: a head with up to four headers goes out in one write; 192 bytes on 32-bit
|
||||
static constexpr int STREAM_HEAD_MAX_IOV = 24;
|
||||
|
||||
/// True if headers holds a header named name, compared case insensitively
|
||||
inline bool has_header(std::span<const HttpHeader> headers, const char *name) {
|
||||
for (const auto &header : headers) {
|
||||
if (strcasecmp(header.name, name) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Gathers the head of a stream response: "HTTP/1.1 <status>", Content-Type, headers, the default_headers that
|
||||
/// headers does not replace, the blank line and body (may be null). send(Iovec *iov, int count) gets each full batch of
|
||||
/// STREAM_HEAD_MAX_IOV pieces and then the rest; once it returns false the later pieces are skipped. Returns false if
|
||||
/// any send failed.
|
||||
template<typename Iovec, typename Send>
|
||||
bool write_stream_head(Send &&send, const char *status, const char *content_type, std::span<const HttpHeader> headers,
|
||||
std::span<const HttpHeader> default_headers, const char *body) {
|
||||
struct Batch {
|
||||
Send &send;
|
||||
int count{0};
|
||||
bool ok{true};
|
||||
Iovec iov[STREAM_HEAD_MAX_IOV]; // left uninitialized on purpose
|
||||
|
||||
void flush() {
|
||||
if (this->ok && this->count > 0) {
|
||||
this->ok = this->send(this->iov, this->count);
|
||||
}
|
||||
this->count = 0;
|
||||
}
|
||||
void add(const char *buf, size_t len) {
|
||||
if (this->count == STREAM_HEAD_MAX_IOV) {
|
||||
this->flush();
|
||||
}
|
||||
this->iov[this->count++] = {const_cast<char *>(buf), len};
|
||||
}
|
||||
void add(const char *str) { this->add(str, strlen(str)); }
|
||||
void add_header(const HttpHeader &header) {
|
||||
this->add("\r\n", 2);
|
||||
this->add(header.name);
|
||||
this->add(": ", 2);
|
||||
this->add(header.value);
|
||||
}
|
||||
} batch{send};
|
||||
|
||||
batch.add("HTTP/1.1 ", 9);
|
||||
batch.add(status);
|
||||
batch.add("\r\nContent-Type: ", 16);
|
||||
batch.add(content_type);
|
||||
for (const auto &header : headers) {
|
||||
batch.add_header(header);
|
||||
}
|
||||
for (const auto &header : default_headers) {
|
||||
if (!has_header(headers, header.name)) {
|
||||
batch.add_header(header);
|
||||
}
|
||||
}
|
||||
batch.add("\r\n\r\n", 4);
|
||||
if (body != nullptr) {
|
||||
batch.add(body);
|
||||
}
|
||||
batch.flush();
|
||||
return batch.ok;
|
||||
}
|
||||
|
||||
/// How long sends have made no progress. Zero means not running, so a start at millis() == 0 records 1.
|
||||
/// The comparison is wraparound safe across a millis() rollover.
|
||||
class StallClock {
|
||||
public:
|
||||
/// Starts the clock; a running clock keeps its start time.
|
||||
void start(uint32_t now) {
|
||||
if (this->started_ms_ == 0) {
|
||||
this->started_ms_ = now != 0 ? now : 1;
|
||||
}
|
||||
}
|
||||
void clear() { this->started_ms_ = 0; }
|
||||
bool is_running() const { return this->started_ms_ != 0; }
|
||||
/// True once the clock has run for timeout_ms
|
||||
bool is_expired(uint32_t now, uint32_t timeout_ms) const {
|
||||
return this->started_ms_ != 0 && static_cast<int32_t>(now - (this->started_ms_ + timeout_ms)) >= 0;
|
||||
}
|
||||
uint32_t elapsed(uint32_t now) const { return now - this->started_ms_; }
|
||||
|
||||
protected:
|
||||
uint32_t started_ms_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::web_server_base
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
// The long lived stream response the /events layer writes to, for the platform's web server
|
||||
#ifdef USE_ESP32
|
||||
#include "esphome/components/web_server_idf/web_server_idf.h"
|
||||
|
||||
#include <sys/socket.h>
|
||||
|
||||
namespace esphome::web_server_base {
|
||||
using web_server_idf::AsyncStreamResponse;
|
||||
// One entry of the gather list AsyncStreamResponse::write() takes
|
||||
using StreamIovec = ::iovec;
|
||||
} // namespace esphome::web_server_base
|
||||
#endif
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::web_server {
|
||||
namespace esphome::web_server_idf {
|
||||
|
||||
void for_each_chunk_piece(const char *message, size_t message_len, ChunkPieceSink sink, void *ctx) {
|
||||
if (message == nullptr) {
|
||||
@@ -63,4 +63,4 @@ void write_chunk_header(char *buf, size_t chunk_len) {
|
||||
buf[9] = '\n';
|
||||
}
|
||||
|
||||
} // namespace esphome::web_server
|
||||
} // namespace esphome::web_server_idf
|
||||
+2
-15
@@ -3,9 +3,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/web_server_base/stream_head.h"
|
||||
|
||||
namespace esphome::web_server {
|
||||
namespace esphome::web_server_idf {
|
||||
|
||||
// Wire framing of one Server-Sent-Events event inside the chunked /events response:
|
||||
// "%08x\r\n" + fields + "data: " line ["\r\ndata: " line]... "\r\n" + "\r\n" + "\r\n"
|
||||
@@ -24,17 +22,6 @@ constexpr size_t SSE_SEP_LEN = sizeof(SSE_SEP) - 1;
|
||||
constexpr char SSE_SUFFIX[] = "\r\n\r\n\r\n";
|
||||
constexpr size_t SSE_SUFFIX_LEN = sizeof(SSE_SUFFIX) - 1;
|
||||
|
||||
// Status, Content-Type and headers of the /events response head
|
||||
constexpr char SSE_STATUS[] = "200 OK";
|
||||
constexpr char SSE_CONTENT_TYPE[] = "text/event-stream";
|
||||
constexpr web_server_base::HttpHeader SSE_HEADERS[] = {
|
||||
{"Transfer-Encoding", "chunked"},
|
||||
{"Cache-Control", "no-cache"},
|
||||
{"Connection", "keep-alive"},
|
||||
};
|
||||
// Hand-framed first chunk holding an empty event (one blank line), as httpd_resp_send_chunk() sent it before
|
||||
constexpr char SSE_FIRST_CHUNK[] = "2\r\n\r\n\r\n";
|
||||
|
||||
// Receives one piece of the chunk; ctx is whatever the caller passed to for_each_chunk_piece()
|
||||
using ChunkPieceSink = void (*)(void *ctx, const char *piece, size_t len);
|
||||
|
||||
@@ -50,4 +37,4 @@ size_t build_chunk_prefix(char *buf, size_t size, const char *event, uint32_t id
|
||||
// Fills the header placeholder at the start of buf once the chunk length is known
|
||||
void write_chunk_header(char *buf, size_t chunk_len);
|
||||
|
||||
} // namespace esphome::web_server
|
||||
} // namespace esphome::web_server_idf
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include "utils.h"
|
||||
#include "sse_chunk.h"
|
||||
#include "web_server_idf.h"
|
||||
|
||||
#ifdef USE_WEBSERVER_AUTH_DIGEST
|
||||
@@ -29,15 +30,17 @@
|
||||
#include "multipart.h" // For parse_multipart_boundary and other utils
|
||||
#endif
|
||||
|
||||
#ifdef USE_WEBSERVER
|
||||
#include "esphome/components/web_server/web_server.h"
|
||||
#include "esphome/components/web_server/list_entities.h"
|
||||
#endif // USE_WEBSERVER
|
||||
|
||||
// Include socket headers after Arduino headers to avoid IPADDR_NONE/INADDR_NONE macro conflicts
|
||||
#include <cerrno>
|
||||
#include <sys/socket.h>
|
||||
|
||||
namespace esphome::web_server_idf {
|
||||
|
||||
using web_server_base::advance_iovecs;
|
||||
using web_server_base::write_stream_head;
|
||||
|
||||
// Status strings not provided by esp_http_server.h
|
||||
#ifndef HTTPD_401
|
||||
#define HTTPD_401 "401 Unauthorized"
|
||||
@@ -52,10 +55,13 @@ using web_server_base::write_stream_head;
|
||||
#define HTTPD_422 "422 Unprocessable Entity"
|
||||
#endif
|
||||
|
||||
#define CRLF_STR "\r\n"
|
||||
#define CRLF_LEN (sizeof(CRLF_STR) - 1)
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "web_server_idf");
|
||||
|
||||
// Every frame in this file is capped below one JSON arena.
|
||||
// Measured at -Os on GCC 14; newer toolchains stay checked on purpose, older ones skip it.
|
||||
// Only prepare_json_() may hold the JSON arena: every other frame in this file is capped below one
|
||||
// arena. Measured at -Os on GCC 14; newer toolchains stay checked on purpose, older ones skip it.
|
||||
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
|
||||
#pragma GCC diagnostic error "-Wstack-usage=2048"
|
||||
#endif
|
||||
@@ -134,8 +140,6 @@ void AsyncWebServer::safe_close_with_shutdown(httpd_handle_t hd, int sockfd) {
|
||||
//
|
||||
// See: https://github.com/esphome/esphome-webserver/issues/163
|
||||
|
||||
AsyncStreamResponse::socket_closing(hd, sockfd);
|
||||
|
||||
// Attempt shutdown - ignore errors as socket may already be closed
|
||||
shutdown(sockfd, SHUT_RD);
|
||||
|
||||
@@ -732,84 +736,224 @@ void AsyncResponseStream::printf(const char *fmt, ...) {
|
||||
this->print(str);
|
||||
}
|
||||
|
||||
void AsyncStreamResponse::begin(AsyncWebServerRequest *request, const char *status, const char *content_type,
|
||||
std::span<const HttpHeader> headers, const char *body) {
|
||||
httpd_req_t *req = *request;
|
||||
// Same head httpd_resp_send_chunk() writes, without its first chunk. Blocking sends: the session's non-blocking
|
||||
// send override is not installed yet.
|
||||
const int fd = httpd_req_to_sockfd(req);
|
||||
const auto &defaults = DefaultHeaders::Instance().headers_;
|
||||
const bool head_sent = write_stream_head<struct iovec>(
|
||||
[fd](struct iovec *iov, int count) {
|
||||
while (count > 0) {
|
||||
struct msghdr msg{};
|
||||
msg.msg_iov = iov;
|
||||
msg.msg_iovlen = count;
|
||||
const ssize_t sent = sendmsg(fd, &msg, 0);
|
||||
if (sent <= 0) {
|
||||
return false;
|
||||
}
|
||||
advance_iovecs(iov, count, sent);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
status, content_type, headers, std::span<const HttpHeader>(defaults.data(), defaults.size()), body);
|
||||
|
||||
req->sess_ctx = this;
|
||||
req->free_ctx = AsyncStreamResponse::destroy;
|
||||
|
||||
this->hd_ = req->handle;
|
||||
this->fd_.store(fd);
|
||||
|
||||
// Use non-blocking send to prevent watchdog timeouts when TCP buffers are full
|
||||
httpd_sess_set_send_override(this->hd_, fd, nonblocking_send);
|
||||
|
||||
if (!head_sent) {
|
||||
// A partly sent head must not be continued by later writes. close() is main loop only, so shut the socket
|
||||
// down here; the first write() then fails and closes the stream, and httpd frees the session as usual.
|
||||
shutdown(fd, SHUT_RDWR);
|
||||
#ifdef USE_WEBSERVER
|
||||
AsyncEventSource::~AsyncEventSource() {
|
||||
LockGuard guard{this->pending_mutex_};
|
||||
for (auto *vec : {&this->sessions_, &this->pending_sessions_}) {
|
||||
for (auto *ses : *vec) {
|
||||
delete ses; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Held around a stream's ownership check and send, and while its socket is marked closed.
|
||||
// Created at boot, so no allocation can fail once requests arrive.
|
||||
static Mutex stream_send_mutex; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void AsyncStreamResponse::socket_closing(httpd_handle_t hd, int fd) {
|
||||
// Only streams set a session context
|
||||
auto *rsp = static_cast<AsyncStreamResponse *>(httpd_sess_get_ctx(hd, fd));
|
||||
if (rsp == nullptr)
|
||||
return;
|
||||
LockGuard guard{stream_send_mutex};
|
||||
rsp->socket_closed_ = true;
|
||||
void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) {
|
||||
// Httpd task: set up the live httpd_req_t and park the session; main loop does the rest.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory,clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
auto *rsp = new AsyncEventSourceResponse(request, this, this->web_server_);
|
||||
{
|
||||
LockGuard guard{this->pending_mutex_};
|
||||
this->pending_sessions_.push_back(rsp);
|
||||
this->has_pending_sessions_.store(true, std::memory_order_release);
|
||||
}
|
||||
this->web_server_->enable_loop_soon_any_context();
|
||||
}
|
||||
|
||||
void AsyncStreamResponse::destroy(void *ptr) {
|
||||
auto *rsp = static_cast<AsyncStreamResponse *>(ptr);
|
||||
// clang-analyzer traces a false-positive leak path from loop() through
|
||||
// adopt_pending_sessions_main_loop_() into start_session_main_loop_() and
|
||||
// finally ArduinoJson. Suppress along the entire in-our-code call chain.
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
bool AsyncEventSource::loop() {
|
||||
// Fast path: one atomic load per tick. Slow path is out-of-line on connect.
|
||||
if (this->has_pending_sessions_.load(std::memory_order_acquire)) {
|
||||
this->adopt_pending_sessions_main_loop_();
|
||||
}
|
||||
|
||||
// Clean up dead sessions safely
|
||||
// This follows the ESP-IDF pattern where free_ctx marks resources as dead
|
||||
// and the main loop handles the actual cleanup to avoid race conditions
|
||||
for (size_t i = 0; i < this->sessions_.size();) {
|
||||
auto *ses = this->sessions_[i];
|
||||
// If the session has a dead socket (marked by destroy callback)
|
||||
if (ses->safe_to_delete_()) {
|
||||
// destroy() already logged the close with the fd; don't double-log here.
|
||||
delete ses; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
// Remove by swapping with last element (O(1) removal, order doesn't matter for sessions)
|
||||
this->sessions_[i] = this->sessions_.back();
|
||||
this->sessions_.pop_back();
|
||||
} else {
|
||||
ses->loop();
|
||||
++i;
|
||||
}
|
||||
}
|
||||
// A session still waiting for httpd to commit its context keeps the loop alive too
|
||||
return !this->sessions_.empty() || this->has_pending_sessions_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void AsyncEventSource::adopt_pending_sessions_main_loop_() {
|
||||
std::vector<AsyncEventSourceResponse *> incoming;
|
||||
{
|
||||
LockGuard guard{this->pending_mutex_};
|
||||
incoming.swap(this->pending_sessions_);
|
||||
this->has_pending_sessions_.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
for (auto *rsp : incoming) {
|
||||
// Already disconnected? Drop it; skip session start on a dead session.
|
||||
if (rsp->safe_to_delete_()) {
|
||||
delete rsp; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
continue;
|
||||
}
|
||||
// httpd commits the session context only after the creating handler returns, so stay
|
||||
// pending until then; httpd_req_cleanup() always commits it or calls destroy()
|
||||
if (httpd_sess_get_ctx(rsp->hd_, rsp->fd_.load()) != rsp) {
|
||||
LockGuard guard{this->pending_mutex_};
|
||||
this->pending_sessions_.push_back(rsp);
|
||||
this->has_pending_sessions_.store(true, std::memory_order_release);
|
||||
continue;
|
||||
}
|
||||
this->sessions_.push_back(rsp);
|
||||
rsp->start_session_main_loop_();
|
||||
}
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
void AsyncEventSource::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
|
||||
uint32_t reconnect) {
|
||||
for (auto *ses : this->sessions_) {
|
||||
if (ses->fd_.load() != 0) { // Skip dead sessions
|
||||
ses->try_send_nodefer(message, message_len, event, id, reconnect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncEventSource::deferrable_send_state(void *source, bool detail_all, message_generator_t *message_generator) {
|
||||
// Skip if no connected clients to avoid unnecessary processing
|
||||
if (this->empty())
|
||||
return;
|
||||
for (auto *ses : this->sessions_) {
|
||||
if (ses->fd_.load() != 0) { // Skip dead sessions
|
||||
ses->deferrable_send_state(source, detail_all, message_generator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *request,
|
||||
esphome::web_server_idf::AsyncEventSource *server,
|
||||
esphome::web_server::WebServer *ws)
|
||||
: server_(server), web_server_(ws), entities_iterator_(ws, server) {
|
||||
// Httpd task only. start_session_main_loop_() sends the greeting and starts the iterator.
|
||||
httpd_req_t *req = *request;
|
||||
|
||||
httpd_resp_set_status(req, HTTPD_200);
|
||||
httpd_resp_set_type(req, "text/event-stream");
|
||||
httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
|
||||
httpd_resp_set_hdr(req, "Connection", "keep-alive");
|
||||
|
||||
DefaultHeaders::Instance().apply_(req);
|
||||
|
||||
httpd_resp_send_chunk(req, CRLF_STR, CRLF_LEN);
|
||||
|
||||
req->sess_ctx = this;
|
||||
req->free_ctx = AsyncEventSourceResponse::destroy;
|
||||
|
||||
this->hd_ = req->handle;
|
||||
this->fd_.store(httpd_req_to_sockfd(req));
|
||||
|
||||
// Use non-blocking send to prevent watchdog timeouts when TCP buffers are full
|
||||
httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send);
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
void AsyncEventSourceResponse::start_session_main_loop_() {
|
||||
auto *ws = this->web_server_;
|
||||
|
||||
// The tcp send buffer is empty on connect. A refusal is a closing session or a failed tail
|
||||
// allocation; nothing retries the greeting, so close and let the client reconnect.
|
||||
auto message = ws->get_config_json();
|
||||
if (!this->try_send_nodefer(message.c_str(), message.size(), "ping", millis(), 30000)) {
|
||||
ESP_LOGW(TAG, "Config not sent to fd %d", this->fd_.load());
|
||||
this->request_close_();
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_WEBSERVER_SORTING
|
||||
for (auto &group : ws->sorting_groups_) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
root["name"] = json::linked(group.second.name);
|
||||
root["sorting_weight"] = group.second.weight;
|
||||
message = builder.serialize();
|
||||
|
||||
// a (very) large number of these should be able to be queued initially without defer
|
||||
// since the only thing in the send buffer at this point is the initial ping/config.
|
||||
// A refusal means the socket is full or closing; the remaining groups are not sent. The
|
||||
// session stays up on purpose: partial grouping beats a reconnect loop on a slow link.
|
||||
if (!this->try_send_nodefer(message.c_str(), message.size(), "sorting_group")) {
|
||||
ESP_LOGW(TAG, "Sorting groups not sent to fd %d", this->fd_.load());
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
this->entities_iterator_.begin(ws->include_internal_);
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
void AsyncEventSourceResponse::destroy(void *ptr) {
|
||||
auto *rsp = static_cast<AsyncEventSourceResponse *>(ptr);
|
||||
int fd = rsp->fd_.exchange(0); // Atomically get and clear fd
|
||||
ESP_LOGD(TAG, "Stream connection closed (fd: %d)", fd);
|
||||
// Mark as dead - the owner deletes it from the main loop once safe_to_delete().
|
||||
ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd);
|
||||
// Mark as dead - will be cleaned up in the main loop
|
||||
// Note: We don't delete or remove from set here to avoid race conditions
|
||||
// httpd will call our custom close_fn (safe_close_with_shutdown) which handles
|
||||
// shutdown() before close() to prevent lwIP race conditions
|
||||
}
|
||||
|
||||
void AsyncStreamResponse::close() {
|
||||
// helper for allowing only unique entries in the queue
|
||||
void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) {
|
||||
DeferredEvent item(source, message_generator);
|
||||
|
||||
// Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
|
||||
for (auto &event : this->deferred_queue_) {
|
||||
if (event == item) {
|
||||
return; // Already in queue, no need to update since items are equal
|
||||
}
|
||||
}
|
||||
this->deferred_queue_.push_back(item);
|
||||
}
|
||||
|
||||
void AsyncEventSourceResponse::process_deferred_queue_() {
|
||||
if (this->close_requested_) {
|
||||
return;
|
||||
}
|
||||
while (!deferred_queue_.empty()) {
|
||||
DeferredEvent &de = deferred_queue_.front();
|
||||
if (this->send_json_(de.source_, de.message_generator_)) {
|
||||
if (this->close_requested_ || deferred_queue_.empty()) {
|
||||
return;
|
||||
}
|
||||
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
|
||||
deferred_queue_.erase(deferred_queue_.begin());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncEventSourceResponse::request_close_() {
|
||||
if (!this->close_requested_) {
|
||||
this->close_requested_ = true;
|
||||
this->deferred_queue_.clear();
|
||||
this->tail_.reset();
|
||||
this->tail_cap_ = 0;
|
||||
this->tail_len_ = 0;
|
||||
this->tail_sent_ = 0;
|
||||
this->next_close_attempt_ms_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
this->process_close_();
|
||||
}
|
||||
|
||||
void AsyncStreamResponse::loop() {
|
||||
if (this->close_requested_) {
|
||||
this->process_close_();
|
||||
} else {
|
||||
this->refresh_lru_();
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncStreamResponse::process_close_() {
|
||||
void AsyncEventSourceResponse::process_close_() {
|
||||
if (!this->close_requested_ || this->close_work_queued_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
@@ -827,7 +971,7 @@ void AsyncStreamResponse::process_close_() {
|
||||
// httpd_sess_trigger_close() queues only a reusable fd/session slot and can
|
||||
// therefore close a new client if the original peer disconnects meanwhile.
|
||||
this->close_work_queued_.store(true, std::memory_order_release);
|
||||
const esp_err_t err = httpd_queue_work(this->hd_, &AsyncStreamResponse::close_session_work, this);
|
||||
const esp_err_t err = httpd_queue_work(this->hd_, &AsyncEventSourceResponse::close_session_work, this);
|
||||
this->next_close_attempt_ms_ = now + (err == ESP_OK ? CLOSE_CONFIRM_INTERVAL_MS : CLOSE_RETRY_INTERVAL_MS);
|
||||
if (err == ESP_OK) {
|
||||
return;
|
||||
@@ -835,17 +979,17 @@ void AsyncStreamResponse::process_close_() {
|
||||
|
||||
this->close_work_queued_.store(false, std::memory_order_release);
|
||||
if (!this->close_retry_warning_logged_) {
|
||||
ESP_LOGW(TAG, "Failed to queue stream close (%s); retrying", esp_err_to_name(err));
|
||||
ESP_LOGW(TAG, "Failed to queue EventSource close (%s); retrying", esp_err_to_name(err));
|
||||
this->close_retry_warning_logged_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncStreamResponse::refresh_lru_() {
|
||||
// httpd only advances a session's LRU counter when a request arrives, and a stream receives one
|
||||
void AsyncEventSourceResponse::refresh_lru_() {
|
||||
// httpd only advances a session's LRU counter when a request arrives, and an event stream receives one
|
||||
// request ever, so the purge that frees a socket for a new client would evict the live streams first.
|
||||
// The HTTPD task owns the session table, so the refresh runs there.
|
||||
// A stalled stream is the socket the purge should take, and a zeroed fd has no session left to refresh.
|
||||
if (this->stall_.is_running() || this->fd_.load() == 0)
|
||||
if (this->send_failure_started_ms_ != 0 || this->fd_.load() == 0)
|
||||
return;
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->last_lru_refresh_ms_ < LRU_REFRESH_INTERVAL_MS ||
|
||||
@@ -854,13 +998,13 @@ void AsyncStreamResponse::refresh_lru_() {
|
||||
}
|
||||
this->last_lru_refresh_ms_ = now; // a full queue also waits for the next interval
|
||||
this->lru_work_queued_.store(true, std::memory_order_release);
|
||||
if (httpd_queue_work(this->hd_, &AsyncStreamResponse::refresh_lru_work, this) != ESP_OK) {
|
||||
if (httpd_queue_work(this->hd_, &AsyncEventSourceResponse::refresh_lru_work, this) != ESP_OK) {
|
||||
this->lru_work_queued_.store(false, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncStreamResponse::refresh_lru_work(void *arg) {
|
||||
auto *response = static_cast<AsyncStreamResponse *>(arg);
|
||||
void AsyncEventSourceResponse::refresh_lru_work(void *arg) {
|
||||
auto *response = static_cast<AsyncEventSourceResponse *>(arg);
|
||||
const int fd = response->fd_.load();
|
||||
if (response->owns_session_(fd)) {
|
||||
httpd_sess_update_lru_counter(response->hd_, fd);
|
||||
@@ -869,8 +1013,8 @@ void AsyncStreamResponse::refresh_lru_work(void *arg) {
|
||||
response->lru_work_queued_.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
void AsyncStreamResponse::close_session_work(void *arg) {
|
||||
auto *response = static_cast<AsyncStreamResponse *>(arg);
|
||||
void AsyncEventSourceResponse::close_session_work(void *arg) {
|
||||
auto *response = static_cast<AsyncEventSourceResponse *>(arg);
|
||||
const int fd = response->fd_.load();
|
||||
if (response->owns_session_(fd)) {
|
||||
// The HTTPD task remains the session owner. Shutting the socket down makes
|
||||
@@ -882,61 +1026,320 @@ void AsyncStreamResponse::close_session_work(void *arg) {
|
||||
response->close_work_queued_.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
ssize_t AsyncStreamResponse::write(struct iovec *iov, int iovcnt) {
|
||||
if (this->close_requested_) {
|
||||
return -1;
|
||||
}
|
||||
// Nothing to send is not a stall
|
||||
size_t total = 0;
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
total += iov[i].iov_len;
|
||||
}
|
||||
if (total == 0) {
|
||||
return 0;
|
||||
}
|
||||
// httpd closes the socket before it clears the session, so the fd number may already be a new client's.
|
||||
ssize_t AsyncEventSourceResponse::send_(struct iovec *iov, int iovcnt) {
|
||||
// httpd frees a session before closing its socket, so the fd may already be a new client's.
|
||||
// Treated as would-block; the stall timer ends a session that never becomes ours again.
|
||||
const int fd = this->fd_.load();
|
||||
ssize_t sent = 0;
|
||||
int err = 0;
|
||||
{
|
||||
// Held across the check and the send, so socket_closing() cannot close the socket in between
|
||||
LockGuard guard{stream_send_mutex};
|
||||
if (!this->socket_closed_ && this->owns_session_(fd)) {
|
||||
struct msghdr msg{};
|
||||
msg.msg_iov = iov;
|
||||
msg.msg_iovlen = iovcnt;
|
||||
sent = sendmsg(fd, &msg, MSG_DONTWAIT);
|
||||
err = errno;
|
||||
}
|
||||
if (!this->owns_session_(fd)) {
|
||||
return 0;
|
||||
}
|
||||
if (sent > 0) {
|
||||
this->stall_.clear();
|
||||
struct msghdr msg{};
|
||||
msg.msg_iov = iov;
|
||||
msg.msg_iovlen = iovcnt;
|
||||
const ssize_t sent = sendmsg(fd, &msg, MSG_DONTWAIT);
|
||||
if (sent >= 0) {
|
||||
return sent;
|
||||
}
|
||||
if (sent < 0 && err != EAGAIN && err != EWOULDBLOCK) {
|
||||
ESP_LOGD(TAG, "send error: errno %d", err);
|
||||
// A hard error is not a timeout, so is_stalled() must not report one
|
||||
this->stall_.clear();
|
||||
this->close();
|
||||
return -1;
|
||||
const int err = errno;
|
||||
if (err == EAGAIN || err == EWOULDBLOCK) {
|
||||
return 0;
|
||||
}
|
||||
// Socket buffer full, try again later; a socket that stays full past the stall timeout is closed
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->stall_.start(now);
|
||||
if (this->stall_.is_expired(now, SEND_STALL_TIMEOUT_MS)) {
|
||||
ESP_LOGW(TAG, "Closing stuck stream after %" PRIu32 " ms without send progress", this->stall_.elapsed(now));
|
||||
this->close();
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
ESP_LOGD(TAG, "send error: errno %d", err);
|
||||
this->request_close_();
|
||||
return -1;
|
||||
}
|
||||
|
||||
ssize_t AsyncStreamResponse::write(const void *data, size_t len) {
|
||||
struct iovec iov = {const_cast<void *>(data), len};
|
||||
return this->write(&iov, 1);
|
||||
void AsyncEventSourceResponse::drain_tail_() {
|
||||
if (this->sending_ || this->close_requested_ || this->tail_len_ == 0) {
|
||||
return;
|
||||
}
|
||||
SendGuard guard{*this};
|
||||
|
||||
const size_t remaining = this->tail_len_ - this->tail_sent_;
|
||||
struct iovec iov = {this->tail_.get() + this->tail_sent_, remaining};
|
||||
const ssize_t sent = this->send_(&iov, 1);
|
||||
if (sent < 0) {
|
||||
return;
|
||||
}
|
||||
if (sent == 0) {
|
||||
// Socket buffer full, try again later
|
||||
// NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_().
|
||||
// The IDF path is intentionally time-based and closes through HTTPD to preserve session ownership.
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (this->send_failure_started_ms_ == 0) {
|
||||
this->send_failure_started_ms_ = now != 0 ? now : 1; // Reserve zero for no stall.
|
||||
}
|
||||
if (static_cast<int32_t>(now - (this->send_failure_started_ms_ + SEND_STALL_TIMEOUT_MS)) >= 0) {
|
||||
ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu32 " ms without send progress",
|
||||
now - this->send_failure_started_ms_);
|
||||
this->request_close_();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this->send_failure_started_ms_ = 0;
|
||||
this->tail_sent_ += sent;
|
||||
if (this->tail_sent_ < this->tail_len_) {
|
||||
ESP_LOGV(TAG, "Partial send: %zd/%zu bytes (total: %u/%u)", sent, remaining, this->tail_sent_, this->tail_len_);
|
||||
return;
|
||||
}
|
||||
// Fully sent; the storage stays for the next stall
|
||||
this->tail_len_ = 0;
|
||||
}
|
||||
|
||||
bool AsyncEventSourceResponse::reserve_tail_(size_t len) {
|
||||
if (this->tail_cap_ >= len) {
|
||||
return true;
|
||||
}
|
||||
if (len > TAIL_MAX_SIZE) {
|
||||
return false;
|
||||
}
|
||||
// Nothing is pending while the tail grows, so free the old block first. PREFER_INTERNAL keeps
|
||||
// the tail where plain new put it.
|
||||
this->tail_.reset();
|
||||
this->tail_ = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::PREFER_INTERNAL).make_unique_array_for_overwrite(len);
|
||||
this->tail_cap_ = this->tail_ ? len : 0;
|
||||
return this->tail_cap_ != 0;
|
||||
}
|
||||
|
||||
bool AsyncEventSourceResponse::stash_chunk_(const char *prefix, size_t prefix_len, const char *message,
|
||||
size_t message_len, size_t total, size_t sent) {
|
||||
// A log event of nothing but line breaks is the largest chunk; reserve_tail_() must never
|
||||
// refuse it for size, only for memory
|
||||
static_assert(SSE_SEP_LEN * web_server::LOG_EVENT_MAX_LEN + PREFIX_BUF_SIZE + SSE_SUFFIX_LEN <= TAIL_MAX_SIZE,
|
||||
"the log cut in web_server.h must keep a worst case log event inside the tail ceiling");
|
||||
if (!this->reserve_tail_(total)) {
|
||||
if (sent != 0) {
|
||||
// Part of the chunk is on the wire, so the stream is broken and the client has to go
|
||||
ESP_LOGW(TAG, "Cannot buffer a %zu byte chunk, closing", total);
|
||||
this->request_close_();
|
||||
} else {
|
||||
this->tail_alloc_failed_(total); // nothing on the wire, the caller retries on the stall clock
|
||||
}
|
||||
return false;
|
||||
}
|
||||
uint8_t *dst = this->tail_.get();
|
||||
std::memcpy(dst, prefix, prefix_len);
|
||||
dst += prefix_len;
|
||||
for_each_chunk_piece(
|
||||
message, message_len,
|
||||
[](void *ctx, const char *piece, size_t len) {
|
||||
auto &out = *static_cast<uint8_t **>(ctx);
|
||||
std::memcpy(out, piece, len);
|
||||
out += len;
|
||||
},
|
||||
&dst);
|
||||
this->tail_len_ = total;
|
||||
this->tail_sent_ = sent;
|
||||
return true;
|
||||
}
|
||||
|
||||
void AsyncEventSourceResponse::loop() {
|
||||
if (this->close_requested_) {
|
||||
this->process_close_();
|
||||
return;
|
||||
}
|
||||
this->refresh_lru_();
|
||||
drain_tail_();
|
||||
process_deferred_queue_();
|
||||
if (this->close_requested_)
|
||||
return;
|
||||
// One step per loop; refusals retry next pass
|
||||
this->entities_iterator_.try_advance(1);
|
||||
}
|
||||
|
||||
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wstack-usage=" // the one frame that holds the JSON arena
|
||||
#endif
|
||||
AsyncEventSourceResponse::JsonPrepareResult AsyncEventSourceResponse::prepare_json_(void *source,
|
||||
message_generator_t *generator,
|
||||
char *buffer, size_t *size_out) {
|
||||
// Log lines emitted while the generator runs are dropped for this session: on the large path they would fill
|
||||
// tail_ before it is overwritten, and on the small path they would go out ahead of the event being built.
|
||||
SendGuard guard{*this};
|
||||
json::JsonArena<json::JSON_ARENA_SIZE> arena;
|
||||
json::JsonBuilder builder(&arena);
|
||||
generator(this->web_server_, source, builder);
|
||||
*size_out = builder.serialize_to(buffer, JSON_BUF_SIZE);
|
||||
if (*size_out < JSON_BUF_SIZE) {
|
||||
return JsonPrepareResult::JSON_PREPARE_RESULT_BUFFER;
|
||||
}
|
||||
|
||||
char prefix[PREFIX_BUF_SIZE];
|
||||
const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), "state", 0, 0, true);
|
||||
|
||||
// Nothing reaches the wire until the complete document and its framing are owned by the caller.
|
||||
size_t json_len = 0;
|
||||
size_t cap = std::max<size_t>(JSON_BUF_SIZE * 2, this->tail_cap_);
|
||||
for (;;) {
|
||||
if (!this->reserve_tail_(cap)) {
|
||||
*size_out = cap;
|
||||
return JsonPrepareResult::JSON_PREPARE_RESULT_NO_MEMORY;
|
||||
}
|
||||
const size_t room = cap - prefix_len - SSE_SUFFIX_LEN;
|
||||
json_len = builder.serialize_to(reinterpret_cast<char *>(this->tail_.get()) + prefix_len, room);
|
||||
if (json_len < room) {
|
||||
break;
|
||||
}
|
||||
if (cap >= TAIL_MAX_SIZE) {
|
||||
*size_out = room;
|
||||
return JsonPrepareResult::JSON_PREPARE_RESULT_TOO_LARGE;
|
||||
}
|
||||
cap = std::min<size_t>(cap * 2, TAIL_MAX_SIZE);
|
||||
}
|
||||
|
||||
const size_t total = prefix_len + json_len + SSE_SUFFIX_LEN;
|
||||
write_chunk_header(prefix, total - CHUNK_HDR_LEN - CHUNK_END_LEN);
|
||||
uint8_t *dst = this->tail_.get();
|
||||
std::memcpy(dst, prefix, prefix_len);
|
||||
std::memcpy(dst + prefix_len + json_len, SSE_SUFFIX, SSE_SUFFIX_LEN);
|
||||
this->tail_len_ = total;
|
||||
this->tail_sent_ = 0;
|
||||
return JsonPrepareResult::JSON_PREPARE_RESULT_TAIL;
|
||||
}
|
||||
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
bool AsyncEventSourceResponse::send_json_(void *source, message_generator_t *generator) {
|
||||
if (!this->ready_to_send_()) {
|
||||
return false;
|
||||
}
|
||||
char buffer[JSON_BUF_SIZE];
|
||||
size_t size;
|
||||
const auto result = this->prepare_json_(source, generator, buffer, &size);
|
||||
switch (result) {
|
||||
case JsonPrepareResult::JSON_PREPARE_RESULT_BUFFER:
|
||||
return this->try_send_nodefer(buffer, size, "state");
|
||||
case JsonPrepareResult::JSON_PREPARE_RESULT_TAIL:
|
||||
this->drain_tail_();
|
||||
return true;
|
||||
case JsonPrepareResult::JSON_PREPARE_RESULT_NO_MEMORY: {
|
||||
SendGuard guard{*this};
|
||||
this->tail_alloc_failed_(size);
|
||||
return false;
|
||||
}
|
||||
case JsonPrepareResult::JSON_PREPARE_RESULT_TOO_LARGE: {
|
||||
SendGuard guard{*this};
|
||||
ESP_LOGW(TAG, "State event does not fit %zu bytes, dropped", size);
|
||||
this->tail_.reset();
|
||||
this->tail_cap_ = 0;
|
||||
this->send_failure_started_ms_ = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AsyncEventSourceResponse::tail_alloc_failed_(size_t cap) {
|
||||
// Same stall clock as a socket that stops draining, so a session cannot retry forever
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (this->send_failure_started_ms_ == 0) {
|
||||
this->send_failure_started_ms_ = now != 0 ? now : 1; // Reserve zero for no stall.
|
||||
ESP_LOGW(TAG, "No memory for a %zu byte chunk", cap);
|
||||
return;
|
||||
}
|
||||
if (static_cast<int32_t>(now - (this->send_failure_started_ms_ + SEND_STALL_TIMEOUT_MS)) >= 0) {
|
||||
ESP_LOGW(TAG, "Closing EventSource after %" PRIu32 " ms without memory", now - this->send_failure_started_ms_);
|
||||
this->request_close_();
|
||||
}
|
||||
}
|
||||
|
||||
bool AsyncEventSourceResponse::ready_to_send_() {
|
||||
if (this->sending_ || this->fd_.load() == 0 || this->close_requested_) {
|
||||
return false;
|
||||
}
|
||||
drain_tail_();
|
||||
return !this->close_requested_ && this->tail_len_ == 0;
|
||||
}
|
||||
|
||||
bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
|
||||
uint32_t reconnect) {
|
||||
if (!this->ready_to_send_()) {
|
||||
return false;
|
||||
}
|
||||
SendGuard guard{*this};
|
||||
|
||||
// Everything after the prefix goes out straight from the caller's buffer
|
||||
char prefix[PREFIX_BUF_SIZE];
|
||||
const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), event, id, reconnect, message != nullptr);
|
||||
if (message == nullptr && prefix_len == CHUNK_HDR_LEN) {
|
||||
return true; // Match ESPAsyncWebServer: nothing to send
|
||||
}
|
||||
if (prefix_len >= PREFIX_BUF_SIZE - 1) {
|
||||
// The appenders truncate silently, which would put a malformed event on the wire
|
||||
ESP_LOGW(TAG, "Event name too long, dropped");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Gather list: the prefix, then the data lines and their separators from the message
|
||||
struct Gather {
|
||||
struct iovec iov[MAX_SEND_IOV]; // left uninitialized on purpose
|
||||
int iovcnt{1};
|
||||
size_t total{0};
|
||||
bool fits{true};
|
||||
} g;
|
||||
g.total = prefix_len;
|
||||
for_each_chunk_piece(
|
||||
message, message_len,
|
||||
[](void *ctx, const char *piece, size_t len) {
|
||||
auto &g = *static_cast<Gather *>(ctx);
|
||||
g.total += len;
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
if (g.iovcnt == MAX_SEND_IOV) {
|
||||
g.fits = false;
|
||||
return;
|
||||
}
|
||||
g.iov[g.iovcnt++] = {const_cast<char *>(piece), len};
|
||||
},
|
||||
&g);
|
||||
// The header and the terminator are not part of the chunk length
|
||||
write_chunk_header(prefix, g.total - CHUNK_HDR_LEN - CHUNK_END_LEN);
|
||||
g.iov[0] = {prefix, prefix_len};
|
||||
|
||||
// A message with more lines than the list holds skips straight to the tail
|
||||
const ssize_t sent = g.fits ? this->send_(g.iov, g.iovcnt) : 0;
|
||||
if (sent < 0) {
|
||||
return false;
|
||||
}
|
||||
if (static_cast<size_t>(sent) == g.total) {
|
||||
this->send_failure_started_ms_ = 0; // progress, whichever stall clock was running
|
||||
return true;
|
||||
}
|
||||
// The caller's buffers do not outlive this call, so keep the chunk and continue from loop()
|
||||
return this->stash_chunk_(prefix, prefix_len, message, message_len, g.total, sent);
|
||||
}
|
||||
|
||||
void AsyncEventSourceResponse::deferrable_send_state(void *source, bool detail_all,
|
||||
message_generator_t *message_generator) {
|
||||
// Let the detail_all listing finish before bare state events, so the web UI never shows unnamed entries and
|
||||
// initial connect sends less
|
||||
if (!detail_all && !this->entities_iterator_.completed())
|
||||
return;
|
||||
|
||||
drain_tail_();
|
||||
process_deferred_queue_();
|
||||
|
||||
if (this->close_requested_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->tail_len_ != 0 || !deferred_queue_.empty()) {
|
||||
// outgoing event buffer or deferred queue still not empty which means downstream tcp send buffer full, no point
|
||||
// trying to send first
|
||||
deq_push_back_with_dedup_(source, message_generator);
|
||||
} else {
|
||||
// A send error closes the session and clears the queue; nothing is queued after that
|
||||
if (!this->send_json_(source, message_generator) && !this->close_requested_) {
|
||||
deq_push_back_with_dedup_(source, message_generator);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *content_type) {
|
||||
// Parse boundary and create reader
|
||||
|
||||
@@ -15,14 +15,21 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/web_server_base/stream_head.h"
|
||||
#ifdef USE_WEBSERVER
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/web_server/list_entities.h"
|
||||
#include "sse_chunk.h"
|
||||
#endif
|
||||
|
||||
struct iovec; // NOLINT(readability-identifier-naming) - forward decl of lwip's gather list entry
|
||||
|
||||
namespace esphome::web_server_idf {
|
||||
|
||||
using web_server_base::HttpHeader;
|
||||
using web_server_base::StallClock;
|
||||
namespace esphome {
|
||||
#ifdef USE_WEBSERVER
|
||||
namespace web_server {
|
||||
class WebServer;
|
||||
}; // namespace web_server
|
||||
#endif
|
||||
namespace web_server_idf {
|
||||
|
||||
class AsyncWebParameter {
|
||||
public:
|
||||
@@ -184,7 +191,6 @@ class AsyncWebServerRequest {
|
||||
void onDisconnect(std::function<void()> fn) { this->on_disconnect_ = std::move(fn); }
|
||||
#endif
|
||||
|
||||
/// sess_ctx is reserved for AsyncStreamResponse: the server's close_fn treats any session context as one.
|
||||
operator httpd_req_t *() const { return this->req_; }
|
||||
optional<std::string> get_header(const char *name) const;
|
||||
/// Copy a header value into buf without heap allocation.
|
||||
@@ -263,85 +269,204 @@ class AsyncWebHandler {
|
||||
virtual bool isRequestHandlerTrivial() const { return true; }
|
||||
};
|
||||
|
||||
/// A response that keeps its connection open and is written from the main loop, such as an event stream or
|
||||
/// an audio stream. Use it directly or derive from it. Call loop() and delete the object only through the type
|
||||
/// it was created as: subclasses hide loop() instead of overriding it, and the destructor is not virtual.
|
||||
///
|
||||
/// Lifecycle:
|
||||
/// 1. Httpd task, inside handleRequest(): create it and call begin(), then hand the pointer to the main loop.
|
||||
/// 2. Main loop: once is_attached() is true, write() may go through. Call loop() on every pass.
|
||||
/// 3. Main loop: delete it once safe_to_delete() is true. httpd owns the socket throughout.
|
||||
class AsyncStreamResponse {
|
||||
public:
|
||||
/// Httpd task, once. Sends the status line (for example HTTPD_200), Content-Type, headers, the default headers
|
||||
/// and body (a C string or null, sent as is), then makes this object the session context. A chunked response
|
||||
/// passes {"Transfer-Encoding", "chunked"} as its first header and frames each chunk itself.
|
||||
void begin(AsyncWebServerRequest *request, const char *status, const char *content_type,
|
||||
std::span<const HttpHeader> headers, const char *body = nullptr);
|
||||
#ifdef USE_WEBSERVER
|
||||
class AsyncEventSource;
|
||||
class AsyncEventSourceResponse;
|
||||
|
||||
/// Main loop. Non-blocking gather write. Returns the bytes accepted, which may be fewer than offered: the caller
|
||||
/// keeps the rest and continues from there. 0 means the socket is full or not ours yet; -1 means the stream failed
|
||||
/// and is closing. Refused writes for SEND_STALL_TIMEOUT_MS in a row also close it.
|
||||
ssize_t write(struct iovec *iov, int iovcnt);
|
||||
ssize_t write(const void *data, size_t len);
|
||||
/// Main loop. Shuts the connection down through httpd; safe to call more than once.
|
||||
void close();
|
||||
/// Main loop, every pass. Keeps the session fresh in httpd's LRU and retries a pending close.
|
||||
void loop();
|
||||
/// Httpd task, from the server's close_fn: no write() reaches the socket after this returns.
|
||||
static void socket_closing(httpd_handle_t hd, int fd);
|
||||
using message_generator_t = void(esphome::web_server::WebServer *, void *, json::JsonBuilder &);
|
||||
|
||||
/// httpd holds this response as the session context; begin() has sent the whole head by then.
|
||||
bool is_attached() const { return this->owns_session_(this->fd_.load()); }
|
||||
/// The client is still connected; httpd has not freed the session.
|
||||
bool is_connected() const { return this->fd_.load() != 0; }
|
||||
bool is_closing() const { return this->close_requested_; }
|
||||
/// The socket fd, for log lines only
|
||||
int log_id() const { return this->fd_.load(); }
|
||||
/// Sends are making no progress; after write() returns -1 this tells a send timeout from a send error.
|
||||
bool is_stalled() const { return this->stall_.is_running(); }
|
||||
/// The session is gone and no queued httpd work still references this object.
|
||||
bool safe_to_delete() const {
|
||||
return this->fd_.load() == 0 && !this->close_work_queued_.load(std::memory_order_acquire) &&
|
||||
!this->lru_work_queued_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
static constexpr uint32_t SEND_STALL_TIMEOUT_MS = 20000;
|
||||
/*
|
||||
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
|
||||
that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for
|
||||
the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a
|
||||
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two pointers
|
||||
per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
|
||||
because of dedup) would take up only 0.8 kB.
|
||||
*/
|
||||
struct DeferredEvent {
|
||||
friend class AsyncEventSourceResponse;
|
||||
|
||||
protected:
|
||||
void *source_;
|
||||
message_generator_t *message_generator_;
|
||||
|
||||
public:
|
||||
DeferredEvent(void *source, message_generator_t *message_generator)
|
||||
: source_(source), message_generator_(message_generator) {}
|
||||
bool operator==(const DeferredEvent &test) const {
|
||||
return (source_ == test.source_ && message_generator_ == test.message_generator_);
|
||||
}
|
||||
};
|
||||
static_assert(sizeof(DeferredEvent) == sizeof(void *) + sizeof(message_generator_t *),
|
||||
"DeferredEvent should have no padding");
|
||||
|
||||
class AsyncEventSourceResponse {
|
||||
friend class AsyncEventSource;
|
||||
|
||||
public:
|
||||
bool try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0,
|
||||
uint32_t reconnect = 0);
|
||||
// detail_all events go out while entities are still being listed; state events wait until listing is done
|
||||
void deferrable_send_state(void *source, bool detail_all, message_generator_t *message_generator);
|
||||
// The old event name argument would otherwise convert to true
|
||||
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator) = delete;
|
||||
void loop();
|
||||
|
||||
protected:
|
||||
AsyncEventSourceResponse(const AsyncWebServerRequest *request, esphome::web_server_idf::AsyncEventSource *server,
|
||||
esphome::web_server::WebServer *ws);
|
||||
|
||||
// Main-loop only: sends initial ping/config/sorting_groups, starts entity iterator.
|
||||
void start_session_main_loop_();
|
||||
|
||||
void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator);
|
||||
void process_deferred_queue_();
|
||||
// A new chunk may go out: not re-entered from a log line, session alive, tail empty
|
||||
bool ready_to_send_();
|
||||
// Non-blocking gather write. Returns bytes written, 0 on would-block or a socket that is not
|
||||
// ours, -1 after requesting the close on any other error.
|
||||
ssize_t send_(struct iovec *iov, int iovcnt);
|
||||
// Push what is left of the chunk in tail_ to the socket; owns the stall timer.
|
||||
void drain_tail_();
|
||||
// Grow tail_ to hold len bytes, kept at its high-water mark. False on OOM.
|
||||
bool reserve_tail_(size_t len);
|
||||
// Keep the whole chunk in tail_ and continue from sent; false when the tail cannot be allocated.
|
||||
bool stash_chunk_(const char *prefix, size_t prefix_len, const char *message, size_t message_len, size_t total,
|
||||
size_t sent);
|
||||
// Send a state event; JSON too large for the stack buffer is serialized into tail_ instead
|
||||
bool send_json_(void *source, message_generator_t *generator);
|
||||
enum class JsonPrepareResult : uint8_t {
|
||||
JSON_PREPARE_RESULT_BUFFER,
|
||||
JSON_PREPARE_RESULT_TAIL,
|
||||
JSON_PREPARE_RESULT_NO_MEMORY,
|
||||
JSON_PREPARE_RESULT_TOO_LARGE,
|
||||
};
|
||||
// Keep the arena out of the caller's frame so it is gone before the state send.
|
||||
// size_out: BUFFER = JSON length, NO_MEMORY = requested tail capacity, TOO_LARGE = available JSON room;
|
||||
// unused for TAIL.
|
||||
JsonPrepareResult __attribute__((noinline)) prepare_json_(void *source, message_generator_t *generator, char *buffer,
|
||||
size_t *size_out);
|
||||
// Warn once, and close the session once the stall timeout passes with no memory for the tail
|
||||
void tail_alloc_failed_(size_t cap);
|
||||
void request_close_();
|
||||
|
||||
// A log line emitted inside a send re-enters try_send_nodefer on this session; refuse it
|
||||
struct SendGuard {
|
||||
AsyncEventSourceResponse &owner;
|
||||
explicit SendGuard(AsyncEventSourceResponse &owner) : owner(owner) { owner.sending_ = true; }
|
||||
~SendGuard() { this->owner.sending_ = false; }
|
||||
};
|
||||
void process_close_();
|
||||
static void close_session_work(void *arg);
|
||||
void refresh_lru_();
|
||||
static void refresh_lru_work(void *arg);
|
||||
// httpd closes the socket before it clears the session, so this can still match a closed fd; write() also checks
|
||||
// socket_closed_
|
||||
// httpd frees a session before closing its socket, so the fd may already belong to a new client
|
||||
bool owns_session_(int fd) const { return fd != 0 && httpd_sess_get_ctx(this->hd_, fd) == this; }
|
||||
static void destroy(void *p);
|
||||
|
||||
// Deletable only after destroy() zeroed fd_ and no queued HTTPD work still references this object.
|
||||
bool safe_to_delete_() const {
|
||||
return this->fd_.load() == 0 && !this->close_work_queued_.load(std::memory_order_acquire) &&
|
||||
!this->lru_work_queued_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
static void destroy(void *p);
|
||||
AsyncEventSource *server_;
|
||||
httpd_handle_t hd_{};
|
||||
std::atomic<int> fd_{};
|
||||
// Runs while sends make no progress; a running clock keeps its start time
|
||||
StallClock stall_;
|
||||
std::vector<DeferredEvent> deferred_queue_;
|
||||
esphome::web_server::WebServer *web_server_;
|
||||
esphome::web_server::ListEntitiesIterator entities_iterator_;
|
||||
// One chunk the socket did not take whole, allocated on the first stall; the only heap use
|
||||
// on the send path
|
||||
RAMUniquePtr<uint8_t[]> tail_;
|
||||
uint32_t send_failure_started_ms_{0}; // Zero means no send stall in progress.
|
||||
uint32_t next_close_attempt_ms_{0};
|
||||
uint32_t last_lru_refresh_ms_{0};
|
||||
uint16_t tail_cap_{0};
|
||||
uint16_t tail_len_{0}; // Zero means nothing pending
|
||||
uint16_t tail_sent_{0};
|
||||
// Set on the main loop before queueing close work, cleared by the HTTPD-task callback when done.
|
||||
std::atomic<bool> close_work_queued_{false};
|
||||
std::atomic<bool> lru_work_queued_{false};
|
||||
// Guarded by the stream send mutex
|
||||
bool socket_closed_{false};
|
||||
// Main-loop only; the HTTPD task never reads or writes these flags.
|
||||
bool close_requested_{false};
|
||||
bool close_retry_warning_logged_{false};
|
||||
bool sending_{false};
|
||||
// The longest multi line log message in the tree (a climate dump_config) has 22 lines; a
|
||||
// longer one goes through the tail
|
||||
static constexpr size_t MAX_SEND_LINES = 22;
|
||||
static constexpr size_t MAX_SEND_IOV = 1 + 2 * MAX_SEND_LINES;
|
||||
// Chunk header, retry/id/event lines and the first "data: "
|
||||
static constexpr size_t PREFIX_BUF_SIZE = 128;
|
||||
|
||||
// Stack buffer for a state event's JSON; a larger document is serialized into the tail
|
||||
static constexpr size_t JSON_BUF_SIZE = 1024;
|
||||
// Same ceiling JsonBuilder::serialize() applies (max_heap_size in json_util.cpp); a larger
|
||||
// document is dropped before anything is on the wire
|
||||
static constexpr size_t JSON_MAX_SIZE = 5120;
|
||||
// Most RAM a stalled session keeps: the largest state document plus any accepted framing
|
||||
static constexpr size_t TAIL_MAX_SIZE = JSON_MAX_SIZE + PREFIX_BUF_SIZE + SSE_SUFFIX_LEN;
|
||||
static constexpr uint32_t SEND_STALL_TIMEOUT_MS = 20000;
|
||||
static constexpr uint32_t CLOSE_RETRY_INTERVAL_MS = 250;
|
||||
static constexpr uint32_t CLOSE_CONFIRM_INTERVAL_MS = 1000;
|
||||
static constexpr uint32_t LRU_REFRESH_INTERVAL_MS = 1000;
|
||||
};
|
||||
|
||||
using AsyncEventSourceClient = AsyncEventSourceResponse;
|
||||
|
||||
class AsyncEventSource : public AsyncWebHandler {
|
||||
friend class AsyncEventSourceResponse;
|
||||
|
||||
public:
|
||||
AsyncEventSource(StringRef url, esphome::web_server::WebServer *ws) : url_(url), web_server_(ws) {}
|
||||
~AsyncEventSource() override;
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
bool canHandle(AsyncWebServerRequest *request) const override {
|
||||
if (request->method() != HTTP_GET)
|
||||
return false;
|
||||
char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
|
||||
return request->url_to(url_buf) == this->url_;
|
||||
}
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
void handleRequest(AsyncWebServerRequest *request) override;
|
||||
void try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0,
|
||||
uint32_t reconnect = 0);
|
||||
void deferrable_send_state(void *source, bool detail_all, message_generator_t *message_generator);
|
||||
// The old event name argument would otherwise convert to true
|
||||
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator) = delete;
|
||||
/// Returns true if there are sessions remaining (including pending cleanup).
|
||||
bool loop();
|
||||
bool empty() { return this->count() == 0; }
|
||||
|
||||
size_t count() const { return this->sessions_.size(); }
|
||||
|
||||
protected:
|
||||
// Cold path: move sessions from pending_sessions_ into sessions_ and greet each one.
|
||||
void __attribute__((noinline, cold)) adopt_pending_sessions_main_loop_();
|
||||
|
||||
StringRef url_; // Must outlive this object (string literal)
|
||||
// Main-loop only. Vector: SSE sessions are 1-5 connections, linear search beats set.
|
||||
std::vector<AsyncEventSourceResponse *> sessions_;
|
||||
// Httpd-task intake; guarded by pending_mutex_, gated by has_pending_sessions_.
|
||||
std::vector<AsyncEventSourceResponse *> pending_sessions_;
|
||||
Mutex pending_mutex_;
|
||||
esphome::web_server::WebServer *web_server_;
|
||||
std::atomic<bool> has_pending_sessions_{false};
|
||||
};
|
||||
#endif // USE_WEBSERVER
|
||||
|
||||
struct HttpHeader {
|
||||
const char *name;
|
||||
const char *value;
|
||||
};
|
||||
|
||||
class DefaultHeaders {
|
||||
friend class AsyncWebServerRequest;
|
||||
friend class AsyncStreamResponse;
|
||||
friend class AsyncWebServerResponse;
|
||||
#ifdef USE_WEBSERVER
|
||||
friend class AsyncEventSourceResponse;
|
||||
#endif
|
||||
static_assert(WEB_SERVER_DEFAULT_HEADERS_COUNT <= 8, "AsyncWebServerResponse::replaced_defaults_ holds 8 bits");
|
||||
|
||||
public:
|
||||
@@ -359,7 +484,8 @@ class DefaultHeaders {
|
||||
void apply_(httpd_req_t *req, uint8_t skip_mask = 0) const;
|
||||
};
|
||||
|
||||
} // namespace esphome::web_server_idf
|
||||
} // namespace web_server_idf
|
||||
} // namespace esphome
|
||||
|
||||
using namespace esphome::web_server_idf; // NOLINT(google-global-names-in-headers)
|
||||
|
||||
|
||||
+25
-3
@@ -151,6 +151,7 @@
|
||||
#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
|
||||
@@ -758,16 +759,26 @@
|
||||
#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_ESP32H21) || defined(USE_ESP32_VARIANT_ESP32H4) || defined(USE_ESP32_VARIANT_ESP32P4) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32S31)
|
||||
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
|
||||
|
||||
@@ -781,6 +792,9 @@
|
||||
#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
|
||||
@@ -826,6 +840,8 @@
|
||||
#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
|
||||
@@ -856,6 +872,9 @@
|
||||
#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
|
||||
@@ -910,8 +929,11 @@
|
||||
#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
|
||||
|
||||
@@ -68,38 +68,6 @@ 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,
|
||||
@@ -168,23 +136,108 @@ def test_log_v2_only_on_native_toolchain(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "usb_cdc"),
|
||||
("config", "port", "name", "zephyr_cdc_loop"),
|
||||
[
|
||||
pytest.param("test_logger_rp2_usb_cdc.yaml", True, id="usb_cdc"),
|
||||
pytest.param("test_logger_rp2_uart0.yaml", False, id="uart0"),
|
||||
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",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_rp2_usb_cdc_selection_define(
|
||||
def test_uart_selection_defines(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
config: str,
|
||||
usb_cdc: bool,
|
||||
port: str,
|
||||
name: str,
|
||||
zephyr_cdc_loop: bool,
|
||||
) -> None:
|
||||
"""The USB CDC crash replay only compiles in when the logger uses USB CDC."""
|
||||
"""The chosen port is fixed at build time: one flag, its enum value and its dump_config name."""
|
||||
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 (
|
||||
"USE_LOGGER_UART_SELECTION_USB_CDC" in {d.name for d in CORE.defines}
|
||||
) is usb_cdc
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_CDC
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
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;"
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
logger:
|
||||
baud_rate: 0
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
nrf52:
|
||||
board: adafruit_itsybitsy_nrf52840
|
||||
bootloader: adafruit_nrf52_sd140_v6
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_CDC
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
nrf52:
|
||||
board: adafruit_itsybitsy_nrf52840
|
||||
bootloader: adafruit_nrf52_sd140_v6
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_CDC
|
||||
baud_rate: 0
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
logger:
|
||||
hardware_uart: UART0_SWAP
|
||||
@@ -0,0 +1,54 @@
|
||||
#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,300 +0,0 @@
|
||||
#ifdef USE_HOST
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <sys/uio.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/web_server/sse_chunk.h"
|
||||
#include "esphome/components/web_server_base/stream_head.h"
|
||||
|
||||
namespace esphome::web_server::testing {
|
||||
|
||||
using namespace web_server_base; // NOLINT(google-build-using-namespace)
|
||||
|
||||
// Collects every batch write_stream_head() sends
|
||||
struct Capture {
|
||||
std::string bytes;
|
||||
std::vector<int> batch_sizes;
|
||||
int attempts{0};
|
||||
int fail_after_batches{-1};
|
||||
|
||||
bool operator()(struct iovec *iov, int count) {
|
||||
this->attempts++;
|
||||
if (static_cast<int>(this->batch_sizes.size()) == this->fail_after_batches) {
|
||||
return false;
|
||||
}
|
||||
this->batch_sizes.push_back(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
this->bytes.append(static_cast<const char *>(iov[i].iov_base), iov[i].iov_len);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
static std::string build_head(Capture &cap, const char *status, const char *content_type,
|
||||
std::span<const HttpHeader> headers, std::span<const HttpHeader> defaults,
|
||||
const char *body) {
|
||||
EXPECT_TRUE(write_stream_head<struct iovec>(cap, status, content_type, headers, defaults, body));
|
||||
return cap.bytes;
|
||||
}
|
||||
|
||||
// What httpd_resp_send_chunk() put on the wire for its first chunk (esp_http_server/src/httpd_txrx.c in
|
||||
// ESP-IDF 5.5): the chunked head with Transfer-Encoding, each httpd_resp_set_hdr() header, the blank line,
|
||||
// then "%lx\r\n", the chunk data and "\r\n".
|
||||
static std::string reference_send_chunk(const char *status, const char *content_type,
|
||||
const std::vector<HttpHeader> &resp_hdrs, const char *buf, size_t buf_len) {
|
||||
char head[256];
|
||||
std::string out(
|
||||
head, snprintf(head, sizeof(head), "HTTP/1.1 %s\r\nContent-Type: %s\r\nTransfer-Encoding: chunked\r\n", status,
|
||||
content_type));
|
||||
for (const auto &hdr : resp_hdrs) {
|
||||
out.append(hdr.name).append(": ").append(hdr.value).append("\r\n");
|
||||
}
|
||||
out.append("\r\n");
|
||||
char len_str[10];
|
||||
out.append(len_str, snprintf(len_str, sizeof(len_str), "%lx\r\n", static_cast<long>(buf_len)));
|
||||
out.append(buf, buf_len);
|
||||
out.append("\r\n");
|
||||
return out;
|
||||
}
|
||||
|
||||
static constexpr HttpHeader DEFAULTS[] = {
|
||||
{"Access-Control-Allow-Origin", "*"},
|
||||
{"Access-Control-Allow-Private-Network", "true"},
|
||||
};
|
||||
|
||||
TEST(StreamHead, SseHeadMatchesHttpdRespSendChunk) {
|
||||
// The old event source set Cache-Control and Connection, then the default headers, and sent "\r\n" as a chunk
|
||||
const std::string expected = reference_send_chunk(
|
||||
"200 OK", "text/event-stream",
|
||||
{{"Cache-Control", "no-cache"}, {"Connection", "keep-alive"}, DEFAULTS[0], DEFAULTS[1]}, "\r\n", 2);
|
||||
Capture cap;
|
||||
EXPECT_EQ(build_head(cap, SSE_STATUS, SSE_CONTENT_TYPE, SSE_HEADERS, DEFAULTS, SSE_FIRST_CHUNK), expected);
|
||||
}
|
||||
|
||||
TEST(StreamHead, HeaderReplacesDefaultOfSameName) {
|
||||
// A default the caller also sets, in any case, goes out once with the caller's value
|
||||
static constexpr HttpHeader HEADERS[] = {
|
||||
{"access-control-allow-origin", "http://example.com"},
|
||||
};
|
||||
Capture cap;
|
||||
EXPECT_EQ(build_head(cap, "200 OK", "text/plain", HEADERS, DEFAULTS, nullptr),
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\naccess-control-allow-origin: http://example.com"
|
||||
"\r\nAccess-Control-Allow-Private-Network: true\r\n\r\n");
|
||||
}
|
||||
|
||||
TEST(StreamHead, RawHeadWithCustomHeadersAndNoBody) {
|
||||
static constexpr HttpHeader HEADERS[] = {
|
||||
{"Cache-Control", "no-store"},
|
||||
{"X-Stream", "audio"},
|
||||
};
|
||||
Capture cap;
|
||||
EXPECT_EQ(build_head(cap, "200 OK", "audio/wav", HEADERS, {}, nullptr), "HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: audio/wav\r\n"
|
||||
"Cache-Control: no-store\r\n"
|
||||
"X-Stream: audio\r\n"
|
||||
"\r\n");
|
||||
}
|
||||
|
||||
TEST(StreamHead, BareHead) {
|
||||
Capture cap;
|
||||
EXPECT_EQ(build_head(cap, "204 No Content", "text/plain", {}, {}, nullptr),
|
||||
"HTTP/1.1 204 No Content\r\nContent-Type: text/plain\r\n\r\n");
|
||||
EXPECT_EQ(cap.batch_sizes, std::vector<int>{5});
|
||||
}
|
||||
|
||||
TEST(StreamHead, ManyHeadersSplitIntoBatchesWithSameBytes) {
|
||||
// 4 + 12 * 4 + 2 = 54 pieces, more than two batches hold
|
||||
static constexpr HttpHeader HEADERS[] = {
|
||||
{"X-Header-0", "value-0"}, {"X-Header-1", "value-1"}, {"X-Header-2", "value-2"}, {"X-Header-3", "value-3"},
|
||||
{"X-Header-4", "value-4"}, {"X-Header-5", "value-5"}, {"X-Header-6", "value-6"}, {"X-Header-7", "value-7"},
|
||||
{"X-Header-8", "value-8"}, {"X-Header-9", "value-9"},
|
||||
};
|
||||
const std::span<const HttpHeader> headers{HEADERS};
|
||||
|
||||
Capture batched;
|
||||
const std::string bytes = build_head(batched, "200 OK", "text/plain", headers, DEFAULTS, "body");
|
||||
EXPECT_EQ(batched.batch_sizes, (std::vector<int>{STREAM_HEAD_MAX_IOV, STREAM_HEAD_MAX_IOV, 6}));
|
||||
|
||||
std::string expected = "HTTP/1.1 200 OK\r\nContent-Type: text/plain";
|
||||
const auto append_headers = [&expected](std::span<const HttpHeader> list) {
|
||||
for (const auto &h : list) {
|
||||
expected.append("\r\n").append(h.name).append(": ").append(h.value);
|
||||
}
|
||||
};
|
||||
append_headers(headers);
|
||||
append_headers(DEFAULTS);
|
||||
expected.append("\r\n\r\nbody");
|
||||
EXPECT_EQ(bytes, expected);
|
||||
}
|
||||
|
||||
TEST(StreamHead, BatchBoundary) {
|
||||
// 4 + 4 * 4 + 1 + 1 = 22 pieces fit one batch; a fifth header makes 4 + 5 * 4 = 24 before the blank line
|
||||
static constexpr HttpHeader HEADERS[] = {{"A", "1"}, {"B", "2"}, {"C", "3"}, {"D", "4"}, {"E", "5"}};
|
||||
Capture four;
|
||||
build_head(four, "200 OK", "text/plain", std::span(HEADERS, 4), {}, "x");
|
||||
EXPECT_EQ(four.batch_sizes, std::vector<int>{22});
|
||||
EXPECT_EQ(stream_head_iov_count(4), 22u);
|
||||
Capture five;
|
||||
const std::string bytes = build_head(five, "200 OK", "text/plain", HEADERS, {}, "x");
|
||||
EXPECT_EQ(five.batch_sizes, (std::vector<int>{STREAM_HEAD_MAX_IOV, 2}));
|
||||
EXPECT_EQ(bytes, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nA: 1\r\nB: 2\r\nC: 3\r\nD: 4\r\nE: 5\r\n\r\nx");
|
||||
}
|
||||
|
||||
TEST(StreamHead, FailedSendSkipsLaterBatches) {
|
||||
// Three batches; the second fails, so the third is never sent
|
||||
std::vector<HttpHeader> headers(12, HttpHeader{"X", "y"});
|
||||
Capture full;
|
||||
build_head(full, "200 OK", "text/plain", headers, {}, nullptr);
|
||||
ASSERT_EQ(full.batch_sizes.size(), 3u);
|
||||
Capture cap;
|
||||
cap.fail_after_batches = 1;
|
||||
EXPECT_FALSE(write_stream_head<struct iovec>(cap, "200 OK", "text/plain", headers, {}, nullptr));
|
||||
EXPECT_EQ(cap.attempts, 2);
|
||||
EXPECT_EQ(cap.batch_sizes.size(), 1u);
|
||||
}
|
||||
|
||||
// Three elements of 3, 4 and 5 bytes
|
||||
struct IovFixture {
|
||||
char a[4] = "abc";
|
||||
char b[5] = "defg";
|
||||
char c[6] = "hijkl";
|
||||
struct iovec iov[3] = {{a, 3}, {b, 4}, {c, 5}};
|
||||
struct iovec *cur = iov;
|
||||
int count = 3;
|
||||
|
||||
std::string rest() const {
|
||||
std::string out;
|
||||
for (int i = 0; i < this->count; i++) {
|
||||
out.append(static_cast<const char *>(this->cur[i].iov_base), this->cur[i].iov_len);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(AdvanceIovecs, ZeroLengthWriteChangesNothing) {
|
||||
IovFixture f;
|
||||
advance_iovecs(f.cur, f.count, 0);
|
||||
EXPECT_EQ(f.cur, f.iov);
|
||||
EXPECT_EQ(f.count, 3);
|
||||
EXPECT_EQ(f.rest(), "abcdefghijkl");
|
||||
}
|
||||
|
||||
TEST(AdvanceIovecs, PartialInsideFirstElement) {
|
||||
IovFixture f;
|
||||
advance_iovecs(f.cur, f.count, 2);
|
||||
EXPECT_EQ(f.cur, f.iov);
|
||||
EXPECT_EQ(f.count, 3);
|
||||
EXPECT_EQ(f.cur->iov_len, 1u);
|
||||
EXPECT_EQ(f.rest(), "cdefghijkl");
|
||||
}
|
||||
|
||||
TEST(AdvanceIovecs, ExactlyOnElementBoundary) {
|
||||
IovFixture f;
|
||||
advance_iovecs(f.cur, f.count, 3);
|
||||
EXPECT_EQ(f.cur, &f.iov[1]);
|
||||
EXPECT_EQ(f.count, 2);
|
||||
EXPECT_EQ(f.rest(), "defghijkl");
|
||||
}
|
||||
|
||||
TEST(AdvanceIovecs, AcrossElementBoundaries) {
|
||||
IovFixture f;
|
||||
advance_iovecs(f.cur, f.count, 9);
|
||||
EXPECT_EQ(f.cur, &f.iov[2]);
|
||||
EXPECT_EQ(f.count, 1);
|
||||
EXPECT_EQ(f.rest(), "jkl");
|
||||
}
|
||||
|
||||
TEST(AdvanceIovecs, RepeatedShortWrites) {
|
||||
IovFixture f;
|
||||
std::string sent;
|
||||
while (f.count > 0) {
|
||||
const std::string before = f.rest();
|
||||
advance_iovecs(f.cur, f.count, 2);
|
||||
sent += before.substr(0, before.size() - f.rest().size());
|
||||
}
|
||||
EXPECT_EQ(sent, "abcdefghijkl");
|
||||
}
|
||||
|
||||
TEST(AdvanceIovecs, EverythingWritten) {
|
||||
IovFixture f;
|
||||
advance_iovecs(f.cur, f.count, 12);
|
||||
EXPECT_EQ(f.count, 0);
|
||||
EXPECT_EQ(f.rest(), "");
|
||||
}
|
||||
|
||||
TEST(AdvanceIovecs, SkipsZeroLengthElements) {
|
||||
char a[] = "ab";
|
||||
char c[] = "cd";
|
||||
struct iovec iov[3] = {{a, 2}, {nullptr, 0}, {c, 2}};
|
||||
struct iovec *cur = iov;
|
||||
int count = 3;
|
||||
advance_iovecs(cur, count, 2);
|
||||
EXPECT_EQ(cur, &iov[2]);
|
||||
EXPECT_EQ(count, 1);
|
||||
}
|
||||
|
||||
static constexpr uint32_t TIMEOUT = 20000;
|
||||
|
||||
TEST(StallClock, NotStarted) {
|
||||
StallClock clock;
|
||||
EXPECT_FALSE(clock.is_running());
|
||||
EXPECT_FALSE(clock.is_expired(0, TIMEOUT));
|
||||
EXPECT_FALSE(clock.is_expired(UINT32_MAX, TIMEOUT));
|
||||
}
|
||||
|
||||
TEST(StallClock, ExpiresAtExactlyTheTimeout) {
|
||||
StallClock clock;
|
||||
clock.start(1000);
|
||||
EXPECT_TRUE(clock.is_running());
|
||||
EXPECT_FALSE(clock.is_expired(1000, TIMEOUT));
|
||||
EXPECT_FALSE(clock.is_expired(1000 + TIMEOUT - 1, TIMEOUT));
|
||||
EXPECT_TRUE(clock.is_expired(1000 + TIMEOUT, TIMEOUT));
|
||||
EXPECT_TRUE(clock.is_expired(1000 + TIMEOUT + 1, TIMEOUT));
|
||||
EXPECT_EQ(clock.elapsed(1000 + TIMEOUT), TIMEOUT);
|
||||
}
|
||||
|
||||
TEST(StallClock, RunningClockKeepsItsStart) {
|
||||
StallClock clock;
|
||||
clock.start(1000);
|
||||
clock.start(5000);
|
||||
EXPECT_TRUE(clock.is_expired(1000 + TIMEOUT, TIMEOUT));
|
||||
}
|
||||
|
||||
TEST(StallClock, Cleared) {
|
||||
StallClock clock;
|
||||
clock.start(1000);
|
||||
clock.clear();
|
||||
EXPECT_FALSE(clock.is_running());
|
||||
EXPECT_FALSE(clock.is_expired(1000 + TIMEOUT, TIMEOUT));
|
||||
clock.start(50000);
|
||||
EXPECT_FALSE(clock.is_expired(50000 + TIMEOUT - 1, TIMEOUT));
|
||||
EXPECT_TRUE(clock.is_expired(50000 + TIMEOUT, TIMEOUT));
|
||||
}
|
||||
|
||||
TEST(StallClock, StartAtZeroStillRuns) {
|
||||
StallClock clock;
|
||||
clock.start(0);
|
||||
EXPECT_TRUE(clock.is_running());
|
||||
// Recorded as 1, so it expires one millisecond late
|
||||
EXPECT_FALSE(clock.is_expired(TIMEOUT, TIMEOUT));
|
||||
EXPECT_TRUE(clock.is_expired(TIMEOUT + 1, TIMEOUT));
|
||||
}
|
||||
|
||||
TEST(StallClock, AcrossMillisWraparound) {
|
||||
StallClock clock;
|
||||
const uint32_t start = UINT32_MAX - 5000;
|
||||
clock.start(start);
|
||||
EXPECT_FALSE(clock.is_expired(UINT32_MAX, TIMEOUT));
|
||||
EXPECT_FALSE(clock.is_expired(start + TIMEOUT - 1, TIMEOUT)); // wrapped to 14998
|
||||
EXPECT_TRUE(clock.is_expired(start + TIMEOUT, TIMEOUT));
|
||||
EXPECT_EQ(clock.elapsed(start + TIMEOUT), TIMEOUT);
|
||||
}
|
||||
|
||||
} // namespace esphome::web_server::testing
|
||||
|
||||
#endif // USE_HOST
|
||||
+3
-3
@@ -6,9 +6,9 @@
|
||||
#include <random>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/components/web_server/sse_chunk.h"
|
||||
#include "esphome/components/web_server_idf/sse_chunk.h"
|
||||
|
||||
namespace esphome::web_server::testing {
|
||||
namespace esphome::web_server_idf::testing {
|
||||
|
||||
// The chunk as try_send_nodefer lays it out: header, fields, first "data: ", then the pieces
|
||||
static std::string build_chunk(const char *message, size_t message_len, const char *event, uint32_t id,
|
||||
@@ -128,4 +128,4 @@ TEST(SseChunk, MatchesReferenceOnRandomMessages) {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::web_server::testing
|
||||
} // namespace esphome::web_server_idf::testing
|
||||
@@ -17,7 +17,6 @@ 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
|
||||
|
||||
Reference in new issue
Block a user