Compare commits

...
Author SHA1 Message Date
J. Nick Koston 1ddabb7c8f Merge remote-tracking branch 'origin/logger-console-api' into HEAD 2026-11-14 08:40:21 -06:00
J. Nick Koston b310c985e0 Merge remote-tracking branch 'origin/logger-simplify' into HEAD 2026-11-14 08:39:56 -06:00
J. Nick Koston 7bc87b9b00 Merge remote-tracking branch 'origin/dev' into HEAD 2026-11-14 08:36:37 -06:00
J. Nick Koston 5623d18036 Merge remote-tracking branch 'origin/logger-console-api' into improv-serial-simplify 2026-11-12 13:36:08 -06:00
J. Nick Koston a5c1b4f532 Merge remote-tracking branch 'origin/logger-simplify' into logger-console-api 2026-11-12 13:35:53 -06:00
J. Nick Koston 8fc6fcf724 Merge remote-tracking branch 'origin/dev' into logger-simplify
# Conflicts:
#	tests/component_tests/logger/test_logger.py
2026-11-12 13:35:46 -06:00
J. Nick Koston c31389c4b6 Check the error frame for a bad checksum 2026-11-12 09:52:34 -06:00
J. Nick Koston 55fc010825 Test that nRF52 Improv requests no console 2026-11-12 09:00:50 -06:00
J. Nick Koston dc74f49433 Merge remote-tracking branch 'origin/logger-console-api' into improv-serial-simplify 2026-11-12 09:00:18 -06:00
J. Nick Koston 22a2efd8fd Compile init_uart only for UART selections 2026-11-12 09:00:02 -06:00
J. Nick Koston 32b46b1997 Merge remote-tracking branch 'origin/logger-console-api' into improv-serial-simplify 2026-11-12 08:22:06 -06:00
J. Nick Koston e0354ae794 Merge remote-tracking branch 'origin/logger-simplify' into logger-console-api 2026-11-12 08:21:49 -06:00
J. Nick Koston f9299c1c58 Merge remote-tracking branch 'origin/dev' into logger-simplify
# Conflicts:
#	esphome/components/logger/task_log_buffer_libretiny.cpp
2026-11-12 08:21:44 -06:00
J. Nick Koston 10396da421 Merge remote-tracking branch 'origin/logger-console-api' into improv-serial-simplify 2026-11-12 07:43:16 -06:00
J. Nick Koston 09c40438e2 Merge remote-tracking branch 'origin/logger-simplify' into logger-console-api 2026-11-12 07:43:13 -06:00
J. Nick Koston 63df68acc4 Read defines.h before the test's platform define 2026-11-12 07:43:09 -06:00
J. Nick Koston 07c7038d9a Test the console transport selection, the full frame buffer and parser recovery 2026-11-12 07:18:26 -06:00
J. Nick Koston ac6061e090 Merge remote-tracking branch 'origin/logger-console-api' into improv-serial-simplify 2026-11-12 07:14:08 -06:00
J. Nick Koston 8936e4f54b Assert the port name and selection per config, define them beside each port in defines.h 2026-11-12 07:13:56 -06:00
J. Nick Koston 1c1cf21b16 Merge remote-tracking branch 'origin/logger-simplify' into logger-console-api 2026-11-12 07:13:10 -06:00
J. Nick Koston 71a0b9cfca Test the USB selection defines, the listener slot count and the task log message helpers 2026-11-12 07:12:45 -06:00
J. Nick Koston 0444e6b3ad Merge remote-tracking branch 'origin/logger-console-api' into improv-serial-simplify 2026-11-12 06:10:56 -06:00
J. Nick Koston ebc4dd2cc9 Keep the hardware_uart selection map for device-builder, check the USB CDC read count 2026-11-12 06:09:59 -06:00
J. Nick Koston 3983021d2c Use the improv library types, one frame buffer, and keep nRF52 builds compiling 2026-11-12 01:28:20 -06:00
J. Nick Koston 4f1ffefcd4 Merge branch 'logger-console-api' into improv-serial-simplify 2026-11-12 01:23:53 -06:00
J. Nick Koston 8f705e7cdb Name the console UART and its selection with one define each 2026-11-12 01:22:29 -06:00
J. Nick Koston 97440f78f6 Drop a comment that restates the code 2026-11-12 00:59:36 -06:00
J. Nick Koston c51834674f [improv_serial] Use the logger console API and drop RAM held between sessions 2026-11-12 00:59:24 -06:00
J. Nick Koston c2067f4452 [logger] Fix the console UART at build time and add a console read/write API 2026-11-12 00:59:19 -06:00
J. Nick Koston e1b38f5171 Copy the va_list in each caller, keep the plain CORE.data dict, assert the UART numbering 2026-11-12 00:56:16 -06:00
J. Nick Koston ef10ea99b9 [logger] Share the per-platform copies and drop dead code 2026-11-12 00:41:27 -06:00
44 changed files with 860 additions and 809 deletions

