1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-11 18:21:52 +00:00

Compare commits

...

14 Commits

Author SHA1 Message Date
J. Nick Koston
242d152edc Use ESPHOME_DEBUG_API guard instead of DEBUG
CI compile tests also define DEBUG, so a dedicated guard is needed.
Enable ESPHOME_DEBUG_API in integration tests via conftest.py.
2026-02-11 11:34:14 -06:00
J. Nick Koston
e63308aa33 Use DEBUG guard for asserts instead of custom define
Integration tests already pass -DDEBUG, so no custom define needed.
2026-02-11 11:30:51 -06:00
J. Nick Koston
93ff39709a Enable ESPHOME_DEBUG_API in API integration tests 2026-02-11 11:28:08 -06:00
J. Nick Koston
d3e47a5d80 Fix encode_bool to encode actual value when force=true 2026-02-11 11:20:41 -06:00
J. Nick Koston
77d8fc80be Guard asserts with ESPHOME_DEBUG_API to avoid flash overhead 2026-02-11 11:18:50 -06:00
J. Nick Koston
fb1c495506 tweaks 2026-02-11 11:12:46 -06:00
J. Nick Koston
dc2085cc25 tweaks 2026-02-11 11:11:22 -06:00
J. Nick Koston
25baec48ee [api] Optimize varint encoding to eliminate uint64_t widening
Replace ProtoVarInt temporaries with direct uint32_t encoding functions
to avoid unnecessary 64-bit widening on 32-bit architectures (ESP32 Xtensa).

- Add encode_varint_to_buffer() free function for direct buffer encoding
- Replace encode_varint_raw(ProtoVarInt) with uint32_t-native implementation
- Add encode_varint_raw_64() for the few call sites needing 64-bit (BLE)
- Remove unused ProtoVarInt::encode() and encode_to_buffer_unchecked()
- Remove dead null checks from ProtoVarInt::parse()

Net savings: -152 bytes flash on ESP32 IDF.
2026-02-11 11:02:03 -06:00
Djordje Mandic
b1f0db9da8 [bl0942] Update reference values (#12867) 2026-02-11 11:10:32 -05:00
J. Nick Koston
923445eb5d [light] Eliminate redundant clamp in LightCall::validate_() (#13923) 2026-02-11 10:06:44 -06:00
tomaszduda23
9bdae5183c [nrf52,logger] add support for task_log_buffer_size (#13862)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-11 15:43:55 +00:00
J. Nick Koston
37f97c9043 [esp8266][rp2040] Eliminate heap fallback in preference save/load (#13928) 2026-02-11 08:41:15 -06:00
J. Nick Koston
8e785a2216 [web_server] Remove unnecessary packed attribute from DeferredEvent (#13932) 2026-02-11 08:40:41 -06:00
schrob
4fb1ddf212 [api] Fix compiler format warnings (#13931) 2026-02-11 08:40:21 -06:00
27 changed files with 599 additions and 435 deletions

View File

@@ -1510,7 +1510,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor;
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(),
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp;

View File

@@ -295,9 +295,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
buf_start[header_offset] = 0x00; // indicator
// Encode varints directly into buffer
ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
ProtoVarInt(msg.message_type)
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1);
encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len);
// Add iovec for this message (header + payload)
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);

View File

@@ -133,7 +133,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
break;
}
default:
ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer));
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
return;
}
}

View File

@@ -57,6 +57,16 @@ inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
return count;
}
/// Encode a varint directly into a pre-allocated buffer.
/// Caller must ensure buffer has space (use ProtoSize::varint() to calculate).
inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) {
while (val > 0x7F) {
*buffer++ = static_cast<uint8_t>(val | 0x80);
val >>= 7;
}
*buffer = static_cast<uint8_t>(val);
}
/*
* StringRef Ownership Model for API Protocol Messages
* ===================================================
@@ -93,17 +103,17 @@ class ProtoVarInt {
ProtoVarInt() : value_(0) {}
explicit ProtoVarInt(uint64_t value) : value_(value) {}
/// Parse a varint from buffer. consumed must be a valid pointer (not null).
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
if (len == 0) {
if (consumed != nullptr)
*consumed = 0;
#ifdef ESPHOME_DEBUG_API
assert(consumed != nullptr);
#endif
if (len == 0)
return {};
}
// Most common case: single-byte varint (values 0-127)
if ((buffer[0] & 0x80) == 0) {
if (consumed != nullptr)
*consumed = 1;
*consumed = 1;
return ProtoVarInt(buffer[0]);
}
@@ -122,14 +132,11 @@ class ProtoVarInt {
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
bitpos += 7;
if ((val & 0x80) == 0) {
if (consumed != nullptr)
*consumed = i + 1;
*consumed = i + 1;
return ProtoVarInt(result);
}
}
if (consumed != nullptr)
*consumed = 0;
return {}; // Incomplete or invalid varint
}
@@ -153,50 +160,6 @@ class ProtoVarInt {
// with ZigZag encoding
return decode_zigzag64(this->value_);
}
/**
* Encode the varint value to a pre-allocated buffer without bounds checking.
*
* @param buffer The pre-allocated buffer to write the encoded varint to
* @param len The size of the buffer in bytes
*
* @note The caller is responsible for ensuring the buffer is large enough
* to hold the encoded value. Use ProtoSize::varint() to calculate
* the exact size needed before calling this method.
* @note No bounds checking is performed for performance reasons.
*/
void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
uint64_t val = this->value_;
if (val <= 0x7F) {
buffer[0] = val;
return;
}
size_t i = 0;
while (val && i < len) {
uint8_t temp = val & 0x7F;
val >>= 7;
if (val) {
buffer[i++] = temp | 0x80;
} else {
buffer[i++] = temp;
}
}
}
void encode(std::vector<uint8_t> &out) {
uint64_t val = this->value_;
if (val <= 0x7F) {
out.push_back(val);
return;
}
while (val) {
uint8_t temp = val & 0x7F;
val >>= 7;
if (val) {
out.push_back(temp | 0x80);
} else {
out.push_back(temp);
}
}
}
protected:
uint64_t value_;
@@ -256,8 +219,24 @@ class ProtoWriteBuffer {
public:
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
void write(uint8_t value) { this->buffer_->push_back(value); }
void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
void encode_varint_raw(uint32_t value) {
if (value <= 0x7F) {
this->buffer_->push_back(static_cast<uint8_t>(value));
return;
}
while (value > 0x7F) {
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
value >>= 7;
}
this->buffer_->push_back(static_cast<uint8_t>(value));
}
void encode_varint_raw_64(uint64_t value) {
while (value > 0x7F) {
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
value >>= 7;
}
this->buffer_->push_back(static_cast<uint8_t>(value));
}
/**
* Encode a field key (tag/wire type combination).
*
@@ -307,13 +286,13 @@ class ProtoWriteBuffer {
if (value == 0 && !force)
return;
this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
this->encode_varint_raw(ProtoVarInt(value));
this->encode_varint_raw_64(value);
}
void encode_bool(uint32_t field_id, bool value, bool force = false) {
if (!value && !force)
return;
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
this->write(0x01);
this->buffer_->push_back(value ? 0x01 : 0x00);
}
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
if (value == 0 && !force)
@@ -938,13 +917,15 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa
this->buffer_->resize(this->buffer_->size() + varint_length_bytes);
// Write the length varint directly
ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes);
encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin);
// Now encode the message content - it will append to the buffer
value.encode(*this);
#ifdef ESPHOME_DEBUG_API
// Verify that the encoded size matches what we calculated
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
#endif
}
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined

View File

@@ -59,10 +59,10 @@ namespace bl0942 {
//
// Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4
static const float BL0942_PREF = 596; // taken from tasmota
static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218
static const float BL0942_IREF = 251213.46469622; // 305978/1.218
static const float BL0942_EREF = 3304.61127328; // Measured
static const float BL0942_PREF = 623.0270705; // calculated using UREF and IREF
static const float BL0942_UREF = 15883.34116; // calculated for (390k x 5 / 510R) voltage divider
static const float BL0942_IREF = 251065.6814; // calculated for 1mR shunt
static const float BL0942_EREF = 5347.484240; // calculated using UREF and IREF
struct DataPacket {
uint8_t frame_header;
@@ -86,11 +86,11 @@ enum LineFrequency : uint8_t {
class BL0942 : public PollingComponent, public uart::UARTDevice {
public:
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; }
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; }
void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; }
void set_energy_sensor(sensor::Sensor *energy_sensor) { this->energy_sensor_ = energy_sensor; }
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; }
void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; }
void set_address(uint8_t address) { this->address_ = address; }
void set_reset(bool reset) { this->reset_ = reset; }

View File

@@ -33,6 +33,10 @@ static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false).
// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need
// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so
// 64 words (256 bytes) suffices since most preferences go to RTC memory instead.
#ifdef USE_ESP8266_PREFERENCES_FLASH
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
#else
@@ -127,9 +131,11 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
return true;
}
// Stack buffer size - 16 words total: up to 15 words of preference data + 1 word CRC (60 bytes of preference data)
// This handles virtually all real-world preferences without heap allocation
static constexpr size_t PREF_BUFFER_WORDS = 16;
// Maximum buffer for any single preference - bounded by storage sizes.
// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words).
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions.
static constexpr size_t PREF_MAX_BUFFER_WORDS =
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
class ESP8266PreferenceBackend : public ESPPreferenceBackend {
public:
@@ -141,15 +147,13 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
bool save(const uint8_t *data, size_t len) override {
if (bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
uint32_t *buffer = buffer_alloc.get();
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
memset(buffer, 0, buffer_size * sizeof(uint32_t));
memcpy(buffer, data, len);
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
: save_to_rtc(this->offset, buffer, buffer_size);
}
@@ -157,19 +161,16 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
bool load(uint8_t *data, size_t len) override {
if (bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
uint32_t *buffer = buffer_alloc.get();
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
: load_from_rtc(this->offset, buffer, buffer_size);
if (!ret)
return false;
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
return false;
memcpy(data, buffer, len);
return true;
}

View File

@@ -270,22 +270,23 @@ LightColorValues LightCall::validate_() {
if (this->has_state())
v.set_state(this->state_);
#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \
// clamp_and_log_if_invalid already clamps in-place, so assign directly
// to avoid redundant clamp code from the setter being inlined.
#define VALIDATE_AND_APPLY(field, name_str, ...) \
if (this->has_##field()) { \
clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
v.setter(this->field##_); \
v.field##_ = this->field##_; \
}
VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness")
VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness")
VALIDATE_AND_APPLY(red, set_red, "Red")
VALIDATE_AND_APPLY(green, set_green, "Green")
VALIDATE_AND_APPLY(blue, set_blue, "Blue")
VALIDATE_AND_APPLY(white, set_white, "White")
VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white")
VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white")
VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(),
traits.get_max_mireds())
VALIDATE_AND_APPLY(brightness, "Brightness")
VALIDATE_AND_APPLY(color_brightness, "Color brightness")
VALIDATE_AND_APPLY(red, "Red")
VALIDATE_AND_APPLY(green, "Green")
VALIDATE_AND_APPLY(blue, "Blue")
VALIDATE_AND_APPLY(white, "White")
VALIDATE_AND_APPLY(cold_white, "Cold white")
VALIDATE_AND_APPLY(warm_white, "Warm white")
VALIDATE_AND_APPLY(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
#undef VALIDATE_AND_APPLY

View File

@@ -95,15 +95,18 @@ class LightColorValues {
*/
void normalize_color() {
if (this->color_mode_ & ColorCapability::RGB) {
float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_blue()));
float max_value = fmaxf(this->red_, fmaxf(this->green_, this->blue_));
// Assign directly to avoid redundant clamp in set_red/green/blue.
// Values are guaranteed in [0,1]: inputs are already clamped to [0,1],
// and dividing by max_value (the largest) keeps results in [0,1].
if (max_value == 0.0f) {
this->set_red(1.0f);
this->set_green(1.0f);
this->set_blue(1.0f);
this->red_ = 1.0f;
this->green_ = 1.0f;
this->blue_ = 1.0f;
} else {
this->set_red(this->get_red() / max_value);
this->set_green(this->get_green() / max_value);
this->set_blue(this->get_blue() / max_value);
this->red_ /= max_value;
this->green_ /= max_value;
this->blue_ /= max_value;
}
}
}
@@ -276,6 +279,8 @@ class LightColorValues {
/// Set the warm white property of these light color values. In range 0.0 to 1.0.
void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
friend class LightCall;
protected:
float state_; ///< ON / OFF, float for transition
float brightness_;

View File

@@ -231,9 +231,16 @@ CONFIG_SCHEMA = cv.All(
bk72xx=768,
ln882x=768,
rtl87xx=768,
nrf52=768,
): cv.All(
cv.only_on(
[PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX]
[
PLATFORM_ESP32,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
PLATFORM_NRF52,
]
),
cv.validate_bytes,
cv.Any(
@@ -313,11 +320,13 @@ async def to_code(config):
)
if CORE.is_esp32:
cg.add(log.create_pthread_key())
if CORE.is_esp32 or CORE.is_libretiny:
if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52:
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
if task_log_buffer_size > 0:
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
cg.add(log.init_log_buffer(task_log_buffer_size))
if CORE.using_zephyr:
zephyr_add_prj_conf("MPSC_PBUF", True)
elif CORE.is_host:
cg.add(log.create_pthread_key())
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
@@ -417,6 +426,7 @@ async def to_code(config):
pass
if CORE.is_nrf52:
zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True)
if config[CONF_HARDWARE_UART] == UART0:
zephyr_add_overlay("""&uart0 { status = "okay";};""")
if config[CONF_HARDWARE_UART] == UART1:

View File

@@ -0,0 +1,190 @@
#pragma once
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::logger {
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
static constexpr uint16_t MAX_HEADER_SIZE = 128;
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
'\0', // NONE
'1', // ERROR (31 = red)
'3', // WARNING (33 = yellow)
'2', // INFO (32 = green)
'5', // CONFIG (35 = magenta)
'6', // DEBUG (36 = cyan)
'7', // VERBOSE (37 = gray)
'8', // VERY_VERBOSE (38 = white)
};
static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
'\0', // NONE
'E', // ERROR
'W', // WARNING
'I', // INFO
'C', // CONFIG
'D', // DEBUG
'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
};
// Buffer wrapper for log formatting functions
struct LogBuffer {
char *data;
uint16_t size;
uint16_t pos{0};
// Replaces the null terminator with a newline for console output.
// Must be called after notify_listeners_() since listeners need null-terminated strings.
// Console output uses length-based writes (buf.pos), so null terminator is not needed.
void terminate_with_newline() {
if (this->pos < this->size) {
this->data[this->pos++] = '\n';
} else if (this->size > 0) {
// Buffer was full - replace last char with newline to ensure it's visible
this->data[this->size - 1] = '\n';
this->pos = this->size;
}
}
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
// Early return if insufficient space - intentionally don't update pos to prevent partial writes
if (this->pos + MAX_HEADER_SIZE > this->size)
return;
char *p = this->current_();
// Write ANSI color
this->write_ansi_color_(p, level);
// Construct: [LEVEL][tag:line]
*p++ = '[';
if (level != 0) {
if (level >= 7) {
*p++ = 'V'; // VERY_VERBOSE = "VV"
*p++ = 'V';
} else {
*p++ = LOG_LEVEL_LETTER_CHARS[level];
}
}
*p++ = ']';
*p++ = '[';
// Copy tag
this->copy_string_(p, tag);
*p++ = ':';
// Format line number without modulo operations
if (line > 999) [[unlikely]] {
int thousands = line / 1000;
*p++ = '0' + thousands;
line -= thousands * 1000;
}
int hundreds = line / 100;
int remainder = line - hundreds * 100;
int tens = remainder / 10;
*p++ = '0' + hundreds;
*p++ = '0' + tens;
*p++ = '0' + (remainder - tens * 10);
*p++ = ']';
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
// Write thread name with bold red color
if (thread_name != nullptr) {
this->write_ansi_color_(p, 1); // Bold red for thread name
*p++ = '[';
this->copy_string_(p, thread_name);
*p++ = ']';
this->write_ansi_color_(p, level); // Restore original color
}
#endif
*p++ = ':';
*p++ = ' ';
this->pos = p - this->data;
}
void HOT format_body(const char *format, va_list args) {
this->format_vsnprintf_(format, args);
this->finalize_();
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
void HOT format_body_P(PGM_P format, va_list args) {
this->format_vsnprintf_P_(format, args);
this->finalize_();
}
#endif
void write_body(const char *text, uint16_t text_length) {
this->write_(text, text_length);
this->finalize_();
}
private:
bool full_() const { return this->pos >= this->size; }
uint16_t remaining_() const { return this->size - this->pos; }
char *current_() { return this->data + this->pos; }
void write_(const char *value, uint16_t length) {
const uint16_t available = this->remaining_();
const uint16_t copy_len = (length < available) ? length : available;
if (copy_len > 0) {
memcpy(this->current_(), value, copy_len);
this->pos += copy_len;
}
}
void finalize_() {
// Write color reset sequence
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
// Null terminate
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
}
void strip_trailing_newlines_() {
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
this->pos--;
}
void process_vsnprintf_result_(int ret) {
if (ret < 0)
return;
const uint16_t rem = this->remaining_();
this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
this->strip_trailing_newlines_();
}
void format_vsnprintf_(const char *format, va_list args) {
if (this->full_())
return;
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
void format_vsnprintf_P_(PGM_P format, va_list args) {
if (this->full_())
return;
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
}
#endif
// Write ANSI color escape sequence to buffer, updates pointer in place
// Caller is responsible for ensuring buffer has sufficient space
void write_ansi_color_(char *&p, uint8_t level) {
if (level == 0)
return;
// Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
*p++ = '\033';
*p++ = '[';
*p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
*p++ = ';';
*p++ = '3';
*p++ = LOG_LEVEL_COLOR_DIGIT[level];
*p++ = 'm';
}
// Copy string without null terminator, updates pointer in place
// Caller is responsible for ensuring buffer has sufficient space
void copy_string_(char *&p, const char *str) {
const size_t len = strlen(str);
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
// piece
memcpy(p, str, len);
p += len;
}
};
} // namespace esphome::logger