No files matched your search

+18 -1
View File
@@ -4,12 +4,29 @@
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::improv_base {
#ifdef USE_IMPROV_NEXT_URL
#if defined(USE_IMPROV_NEXT_URL) || defined(USE_WEBSERVER)
ESPHOME_LOG_TAG(TAG, "improv_base");
#endif
#ifdef USE_WEBSERVER
void ImprovBase::add_webserver_url_(improv::RpcResponseBuilder &builder, const network::IPAddress &ip) {
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
ip.str_to(ip_buf);
// "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54
char webserver_url[7 + network::IP_ADDRESS_BUFFER_SIZE + 1 + 5 + 1];
size_t len = buf_append_printf(webserver_url, sizeof(webserver_url), 0, "http://%s:%u", ip_buf, USE_WEBSERVER_PORT);
if (!builder.add_string(webserver_url, len)) {
ESP_LOGW(TAG, "Response full; URL dropped");
}
}
#endif
#ifdef USE_IMPROV_NEXT_URL
static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}";
static constexpr size_t DEVICE_NAME_PLACEHOLDER_LEN = sizeof(DEVICE_NAME_PLACEHOLDER) - 1;
+8 -1
View File
@@ -3,9 +3,12 @@
#include <cstddef>
#include "esphome/core/defines.h"
#ifdef USE_IMPROV_NEXT_URL
#if defined(USE_IMPROV_NEXT_URL) || defined(USE_WEBSERVER)
#include <improv.h>
#endif
#ifdef USE_WEBSERVER
#include "esphome/components/network/ip_address.h"
#endif
namespace esphome::improv_base {
@@ -23,6 +26,10 @@ class ImprovBase {
void add_next_url_(improv::RpcResponseBuilder &builder, size_t max_len);
const char *next_url_{nullptr};
#endif
#ifdef USE_WEBSERVER
/// Append "http://<ip>:<port>" to the response, warning if it does not fit.
void add_webserver_url_(improv::RpcResponseBuilder &builder, const network::IPAddress &ip);
#endif
};
} // namespace esphome::improv_base
@@ -457,15 +457,7 @@ void ImprovBLEComponent::check_wifi_connection_() {
#ifdef USE_WEBSERVER
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
if (ip.is_ip4()) {
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
ip.str_to(ip_buf);
// "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54
char webserver_url[7 + network::IP_ADDRESS_BUFFER_SIZE + 1 + 5 + 1];
size_t len =
buf_append_printf(webserver_url, sizeof(webserver_url), 0, "http://%s:%u", ip_buf, USE_WEBSERVER_PORT);
if (!builder.add_string(webserver_url, len)) {
ESP_LOGW(TAG, "Response full; URL dropped");
}
this->add_webserver_url_(builder, ip);
break;
}
}
+5 -2
View File
@@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import improv_base, uart
from esphome.components.esp32 import VARIANT_ESP32S3, get_esp32_variant
from esphome.components.logger import DOMAIN as LOGGER_DOMAIN, USB_CDC
from esphome.components.logger import DOMAIN as LOGGER_DOMAIN, USB_CDC, request_console
import esphome.config_validation as cv
from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_UART_ID
from esphome.core import CORE
@@ -34,7 +34,7 @@ CONFIG_SCHEMA = (
_UART_FINAL_VALIDATE = uart.final_validate_device_schema(
"improv_serial", require_tx=True, require_rx=True
DOMAIN, require_tx=True, require_rx=True
)
@@ -70,3 +70,6 @@ async def to_code(config: ConfigType) -> None:
if (uart_id := config.get(CONF_UART_ID)) is not None:
await cg.add_variable(var.set_uart, uart_id)
cg.add_define("USE_IMPROV_SERIAL_UART")
# The Zephyr logger has no console, so Improv there stays a no-op
elif CORE.is_esp32 or CORE.is_esp8266 or CORE.is_rp2 or CORE.is_libretiny:
request_console()
@@ -20,26 +20,36 @@ ESPHOME_LOG_TAG(TAG, "improv_serial");
static constexpr uint32_t WIFI_CONNECT_TIMEOUT_ID = 0;
static constexpr uint8_t FRAME_PREFIX[] PROGMEM = {'I', 'M', 'P', 'R', 'O', 'V', improv::IMPROV_SERIAL_VERSION};
static_assert(sizeof(FRAME_PREFIX) + 2 == FRAME_HEADER_SIZE, "type and length follow the prefix");
#if defined(USE_NETWORK_PRIMARY_INTERFACE_WIFI) && defined(USE_WIFI)
static constexpr bool WIFI_URL_FIRST = true;
#else
static constexpr bool WIFI_URL_FIRST = false;
#endif
/// Fill the frame header and return its checksum.
static uint8_t write_header(uint8_t *frame, improv::ImprovSerialType type, uint8_t length) {
progmem_memcpy(frame, FRAME_PREFIX, sizeof(FRAME_PREFIX));
frame[FRAME_HEADER_SIZE - 2] = type;
frame[FRAME_HEADER_SIZE - 1] = length;
return sum_bytes(frame, FRAME_HEADER_SIZE);
}
void ImprovSerialComponent::setup() {
global_improv_serial_component = this;
#ifdef USE_IMPROV_SERIAL_UART
// Transport is a dedicated UART bus set via set_uart() in generated code
#elif defined(USE_ESP32)
this->uart_num_ = logger::global_logger->get_uart_num();
this->uart_selection_ = logger::global_logger->get_uart();
#elif defined(USE_ARDUINO)
this->hw_serial_ = logger::global_logger->get_hw_serial();
#endif
// The Improv state machine tracks Wi-Fi provisioning only. General device
// connectivity (e.g. Ethernet) is reported separately via GET_NETWORK_STATE.
#ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->has_sta()) {
auto *wifi = wifi::global_wifi_component;
if (wifi != nullptr && wifi->has_sta()) {
this->state_ = improv::STATE_PROVISIONED;
} else if (wifi::global_wifi_component != nullptr && !wifi::global_wifi_component->is_disabled()) {
} else if (wifi != nullptr && !wifi->is_disabled()) {
// Respect Wi-Fi's disabled state; forcing a scan while disabled throws
// the wifi component into an invalid state from which it cannot recover.
wifi::global_wifi_component->start_scanning();
wifi->start_scanning();
}
#endif
}
@@ -48,33 +58,30 @@ void ImprovSerialComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
if (this->last_read_byte_ && (now - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
this->last_read_byte_ = 0;
this->rx_buffer_.clear();
this->reset_rx_buffer_();
ESP_LOGV(TAG, "Timeout");
}
while (true) {
auto byte = this->read_byte_();
if (!byte.has_value())
break;
if (this->parse_improv_serial_byte_(byte.value())) {
uint8_t byte;
while (this->read_byte_(byte)) {
if (this->parse_improv_serial_byte_(byte)) {
this->last_read_byte_ = now;
} else {
this->last_read_byte_ = 0;
this->rx_buffer_.clear();
this->reset_rx_buffer_();
}
}
#ifdef USE_WIFI
if (this->state_ == improv::STATE_PROVISIONING && wifi::global_wifi_component != nullptr &&
wifi::global_wifi_component->is_connected()) {
auto *wifi = wifi::global_wifi_component;
if (this->state_ == improv::STATE_PROVISIONING && wifi != nullptr && wifi->is_connected()) {
// Being connected is not enough: re-provisioning a device that is already online leaves the
// prior network up until it drops, so check that the joined network is the requested one
// before reporting success. Same test as the wifi.connect action.
char ssid_buf[wifi::SSID_BUFFER_SIZE];
if (strcmp(wifi::global_wifi_component->wifi_ssid_to(ssid_buf), this->connecting_sta_.get_ssid().c_str()) == 0) {
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
this->connecting_sta_.get_password());
this->connecting_sta_ = {};
if (strcmp(wifi->wifi_ssid_to(ssid_buf), this->connecting_sta_->get_ssid().c_str()) == 0) {
wifi->save_wifi_sta(this->connecting_sta_->get_ssid(), this->connecting_sta_->get_password());
this->connecting_sta_.reset();
this->cancel_timeout(WIFI_CONNECT_TIMEOUT_ID);
this->set_state_(improv::STATE_PROVISIONED);
@@ -86,72 +93,6 @@ void ImprovSerialComponent::loop() {
void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); }
void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) {
// First, set length field
this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1;
const bool there_is_data = data != nullptr && size > 0;
// If there_is_data, checksum must not include our optional data byte
const uint8_t header_checksum_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE - 2;
// Only transmit the full buffer length if there is no data (only state/error byte is provided in this case)
const uint8_t header_tx_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE;
uint8_t checksum = sum_bytes(this->tx_header_, header_checksum_len);
if (there_is_data) {
checksum = sum_bytes(data, size, checksum);
}
this->tx_header_[TX_CHECKSUM_IDX] = checksum;
#ifdef USE_IMPROV_SERIAL_UART
this->uart_->write_array(this->tx_header_, header_tx_len);
if (there_is_data) {
this->uart_->write_array(data, size);
this->uart_->write_array(&this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline
}
#elif defined(USE_ESP32)
switch (this->uart_selection_) {
case logger::UART_SELECTION_UART0:
case logger::UART_SELECTION_UART1:
#if defined(USE_ESP32_VARIANT_ESP32)
case logger::UART_SELECTION_UART2:
#endif
uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len);
if (there_is_data) {
uart_write_bytes(this->uart_num_, data, size);
uart_write_bytes(this->uart_num_, &this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline
}
break;
#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
case logger::UART_SELECTION_USB_CDC:
esp_usb_console_write_buf((const char *) this->tx_header_, header_tx_len);
if (there_is_data) {
esp_usb_console_write_buf((const char *) data, size);
esp_usb_console_write_buf((const char *) &this->tx_header_[TX_CHECKSUM_IDX],
2); // Footer: checksum and newline
}
break;
#endif
#ifdef USE_LOGGER_USB_SERIAL_JTAG
case logger::UART_SELECTION_USB_SERIAL_JTAG:
usb_serial_jtag_write_bytes((const char *) this->tx_header_, header_tx_len, 20 / portTICK_PERIOD_MS);
if (there_is_data) {
usb_serial_jtag_write_bytes((const char *) data, size, 20 / portTICK_PERIOD_MS);
usb_serial_jtag_write_bytes((const char *) &this->tx_header_[TX_CHECKSUM_IDX], 2,
20 / portTICK_PERIOD_MS); // Footer: checksum and newline
}
break;
#endif
default:
break;
}
#elif defined(USE_ARDUINO)
this->hw_serial_->write(this->tx_header_, header_tx_len);
if (there_is_data) {
this->hw_serial_->write(data, size);
this->hw_serial_->write(&this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline
}
#endif
}
#ifdef USE_WEBSERVER
void ImprovSerialComponent::add_webserver_urls_(improv::RpcResponseBuilder &builder, [[maybe_unused]] bool wifi_first) {
// The webserver listens on every interface, so advertise each one that has a usable IPv4.
@@ -159,20 +100,10 @@ void ImprovSerialComponent::add_webserver_urls_(improv::RpcResponseBuilder &buil
// interface's addresses, which are all-unset (0.0.0.0) when e.g. Ethernet has no link while
// the device is online via Wi-Fi, and 0.0.0.0 must not become the advertised URL. OpenThread
// is omitted: it only ever has IPv6 addresses, which cannot form an IPv4 http:// URL.
const auto append_urls = [&builder](const network::IPAddresses &addresses) {
const auto append_urls = [this, &builder](const network::IPAddresses &addresses) {
for (const auto &ip : addresses) {
if (!ip.is_ip4() || !ip.is_set())
continue;
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
ip.str_to(ip_buf);
// "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54
char webserver_url[7 + network::IP_ADDRESS_BUFFER_SIZE + 1 + 5 + 1];
// buf_append_printf keeps the format string in flash on ESP8266
size_t len =
buf_append_printf(webserver_url, sizeof(webserver_url), 0, "http://%s:%u", ip_buf, USE_WEBSERVER_PORT);
if (!builder.add_string(webserver_url, len)) {
ESP_LOGW(TAG, "Response full; URL dropped");
}
if (ip.is_ip4() && ip.is_set())
this->add_webserver_url_(builder, ip);
}
};
#ifdef USE_WIFI
@@ -260,39 +191,52 @@ void ImprovSerialComponent::send_version_info_() {
}
bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) {
size_t at = this->rx_buffer_.size();
this->rx_buffer_.push_back(byte);
const size_t at = this->rx_len_;
ESP_LOGV(TAG, "Byte: 0x%02X", byte);
const uint8_t *raw = &this->rx_buffer_[0];
if (at == 0) {
// The parser's own first test; a stray byte then costs no heap block
if (byte != 'I')
return false;
this->rx_buffer_ = std::make_unique<uint8_t[]>(MAX_FRAME_SIZE);
} else if (at > FRAME_HEADER_SIZE && at > FRAME_HEADER_SIZE + this->rx_buffer_[FRAME_HEADER_SIZE - 1]) {
// Past the checksum, where the parser rejects the byte without reading it
return false;
}
this->rx_buffer_[at] = byte;
this->rx_len_ = at + 1;
return improv::parse_improv_serial_byte(
at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); },
at, byte, this->rx_buffer_.get(),
[this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); },
[this](improv::Error error) -> void {
ESP_LOGW(TAG, "Error decoding payload");
this->set_error_(error);
this->send_state_byte_(improv::TYPE_ERROR_STATE, error);
});
}
bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command) {
#ifdef USE_WIFI
auto *wifi = wifi::global_wifi_component;
#endif
switch (command.command) {
case improv::WIFI_SETTINGS: {
#ifdef USE_WIFI
if (wifi::global_wifi_component == nullptr || wifi::global_wifi_component->is_disabled()) {
if (wifi == nullptr || wifi->is_disabled()) {
// Wi-Fi is disabled, so we can't provision. Respond immediately
// instead of letting the client wait out its provisioning timeout.
ESP_LOGW(TAG, "Wi-Fi is disabled; cannot provision");
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
this->send_state_byte_(improv::TYPE_ERROR_STATE, improv::ERROR_UNABLE_TO_CONNECT);
return true;
}
wifi::WiFiAP sta{};
this->connecting_sta_ = std::make_unique<wifi::WiFiAP>();
auto &sta = *this->connecting_sta_;
sta.set_ssid(command.ssid.c_str());
sta.set_password(command.password.c_str());
this->connecting_sta_ = sta;
// Sampled before start_connecting(): the old connection drops asynchronously after it.
const bool switching = wifi::global_wifi_component->is_connected();
wifi::global_wifi_component->set_sta(sta);
wifi::global_wifi_component->start_connecting(sta);
const bool switching = wifi->is_connected();
wifi->set_sta(sta);
wifi->start_connecting(sta);
this->set_state_(improv::STATE_PROVISIONING);
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());
@@ -302,7 +246,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
#else
// No Wi-Fi support compiled in; there is nothing to provision.
ESP_LOGW(TAG, "Wi-Fi not supported; cannot provision");
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
this->send_state_byte_(improv::TYPE_ERROR_STATE, improv::ERROR_UNABLE_TO_CONNECT);
#endif
return true;
}
@@ -312,10 +256,10 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
// offer a Wi-Fi form. General connectivity (e.g. Ethernet) is reported separately
// via GET_NETWORK_STATE.
#ifdef USE_WIFI
if (wifi::global_wifi_component == nullptr || wifi::global_wifi_component->is_disabled()) {
if (wifi == nullptr || wifi->is_disabled()) {
// Reported transiently without disturbing our internal provisioning state machine,
// so a later `wifi.enable` still reports the correct state.
this->send_current_state_(improv::STATE_STOPPED);
this->send_state_byte_(improv::TYPE_CURRENT_STATE, improv::STATE_STOPPED);
return true;
}
this->set_state_(this->state_);
@@ -323,7 +267,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
this->send_settings_response_(improv::GET_CURRENT_STATE);
}
#else
this->send_current_state_(improv::STATE_STOPPED);
this->send_state_byte_(improv::TYPE_CURRENT_STATE, improv::STATE_STOPPED);
#endif
return true;
}
@@ -335,7 +279,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
// Declared out here because the terminating empty response is sent with or without Wi-Fi
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
#ifdef USE_WIFI
const auto &results = wifi::global_wifi_component->get_scan_result();
const auto &results = wifi->get_scan_result();
for (const auto &scan : results) {
bool with_auth = false;
if (!wifi::should_show_scan_entry(results, scan, with_auth))
@@ -387,20 +331,15 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
#ifdef USE_WEBSERVER
// Not tied to one interface, so follow the configured priority the way
// network::get_ip_addresses() does: a wifi-first network priority list leads with Wi-Fi.
if (flags & improv::NETWORK_IS_ONLINE) {
#if defined(USE_NETWORK_PRIMARY_INTERFACE_WIFI) && defined(USE_WIFI)
this->add_webserver_urls_(builder, /*wifi_first=*/true);
#else
this->add_webserver_urls_(builder, /*wifi_first=*/false);
#endif
}
if (flags & improv::NETWORK_IS_ONLINE)
this->add_webserver_urls_(builder, WIFI_URL_FIRST);
#endif
this->send_response_(builder.finish(false));
return true;
}
default: {
ESP_LOGW(TAG, "Unknown payload");
this->set_error_(improv::ERROR_UNKNOWN_RPC);
this->send_state_byte_(improv::TYPE_ERROR_STATE, improv::ERROR_UNKNOWN_RPC);
return false;
}
}
@@ -408,19 +347,16 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
void ImprovSerialComponent::set_state_(improv::State state) {
this->state_ = state;
this->send_current_state_(state);
this->send_state_byte_(improv::TYPE_CURRENT_STATE, state);
}
void ImprovSerialComponent::send_current_state_(improv::State state) {
this->tx_header_[TX_TYPE_IDX] = TYPE_CURRENT_STATE;
this->tx_header_[TX_DATA_IDX] = state;
this->write_data_();
}
void ImprovSerialComponent::set_error_(improv::Error error) {
this->tx_header_[TX_TYPE_IDX] = TYPE_ERROR_STATE;
this->tx_header_[TX_DATA_IDX] = error;
this->write_data_();
void ImprovSerialComponent::send_state_byte_(improv::ImprovSerialType type, uint8_t value) {
uint8_t frame[FRAME_HEADER_SIZE + 3];
const uint8_t checksum = write_header(frame, type, 1) + value;
frame[FRAME_HEADER_SIZE] = value;
frame[FRAME_HEADER_SIZE + 1] = checksum;
frame[FRAME_HEADER_SIZE + 2] = '\n';
this->write_(frame, sizeof(frame));
}
void ImprovSerialComponent::send_response_(std::span<const uint8_t> response) {
@@ -428,16 +364,22 @@ void ImprovSerialComponent::send_response_(std::span<const uint8_t> response) {
if (response.size() > MAX_SERIAL_RESPONSE) {
ESP_LOGE(TAG, "Response too long");
// Fail fast instead of leaving the client to wait out its timeout
this->set_error_(improv::ERROR_UNKNOWN);
this->send_state_byte_(improv::TYPE_ERROR_STATE, improv::ERROR_UNKNOWN);
return;
}
this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE;
this->write_data_(response.data(), response.size());
uint8_t header[FRAME_HEADER_SIZE];
const uint8_t footer[2] = {
sum_bytes(response.data(), response.size(), write_header(header, improv::TYPE_RPC_RESPONSE, response.size())),
'\n'};
this->write_(header, sizeof(header));
this->write_(response.data(), response.size());
this->write_(footer, sizeof(footer));
}
#ifdef USE_WIFI
void ImprovSerialComponent::on_wifi_connect_timeout_() {
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
this->connecting_sta_.reset();
this->send_state_byte_(improv::TYPE_ERROR_STATE, improv::ERROR_UNABLE_TO_CONNECT);
this->set_state_(improv::STATE_AUTHORIZED);
ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
wifi::global_wifi_component->clear_sta();
@@ -8,8 +8,8 @@
#include "esphome/core/helpers.h"
#ifdef USE_IMPROV_SERIAL
#include <improv.h>
#include <memory>
#include <span>
#include <vector>
#ifdef USE_WIFI
#include "esphome/components/wifi/wifi_component.h"
@@ -17,51 +17,26 @@
#ifdef USE_IMPROV_SERIAL_UART
#include "esphome/components/uart/uart_component.h"
#elif defined(USE_ESP32)
#include <driver/uart.h>
#ifdef USE_LOGGER_USB_SERIAL_JTAG
#include <driver/usb_serial_jtag.h>
#include <hal/usb_serial_jtag_ll.h>
#endif
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include <esp_private/usb_console.h>
#endif
#elif defined(USE_ARDUINO)
#include <HardwareSerial.h>
#endif
namespace esphome::improv_serial {
// TX buffer layout constants
static constexpr uint8_t TX_HEADER_SIZE = 6; // Bytes 0-5 = "IMPROV"
static constexpr uint8_t TX_VERSION_IDX = 6;
static constexpr uint8_t TX_TYPE_IDX = 7;
static constexpr uint8_t TX_LENGTH_IDX = 8;
static constexpr uint8_t TX_DATA_IDX = 9; // For state/error messages only
static constexpr uint8_t TX_CHECKSUM_IDX = 10;
static constexpr uint8_t TX_NEWLINE_IDX = 11;
static constexpr uint8_t TX_BUFFER_SIZE = 12;
enum ImprovSerialType : uint8_t {
TYPE_CURRENT_STATE = 0x01,
TYPE_ERROR_STATE = 0x02,
TYPE_RPC = 0x03,
TYPE_RPC_RESPONSE = 0x04
};
static const uint16_t IMPROV_SERIAL_TIMEOUT = 100;
static const uint8_t IMPROV_SERIAL_VERSION = 1;
static constexpr uint16_t IMPROV_SERIAL_TIMEOUT = 100;
// "IMPROV", version, type, length
static constexpr size_t FRAME_HEADER_SIZE = 9;
#ifdef USE_WIFI
// Wi-Fi connect failure timers: a fresh provision reports at 30 s (stock behavior), while
// switching networks on an already-connected device (disconnect + reconnect) can legitimately
// take longer; 90 s matches improv_ble's default wifi_timeout.
static const uint32_t WIFI_CONNECT_TIMEOUT_MS = 30000;
static const uint32_t WIFI_SWITCH_TIMEOUT_MS = 90000;
static constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 30000;
static constexpr uint32_t WIFI_SWITCH_TIMEOUT_MS = 90000;
#endif
// The serial frame length field is one byte
static constexpr size_t MAX_SERIAL_RESPONSE = 255;
// Header, payload, checksum
static constexpr size_t MAX_FRAME_SIZE = FRAME_HEADER_SIZE + MAX_SERIAL_RESPONSE + 1;
// command + data length + trailing byte
static constexpr size_t RPC_RESPONSE_OVERHEAD = 3;
static constexpr size_t MAX_SERIAL_PAYLOAD = MAX_SERIAL_RESPONSE - RPC_RESPONSE_OVERHEAD;
@@ -92,10 +67,14 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv
protected:
bool parse_improv_serial_byte_(uint8_t byte);
bool parse_improv_payload_(improv::ImprovCommand &command);
/// Free the frame buffer; a session holds it only while a frame is arriving.
void reset_rx_buffer_() {
this->rx_buffer_.reset();
this->rx_len_ = 0;
}
void set_state_(improv::State state);
void send_current_state_(improv::State state);
void set_error_(improv::Error error);
void send_state_byte_(improv::ImprovSerialType type, uint8_t value);
void send_response_(std::span<const uint8_t> response);
#ifdef USE_WIFI
void on_wifi_connect_timeout_();
@@ -109,88 +88,35 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv
void send_settings_response_(improv::Command command);
void send_version_info_();
ESPHOME_ALWAYS_INLINE optional<uint8_t> read_byte_() {
optional<uint8_t> byte;
uint8_t data = 0;
ESPHOME_ALWAYS_INLINE bool read_byte_(uint8_t &byte) {
#ifdef USE_IMPROV_SERIAL_UART
if (this->uart_->available() && this->uart_->read_byte(&data)) {
byte = data;
}
#elif defined(USE_ESP32)
switch (this->uart_selection_) {
case logger::UART_SELECTION_UART0:
case logger::UART_SELECTION_UART1:
#if defined(USE_ESP32_VARIANT_ESP32)
case logger::UART_SELECTION_UART2:
return this->uart_->available() && this->uart_->read_byte(&byte);
#elif defined(USE_LOGGER_CONSOLE)
return logger::global_logger->console_read(byte);
#else
return false;
#endif
}
ESPHOME_ALWAYS_INLINE void write_(const uint8_t *data, size_t len) {
#ifdef USE_IMPROV_SERIAL_UART
this->uart_->write_array(data, len);
#elif defined(USE_LOGGER_CONSOLE)
logger::global_logger->console_write(data, len);
#endif
if (this->uart_num_ >= 0) {
size_t available;
uart_get_buffered_data_len(this->uart_num_, &available);
if (available) {
uart_read_bytes(this->uart_num_, &data, 1, 0);
byte = data;
}
}
break;
#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
case logger::UART_SELECTION_USB_CDC:
if (esp_usb_console_available_for_read()) {
esp_usb_console_read_buf((char *) &data, 1);
byte = data;
}
break;
#endif
#ifdef USE_LOGGER_USB_SERIAL_JTAG
case logger::UART_SELECTION_USB_SERIAL_JTAG: {
if (usb_serial_jtag_read_bytes((char *) &data, 1, 0)) {
byte = data;
}
break;
}
#endif
default:
break;
}
#elif defined(USE_ARDUINO)
if (this->hw_serial_->available()) {
this->hw_serial_->readBytes(&data, 1);
byte = data;
}
#endif
return byte;
}
void write_data_(const uint8_t *data = nullptr, size_t size = 0);
uint8_t tx_header_[TX_BUFFER_SIZE] = {
'I', // 0: Header
'M', // 1: Header
'P', // 2: Header
'R', // 3: Header
'O', // 4: Header
'V', // 5: Header
IMPROV_SERIAL_VERSION, // 6: Version
0, // 7: ImprovSerialType
0, // 8: Length
0, // 9...X: Data (here, one byte reserved for state/error)
0, // X + 10: Checksum
'\n',
};
#ifdef USE_IMPROV_SERIAL_UART
uart::UARTComponent *uart_{nullptr};
#elif defined(USE_ESP32)
uart_port_t uart_num_;
logger::UARTSelection uart_selection_{logger::UART_SELECTION_UART0};
#elif defined(USE_ARDUINO)
Stream *hw_serial_{nullptr};
#endif
std::vector<uint8_t> rx_buffer_;
std::unique_ptr<uint8_t[]> rx_buffer_;
uint32_t last_read_byte_{0};
#ifdef USE_WIFI
wifi::WiFiAP connecting_sta_;
// Only held while provisioning
std::unique_ptr<wifi::WiFiAP> connecting_sta_;
#endif
improv::State state_{improv::STATE_AUTHORIZED};
uint16_t rx_len_{0};
};
extern ImprovSerialComponent
+36 -55
View File
@@ -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()
+37 -20
View File
@@ -32,7 +32,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
// Get task handle once - used for both main task check and passing to non-main thread handler
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
const bool is_main_task = (current_task == this->main_task_);
#elif (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
+74 -84
View File
@@ -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
+42 -46
View File
@@ -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() {
+1 -1
View File
@@ -19,7 +19,7 @@ inline void HOT Logger::write_msg_(const char *msg, uint16_t len) {
fwrite(msg, 1, len, stdout);
#else
// Regular UART - single write including newline (already in buffer)
uart_write_bytes(this->uart_num_, msg, len);
uart_write_bytes(this->get_uart_num(), msg, len);
#endif
}
+3 -16
View File
@@ -14,9 +14,9 @@ void Logger::pre_setup() {
#if defined(USE_ESP8266_LOGGER_SERIAL)
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
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
+20 -51
View File
@@ -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
+10 -29
View File
@@ -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(); }
-13
View File
@@ -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
+17 -42
View File
@@ -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
+25 -3
View File
@@ -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
Whitespace-only changes.
@@ -0,0 +1,20 @@
esphome:
name: test
esp8266:
board: d1_mini
logger:
wifi:
ssid: test
password: testtesttest
uart:
id: improv_uart
tx_pin: 4
rx_pin: 5
baud_rate: 115200
improv_serial:
uart_id: improv_uart
@@ -0,0 +1,13 @@
esphome:
name: test
esp8266:
board: d1_mini
logger:
wifi:
ssid: test
password: testtesttest
improv_serial: {}
@@ -0,0 +1,16 @@
esphome:
name: test
nrf52:
board: adafruit_itsybitsy_nrf52840
bootloader: adafruit_nrf52_sd140_v6
logger:
network:
enable_ipv6: true
openthread:
tlv: 0E080000000000010000
improv_serial: {}
@@ -0,0 +1,42 @@
"""Tests for the improv_serial transport selection."""
from collections.abc import Callable
from pathlib import Path
from esphome.core import CORE
def test_logger_port_requests_console(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Without uart_id, Improv shares the logger's port through its console API."""
generate_main(component_config_path("improv_logger_port.yaml"))
defines = {d.name for d in CORE.defines}
assert "USE_LOGGER_CONSOLE" in defines
assert "USE_IMPROV_SERIAL_UART" not in defines
def test_dedicated_uart_skips_console(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""A dedicated UART leaves the logger's console API out."""
generate_main(component_config_path("improv_dedicated_uart.yaml"))
defines = {d.name for d in CORE.defines}
assert "USE_IMPROV_SERIAL_UART" in defines
assert "USE_LOGGER_CONSOLE" not in defines
def test_nrf52_requests_nothing(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""The Zephyr logger has no console, so Improv there is a no-op."""
generate_main(component_config_path("improv_nrf52.yaml"))
defines = {d.name for d in CORE.defines}
assert "USE_LOGGER_CONSOLE" not in defines
assert "USE_IMPROV_SERIAL_UART" not in defines
+93 -40
View File
@@ -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
View File
@@ -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
+40 -3
View File
@@ -10,10 +10,15 @@ Covered:
2. Get Device Info returns the firmware/device info RPC response
3. Get Wi-Fi Networks returns deduplicated scan results and a terminator
4. Wi-Fi Settings provisions: saves credentials and reports PROVISIONED
Between 1 and 2, a 255 byte payload, malformed input and a frame cut off past
the timeout must each leave the parser ready for the next request.
"""
from __future__ import annotations
import asyncio
import pytest
from .log_utils import LineWaiter
@@ -24,6 +29,7 @@ from .types import APIClientConnectedFactory, RunCompiledFunction
IMPROV_HEADER = b"IMPROV"
IMPROV_VERSION = 1
TYPE_CURRENT_STATE = 0x01
TYPE_ERROR_STATE = 0x02
TYPE_RPC = 0x03
TYPE_RPC_RESPONSE = 0x04
@@ -33,6 +39,9 @@ CMD_GET_DEVICE_INFO = 0x03
CMD_GET_WIFI_NETWORKS = 0x04
CMD_WIFI_SETTINGS = 0x01
# improv::Error values
ERROR_INVALID_RPC = 0x01
def build_rpc_frame(command: int, data: bytes = b"") -> list[int]:
"""Build a full improv serial frame carrying one RPC command."""
@@ -42,9 +51,9 @@ def build_rpc_frame(command: int, data: bytes = b"") -> list[int]:
return list(frame + bytes([checksum]) + b"\n")
def state_frame_hex(state: int) -> str:
"""Full 12 byte current-state frame as hex, checksum and newline included."""
frame = IMPROV_HEADER + bytes([IMPROV_VERSION, TYPE_CURRENT_STATE, 1, state])
def state_frame_hex(state: int, frame_type: int = TYPE_CURRENT_STATE) -> str:
"""Full 12 byte one-value frame as hex, checksum and newline included."""
frame = IMPROV_HEADER + bytes([IMPROV_VERSION, frame_type, 1, state])
checksum = sum(frame) & 0xFF
return ":".join(f"{b:02X}" for b in frame + bytes([checksum]) + b"\n")
@@ -89,6 +98,34 @@ async def test_improv_serial_uart(
)
await waiter.wait_for("uart_mock", f"TX 12 bytes: {state_frame_hex(0x02)}")
authorized = f"TX 12 bytes: {state_frame_hex(0x02)}"
get_state = build_rpc_frame(CMD_GET_CURRENT_STATE)
# The largest payload (255 bytes) fills the fixed frame buffer exactly
reply = waiter.expect(authorized)
await client.execute_service(
inject, {"payload": build_rpc_frame(CMD_GET_CURRENT_STATE, bytes(253))}
)
await waiter.wait(reply, msg="no reply to a 255 byte payload")
# A bad checksum is reported, and the next frame parses from the start
bad = get_state.copy()
bad[-2] ^= 0xFF
error = waiter.expect(
f"TX 12 bytes: {state_frame_hex(ERROR_INVALID_RPC, TYPE_ERROR_STATE)}"
)
reply = waiter.expect(authorized)
await client.execute_service(inject, {"payload": bad + get_state})
await waiter.wait(error, msg="no error frame for a bad checksum")
await waiter.wait(reply, msg="no reply after a bad checksum")
# A frame cut off for longer than the 100 ms timeout is discarded
await client.execute_service(inject, {"payload": get_state[:8]})
await asyncio.sleep(0.3)
reply = waiter.expect(authorized)
await client.execute_service(inject, {"payload": get_state})
await waiter.wait(reply, msg="no reply after a timed out frame")
# 2. Get Device Info: the always logged 9 byte response header, then the
# payload with the firmware name (must stay under uart_mock's 64 byte
# hex dump cap or the payload line reads "too large to log")