View File

@@ -10,9 +10,9 @@ namespace esphome::logger {
static const char *const TAG = "logger";
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS)
// Main thread/task always uses direct buffer access for console output and callbacks
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS,
// Zephyr) Main thread/task always uses direct buffer access for console output and callbacks
//
// For non-main threads/tasks:
// - WITH task log buffer: Prefer sending to ring buffer for async processing
@@ -31,6 +31,9 @@ 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)
k_tid_t current_task = k_current_get();
const bool is_main_task = (current_task == this->main_task_);
#else // USE_HOST
const bool is_main_task = pthread_equal(pthread_self(), this->main_thread_);
#endif
@@ -54,6 +57,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
// 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);
#elif defined(USE_ZEPHYR)
char thread_name_buf[MAX_POINTER_REPRESENTATION];
const char *thread_name = get_thread_name_(thread_name_buf, current_task);
#else // USE_HOST
char thread_name_buf[THREAD_NAME_BUF_SIZE];
const char *thread_name = this->get_thread_name_(thread_name_buf);
@@ -83,18 +89,21 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
// This is safe to call from any context including ISRs
this->enable_loop_soon_any_context();
}
#endif // USE_ESPHOME_TASK_LOG_BUFFER
#endif
// Emergency console logging for non-main threads when ring buffer is full or disabled
// This is a fallback mechanism to ensure critical log messages are visible
// Note: This may cause interleaved/corrupted console output if multiple threads
// log simultaneously, but it's better than losing important messages entirely
#ifdef USE_HOST
if (!message_sent) {
if (!message_sent)
#else
if (!message_sent && this->baud_rate_ > 0) // If logging is enabled, write to console
#endif
{
#ifdef USE_HOST
// Host always has console output - no baud_rate check needed
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512;
#else
if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console
// Maximum size for console log messages (includes null terminator)
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
#endif
@@ -107,22 +116,16 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
// RAII guard automatically resets on return
}
#else
// Implementation for single-task platforms (ESP8266, RP2040, Zephyr)
// TODO: Zephyr may have multiple threads (work queues, etc.) but uses this single-task path.
// 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).
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_)
return;
#ifdef USE_ZEPHYR
char tmp[MAX_POINTER_REPRESENTATION];
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args,
this->get_thread_name_(tmp));
#else // Other single-task platforms don't have thread names, so pass nullptr
// Other single-task platforms don't have thread names, so pass nullptr
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
#endif
}
#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY
#endif // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR
#ifdef USE_STORE_LOG_STR_IN_FLASH
// Implementation for ESP8266 with flash string support.
@@ -163,19 +166,12 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
}
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
void Logger::init_log_buffer(size_t total_buffer_size) {
#ifdef USE_HOST
// Host uses slot count instead of byte size
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->log_buffer_ = new logger::TaskLogBufferHost(total_buffer_size);
#elif defined(USE_ESP32)
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size);
#elif defined(USE_LIBRETINY)
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->log_buffer_ = new logger::TaskLogBufferLibreTiny(total_buffer_size);
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Zephyr needs loop working to check when CDC port is open
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
// Start with loop disabled when using task buffer (unless using USB CDC on ESP32)
// The loop will be enabled automatically when messages arrive
this->disable_loop_when_buffer_empty_();
@@ -183,52 +179,33 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
}
#endif
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
void Logger::loop() { this->process_messages_(); }
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC))
void Logger::loop() {
this->process_messages_();
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)
this->cdc_loop_();
#endif
}
#endif
void Logger::process_messages_() {
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
// Process any buffered messages when available
if (this->log_buffer_->has_messages()) {
#ifdef USE_HOST
logger::TaskLogBufferHost::LogMessage *message;
while (this->log_buffer_->get_message_main_loop(&message)) {
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text,
message->text_length, buf);
this->log_buffer_->release_message_main_loop();
this->write_log_buffer_to_console_(buf);
}
#elif defined(USE_ESP32)
logger::TaskLogBuffer::LogMessage *message;
const char *text;
void *received_token;
while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) {
uint16_t text_length;
while (this->log_buffer_->borrow_message_main_loop(message, text_length)) {
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text,
message->text_length, buf);
// Release the message to allow other tasks to use it as soon as possible
this->log_buffer_->release_message_main_loop(received_token);
this->write_log_buffer_to_console_(buf);
}
#elif defined(USE_LIBRETINY)
logger::TaskLogBufferLibreTiny::LogMessage *message;
const char *text;
while (this->log_buffer_->borrow_message_main_loop(&message, &text)) {
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text,
message->text_length, buf);
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name,
message->text_data(), text_length, buf);
// Release the message to allow other tasks to use it as soon as possible
this->log_buffer_->release_message_main_loop();
this->write_log_buffer_to_console_(buf);
}
#endif
}
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Zephyr needs loop working to check when CDC port is open
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
else {
// No messages to process, disable loop if appropriate
// This reduces overhead when there's no async logging activity

View File

@@ -13,15 +13,11 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
#ifdef USE_HOST
#include "log_buffer.h"
#include "task_log_buffer_host.h"
#elif defined(USE_ESP32)
#include "task_log_buffer_esp32.h"
#elif defined(USE_LIBRETINY)
#include "task_log_buffer_libretiny.h"
#endif
#endif
#include "task_log_buffer_zephyr.h"
#ifdef USE_ARDUINO
#if defined(USE_ESP8266)
@@ -97,195 +93,10 @@ struct CStrCompare {
};
#endif
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
'\0', // NONE
'1', // ERROR (31 = red)
'3', // WARNING (33 = yellow)
'2', // INFO (32 = green)
'5', // CONFIG (35 = magenta)
'6', // DEBUG (36 = cyan)
'7', // VERBOSE (37 = gray)
'8', // VERY_VERBOSE (38 = white)
};
static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
'\0', // NONE
'E', // ERROR
'W', // WARNING
'I', // INFO
'C', // CONFIG
'D', // DEBUG
'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
};
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
static constexpr uint16_t MAX_HEADER_SIZE = 128;
// "0x" + 2 hex digits per byte + '\0'
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
// Stack buffer size for retrieving thread/task names from the OS
// macOS allows up to 64 bytes, Linux up to 16
static constexpr size_t THREAD_NAME_BUF_SIZE = 64;
// Buffer wrapper for log formatting functions
struct LogBuffer {
char *data;
uint16_t size;
uint16_t pos{0};
// Replaces the null terminator with a newline for console output.
// Must be called after notify_listeners_() since listeners need null-terminated strings.
// Console output uses length-based writes (buf.pos), so null terminator is not needed.
void terminate_with_newline() {
if (this->pos < this->size) {
this->data[this->pos++] = '\n';
} else if (this->size > 0) {
// Buffer was full - replace last char with newline to ensure it's visible
this->data[this->size - 1] = '\n';
this->pos = this->size;
}
}
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
// Early return if insufficient space - intentionally don't update pos to prevent partial writes
if (this->pos + MAX_HEADER_SIZE > this->size)
return;
char *p = this->current_();
// Write ANSI color
this->write_ansi_color_(p, level);
// Construct: [LEVEL][tag:line]
*p++ = '[';
if (level != 0) {
if (level >= 7) {
*p++ = 'V'; // VERY_VERBOSE = "VV"
*p++ = 'V';
} else {
*p++ = LOG_LEVEL_LETTER_CHARS[level];
}
}
*p++ = ']';
*p++ = '[';
// Copy tag
this->copy_string_(p, tag);
*p++ = ':';
// Format line number without modulo operations
if (line > 999) [[unlikely]] {
int thousands = line / 1000;
*p++ = '0' + thousands;
line -= thousands * 1000;
}
int hundreds = line / 100;
int remainder = line - hundreds * 100;
int tens = remainder / 10;
*p++ = '0' + hundreds;
*p++ = '0' + tens;
*p++ = '0' + (remainder - tens * 10);
*p++ = ']';
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
// Write thread name with bold red color
if (thread_name != nullptr) {
this->write_ansi_color_(p, 1); // Bold red for thread name
*p++ = '[';
this->copy_string_(p, thread_name);
*p++ = ']';
this->write_ansi_color_(p, level); // Restore original color
}
#endif
*p++ = ':';
*p++ = ' ';
this->pos = p - this->data;
}
void HOT format_body(const char *format, va_list args) {
this->format_vsnprintf_(format, args);
this->finalize_();
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
void HOT format_body_P(PGM_P format, va_list args) {
this->format_vsnprintf_P_(format, args);
this->finalize_();
}
#endif
void write_body(const char *text, uint16_t text_length) {
this->write_(text, text_length);
this->finalize_();
}
private:
bool full_() const { return this->pos >= this->size; }
uint16_t remaining_() const { return this->size - this->pos; }
char *current_() { return this->data + this->pos; }
void write_(const char *value, uint16_t length) {
const uint16_t available = this->remaining_();
const uint16_t copy_len = (length < available) ? length : available;
if (copy_len > 0) {
memcpy(this->current_(), value, copy_len);
this->pos += copy_len;
}
}
void finalize_() {
// Write color reset sequence
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
// Null terminate
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
}
void strip_trailing_newlines_() {
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
this->pos--;
}
void process_vsnprintf_result_(int ret) {
if (ret < 0)
return;
const uint16_t rem = this->remaining_();
this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
this->strip_trailing_newlines_();
}
void format_vsnprintf_(const char *format, va_list args) {
if (this->full_())
return;
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
void format_vsnprintf_P_(PGM_P format, va_list args) {
if (this->full_())
return;
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
}
#endif
// Write ANSI color escape sequence to buffer, updates pointer in place
// Caller is responsible for ensuring buffer has sufficient space
void write_ansi_color_(char *&p, uint8_t level) {
if (level == 0)
return;
// Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
*p++ = '\033';
*p++ = '[';
*p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
*p++ = ';';
*p++ = '3';
*p++ = LOG_LEVEL_COLOR_DIGIT[level];
*p++ = 'm';
}
// Copy string without null terminator, updates pointer in place
// Caller is responsible for ensuring buffer has sufficient space
void copy_string_(char *&p, const char *str) {
const size_t len = strlen(str);
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
// piece
memcpy(p, str, len);
p += len;
}
};
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
/** Enum for logging UART selection
*
@@ -411,11 +222,14 @@ class Logger : public Component {
bool &flag_;
};
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
// Handles non-main thread logging only (~0.1% of calls)
// thread_name is resolved by the caller from the task handle, avoiding redundant lookups
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_USB_CDC)
void cdc_loop_();
#endif
void process_messages_();
void write_msg_(const char *msg, uint16_t len);
@@ -534,13 +348,7 @@ class Logger : public Component {
std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners
#endif
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
#ifdef USE_HOST
logger::TaskLogBufferHost *log_buffer_{nullptr}; // Allocated once, never freed
#elif defined(USE_ESP32)
logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed
#elif defined(USE_LIBRETINY)
logger::TaskLogBufferLibreTiny *log_buffer_{nullptr}; // Allocated once, never freed
#endif
#endif
// Group smaller types together at the end
@@ -552,7 +360,7 @@ class Logger : public Component {
#ifdef USE_LIBRETINY
UARTSelection uart_{UART_SELECTION_DEFAULT};
#endif
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
bool main_task_recursion_guard_{false};
#ifdef USE_LIBRETINY
bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny
@@ -595,8 +403,10 @@ class Logger : public Component {
}
#elif defined(USE_ZEPHYR)
const char *HOT get_thread_name_(std::span<char> buff) {
k_tid_t current_task = k_current_get();
const char *HOT get_thread_name_(std::span<char> buff, k_tid_t current_task = nullptr) {
if (current_task == nullptr) {
current_task = k_current_get();
}
if (current_task == main_task_) {
return nullptr; // Main task
}
@@ -635,7 +445,7 @@ class Logger : public Component {
// Create RAII guard for non-main task recursion
inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); }
#elif defined(USE_LIBRETINY)
#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
@@ -643,6 +453,8 @@ class Logger : public Component {
// - 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_; }
@@ -651,7 +463,8 @@ class Logger : public Component {
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Zephyr needs loop working to check when CDC port is open
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
// Disable loop when task buffer is empty (with USB CDC check on ESP32)
inline void disable_loop_when_buffer_empty_() {
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context()

View File

@@ -14,7 +14,7 @@ namespace esphome::logger {
static const char *const TAG = "logger";
#ifdef USE_LOGGER_USB_CDC
void Logger::loop() {
void Logger::cdc_loop_() {
if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) {
return;
}

View File

@@ -31,8 +31,8 @@ TaskLogBuffer::~TaskLogBuffer() {
}
}
bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **text, void **received_token) {
if (message == nullptr || text == nullptr || received_token == nullptr) {
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
if (this->current_token_) {
return false;
}
@@ -43,18 +43,19 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **
}
LogMessage *msg = static_cast<LogMessage *>(received_item);
*message = msg;
*text = msg->text_data();
*received_token = received_item;
message = msg;
text_length = msg->text_length;
this->current_token_ = received_item;
return true;
}
void TaskLogBuffer::release_message_main_loop(void *token) {
if (token == nullptr) {
void TaskLogBuffer::release_message_main_loop() {
if (this->current_token_ == nullptr) {
return;
}
vRingbufferReturnItem(ring_buffer_, token);
vRingbufferReturnItem(ring_buffer_, this->current_token_);
this->current_token_ = nullptr;
// Update counter to mark all messages as processed
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
}

View File

@@ -52,10 +52,10 @@ class TaskLogBuffer {
~TaskLogBuffer();
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
bool borrow_message_main_loop(LogMessage **message, const char **text, void **received_token);
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
void release_message_main_loop(void *token);
void release_message_main_loop();
// Thread-safe - send a message to the ring buffer from any thread
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
@@ -78,6 +78,7 @@ class TaskLogBuffer {
// Atomic counter for message tracking (only differences matter)
std::atomic<uint16_t> message_counter_{0}; // Incremented when messages are committed
mutable uint16_t last_processed_counter_{0}; // Tracks last processed message
void *current_token_{nullptr};
};
} // namespace esphome::logger

View File

@@ -10,16 +10,16 @@
namespace esphome::logger {
TaskLogBufferHost::TaskLogBufferHost(size_t slot_count) : slot_count_(slot_count) {
TaskLogBuffer::TaskLogBuffer(size_t slot_count) : slot_count_(slot_count) {
// Allocate message slots
this->slots_ = std::make_unique<LogMessage[]>(slot_count);
}
TaskLogBufferHost::~TaskLogBufferHost() {
TaskLogBuffer::~TaskLogBuffer() {
// unique_ptr handles cleanup automatically
}
int TaskLogBufferHost::acquire_write_slot_() {
int TaskLogBuffer::acquire_write_slot_() {
// Try to reserve a slot using compare-and-swap
size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed);
@@ -43,7 +43,7 @@ int TaskLogBufferHost::acquire_write_slot_() {
}
}
void TaskLogBufferHost::commit_write_slot_(int slot_index) {
void TaskLogBuffer::commit_write_slot_(int slot_index) {
// Mark the slot as ready for reading
this->slots_[slot_index].ready.store(true, std::memory_order_release);
@@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) {
}
}
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args) {
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) {
// Acquire a slot
int slot_index = this->acquire_write_slot_();
if (slot_index < 0) {
@@ -115,11 +115,7 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag,
return true;
}
bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
if (message == nullptr) {
return false;
}
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
size_t current_write = this->write_index_.load(std::memory_order_acquire);
@@ -134,11 +130,12 @@ bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
return false;
}
*message = &msg;
message = &msg;
text_length = msg.text_length;
return true;
}
void TaskLogBufferHost::release_message_main_loop() {
void TaskLogBuffer::release_message_main_loop() {
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
// Clear the ready flag

View File

@@ -21,12 +21,12 @@ namespace esphome::logger {
*
* Threading Model: Multi-Producer Single-Consumer (MPSC)
* - Multiple threads can safely call send_message_thread_safe() concurrently
* - Only the main loop thread calls get_message_main_loop() and release_message_main_loop()
* - Only the main loop thread calls borrow_message_main_loop() and release_message_main_loop()
*
* Producers (multiple threads) Consumer (main loop only)
* │ │
* ▼ ▼
* acquire_write_slot_() get_message_main_loop()
* acquire_write_slot_() bool borrow_message_main_loop()
* CAS on reserve_index_ read write_index_
* │ check ready flag
* ▼ │
@@ -48,7 +48,7 @@ namespace esphome::logger {
* - Atomic CAS for slot reservation allows multiple producers without locks
* - Single consumer (main loop) processes messages in order
*/
class TaskLogBufferHost {
class TaskLogBuffer {
public:
// Default number of message slots - host has plenty of memory
static constexpr size_t DEFAULT_SLOT_COUNT = 64;
@@ -71,15 +71,16 @@ class TaskLogBufferHost {
thread_name[0] = '\0';
text[0] = '\0';
}
inline char *text_data() { return this->text; }
};
/// Constructor that takes the number of message slots
explicit TaskLogBufferHost(size_t slot_count);
~TaskLogBufferHost();
explicit TaskLogBuffer(size_t slot_count);
~TaskLogBuffer();
// NOT thread-safe - get next message from buffer, only call from main loop
// Returns true if a message was retrieved, false if buffer is empty
bool get_message_main_loop(LogMessage **message);
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
// NOT thread-safe - release the message after processing, only call from main loop
void release_message_main_loop();

View File

@@ -8,7 +8,7 @@
namespace esphome::logger {
TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) {
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
this->size_ = total_buffer_size;
// Allocate memory for the circular buffer using ESPHome's RAM allocator
RAMAllocator<uint8_t> allocator;
@@ -17,7 +17,7 @@ TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) {
this->mutex_ = xSemaphoreCreateMutex();
}
TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
TaskLogBuffer::~TaskLogBuffer() {
if (this->mutex_ != nullptr) {
vSemaphoreDelete(this->mutex_);
this->mutex_ = nullptr;
@@ -29,7 +29,7 @@ TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
}
}
size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
size_t TaskLogBuffer::available_contiguous_space() const {
if (this->head_ >= this->tail_) {
// head is ahead of or equal to tail
// Available space is from head to end, plus from start to tail
@@ -47,11 +47,7 @@ size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
}
}
bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, const char **text) {
if (message == nullptr || text == nullptr) {
return false;
}
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
// Check if buffer was initialized successfully
if (this->mutex_ == nullptr || this->storage_ == nullptr) {
return false;
@@ -77,15 +73,15 @@ bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, cons
this->tail_ = 0;
msg = reinterpret_cast<LogMessage *>(this->storage_);
}
*message = msg;
*text = msg->text_data();
message = msg;
text_length = msg->text_length;
this->current_message_size_ = message_total_size(msg->text_length);
// Keep mutex held until release_message_main_loop()
return true;
}
void TaskLogBufferLibreTiny::release_message_main_loop() {
void TaskLogBuffer::release_message_main_loop() {
// Advance tail past the current message
this->tail_ += this->current_message_size_;
@@ -100,8 +96,8 @@ void TaskLogBufferLibreTiny::release_message_main_loop() {
xSemaphoreGive(this->mutex_);
}
bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line,
const char *thread_name, const char *format, va_list args) {
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)
va_list args_copy;
va_copy(args_copy, args);

View File

@@ -40,7 +40,7 @@ namespace esphome::logger {
* - Volatile counter enables fast has_messages() without lock overhead
* - If message doesn't fit at end, padding is added and message wraps to start
*/
class TaskLogBufferLibreTiny {
class TaskLogBuffer {
public:
// Structure for a log message header (text data follows immediately after)
struct LogMessage {
@@ -60,11 +60,11 @@ class TaskLogBufferLibreTiny {
static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF;
// Constructor that takes a total buffer size
explicit TaskLogBufferLibreTiny(size_t total_buffer_size);
~TaskLogBufferLibreTiny();
explicit TaskLogBuffer(size_t total_buffer_size);
~TaskLogBuffer();
// NOT thread-safe - borrow a message from the buffer, only call from main loop
bool borrow_message_main_loop(LogMessage **message, const char **text);
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
// NOT thread-safe - release a message buffer, only call from main loop
void release_message_main_loop();

View File

@@ -0,0 +1,116 @@
#ifdef USE_ZEPHYR
#include "task_log_buffer_zephyr.h"
namespace esphome::logger {
__thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
static inline uint32_t total_size_in_32bit_words(uint16_t text_length) {
// Calculate total size in 32-bit words needed (header + text length + null terminator + 3(4 bytes alignment)
return (sizeof(TaskLogBuffer::LogMessage) + text_length + 1 + 3) / sizeof(uint32_t);
}
static inline uint32_t get_wlen(const mpsc_pbuf_generic *item) {
return total_size_in_32bit_words(reinterpret_cast<const TaskLogBuffer::LogMessage *>(item)->text_length);
}
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
// alignment to 4 bytes
total_buffer_size = (total_buffer_size + 3) / sizeof(uint32_t);
this->mpsc_config_.buf = new uint32_t[total_buffer_size];
this->mpsc_config_.size = total_buffer_size;
this->mpsc_config_.flags = MPSC_PBUF_MODE_OVERWRITE;
this->mpsc_config_.get_wlen = get_wlen,
mpsc_pbuf_init(&this->log_buffer_, &this->mpsc_config_);
}
TaskLogBuffer::~TaskLogBuffer() { delete[] this->mpsc_config_.buf; }
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)
va_list args_copy;
va_copy(args_copy, args);
int ret = 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;
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
// 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);
// Handle unexpected formatting error (ret < 0 is encoding error; ret == 0 is valid empty output)
if (ret < 0) {
// this should not happen, vsnprintf was called already once
// fill with '\n' to not call mpsc_pbuf_free from producer
// it will be trimmed anyway
for (size_t i = 0; i < text_length; ++i) {
text_area[i] = '\n';
}
text_area[text_length] = 0;
// do not return false to free the buffer from main thread
}
msg->text_length = text_length;
mpsc_pbuf_commit(&this->log_buffer_, reinterpret_cast<mpsc_pbuf_generic *>(msg));
return true;
}
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
if (this->current_token_) {
return false;
}
this->current_token_ = mpsc_pbuf_claim(&this->log_buffer_);
if (this->current_token_ == nullptr) {
return false;
}
// 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--;
}
return true;
}
void TaskLogBuffer::release_message_main_loop() {
if (this->current_token_ == nullptr) {
return;
}
mpsc_pbuf_free(&this->log_buffer_, this->current_token_);
this->current_token_ = nullptr;
}
#endif // USE_ESPHOME_TASK_LOG_BUFFER
} // namespace esphome::logger
#endif // USE_ZEPHYR

View File

@@ -0,0 +1,66 @@
#pragma once
#ifdef USE_ZEPHYR
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include <zephyr/sys/mpsc_pbuf.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)
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
class TaskLogBuffer {
public:
// Structure for a log message header (text data follows immediately after)
struct LogMessage {
MPSC_PBUF_HDR; // this is only 2 bits but no more than 30 bits directly after
uint16_t line; // Source code line number
uint8_t level; // Log level (0-7)
#if defined(CONFIG_THREAD_NAME)
char thread_name[CONFIG_THREAD_MAX_NAME_LEN]; // Store thread name directly (only used for non-main threads)
#else
char thread_name[MAX_POINTER_REPRESENTATION]; // Store thread name directly (only used for non-main threads)
#endif
const char *tag; // We store the pointer, assuming tags are static
uint16_t text_length; // Length of the message text (up to ~64KB)
// Methods for accessing message contents
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); }
};
// Constructor that takes a total buffer size
explicit TaskLogBuffer(size_t total_buffer_size);
~TaskLogBuffer();
// 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_); }
// Get the total buffer size in bytes
inline size_t size() const { return this->mpsc_config_.size * sizeof(uint32_t); }
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
void release_message_main_loop();
// Thread-safe - send a message to the ring buffer from any thread
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args);
protected:
mpsc_pbuf_buffer_config mpsc_config_{};
mpsc_pbuf_buffer log_buffer_{};
const mpsc_pbuf_generic *current_token_{};
};
#endif // USE_ESPHOME_TASK_LOG_BUFFER
} // namespace esphome::logger
#endif // USE_ZEPHYR

View File

@@ -25,8 +25,8 @@ static uint8_t
s_flash_storage[RP2040_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Stack buffer size for preferences - covers virtually all real-world preferences without heap allocation
static constexpr size_t PREF_BUFFER_SIZE = 64;
// No preference can exceed the total flash storage, so stack buffer covers all cases.
static constexpr size_t PREF_MAX_BUFFER_SIZE = RP2040_FLASH_STORAGE_SIZE;
extern "C" uint8_t _EEPROM_start;
@@ -46,14 +46,14 @@ class RP2040PreferenceBackend : public ESPPreferenceBackend {
bool save(const uint8_t *data, size_t len) override {
const size_t buffer_size = len + 1;
SmallBufferWithHeapFallback<PREF_BUFFER_SIZE> buffer_alloc(buffer_size);
uint8_t *buffer = buffer_alloc.get();
if (buffer_size > PREF_MAX_BUFFER_SIZE)
return false;
uint8_t buffer[PREF_MAX_BUFFER_SIZE];
memcpy(buffer, data, len);
buffer[len] = calculate_crc(buffer, buffer + len, type);
buffer[len] = calculate_crc(buffer, buffer + len, this->type);
for (size_t i = 0; i < buffer_size; i++) {
uint32_t j = offset + i;
uint32_t j = this->offset + i;
if (j >= RP2040_FLASH_STORAGE_SIZE)
return false;
uint8_t v = buffer[i];
@@ -66,17 +66,18 @@ class RP2040PreferenceBackend : public ESPPreferenceBackend {
}
bool load(uint8_t *data, size_t len) override {
const size_t buffer_size = len + 1;
SmallBufferWithHeapFallback<PREF_BUFFER_SIZE> buffer_alloc(buffer_size);
uint8_t *buffer = buffer_alloc.get();
if (buffer_size > PREF_MAX_BUFFER_SIZE)
return false;
uint8_t buffer[PREF_MAX_BUFFER_SIZE];
for (size_t i = 0; i < buffer_size; i++) {
uint32_t j = offset + i;
uint32_t j = this->offset + i;
if (j >= RP2040_FLASH_STORAGE_SIZE)
return false;
buffer[i] = s_flash_storage[j];
}
uint8_t crc = calculate_crc(buffer, buffer + len, type);
uint8_t crc = calculate_crc(buffer, buffer + len, this->type);
if (buffer[len] != crc) {
return false;
}

View File

@@ -112,10 +112,10 @@ class DeferredUpdateEventSource : public AsyncEventSource {
/*
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for
the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per
entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
because of dedup) would take up only 0.8 kB.
the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two
pointers per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors
publishing because of dedup) would take up only 0.8 kB.
*/
struct DeferredEvent {
friend class DeferredUpdateEventSource;
@@ -130,7 +130,9 @@ class DeferredUpdateEventSource : public AsyncEventSource {
bool operator==(const DeferredEvent &test) const {
return (source_ == test.source_ && message_generator_ == test.message_generator_);
}
} __attribute__((packed));
};
static_assert(sizeof(DeferredEvent) == sizeof(void *) + sizeof(message_generator_t *),
"DeferredEvent should have no padding");
protected:
// surface a couple methods from the base class

View File

@@ -259,9 +259,9 @@ using message_generator_t = std::string(esphome::web_server::WebServer *, void *
/*
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for
the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per
entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two pointers
per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
because of dedup) would take up only 0.8 kB.
*/
struct DeferredEvent {
@@ -277,7 +277,9 @@ struct DeferredEvent {
bool operator==(const DeferredEvent &test) const {
return (source_ == test.source_ && message_generator_ == test.message_generator_);
}
} __attribute__((packed));
};
static_assert(sizeof(DeferredEvent) == sizeof(void *) + sizeof(message_generator_t *),
"DeferredEvent should have no padding");
class AsyncEventSourceResponse {
friend class AsyncEventSource;

View File

@@ -14,6 +14,7 @@
#define ESPHOME_PROJECT_VERSION_30 "v2"
#define ESPHOME_VARIANT "ESP32"
#define ESPHOME_DEBUG_SCHEDULER
#define ESPHOME_DEBUG_API
// Default threading model for static analysis (ESP32 is multi-threaded with atomics)
#define ESPHOME_THREAD_MULTI_ATOMICS
@@ -320,6 +321,7 @@
#endif
#ifdef USE_NRF52
#define USE_ESPHOME_TASK_LOG_BUFFER
#define USE_NRF52_DFU
#define USE_NRF52_REG0_VOUT 5
#define USE_NRF52_UICR_ERASE

View File

@@ -5,3 +5,4 @@ esphome:
logger:
level: DEBUG
task_log_buffer_size: 0

View File

@@ -197,6 +197,7 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s
" platformio_options:\n"
" build_flags:\n"
' - "-DDEBUG" # Enable assert() statements\n'
' - "-DESPHOME_DEBUG_API" # Enable API protocol asserts\n'
' - "-g" # Add debug symbols',
)