From 14b7539094833cd5fab4e09852cce72639399dad Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 15 Jan 2026 22:04:21 -0600 Subject: [PATCH 1/2] [infrared, remote_base] Optimize IR transmit path for `web_server` base85 data (#13238) --- esphome/components/infrared/infrared.cpp | 21 ++++++++- esphome/components/infrared/infrared.h | 43 +++++++++++++++---- .../components/remote_base/remote_base.cpp | 4 ++ esphome/components/remote_base/remote_base.h | 5 +++ 4 files changed, 62 insertions(+), 11 deletions(-) diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index 5f8d63926a9..294d69e5233 100644 --- a/esphome/components/infrared/infrared.cpp +++ b/esphome/components/infrared/infrared.cpp @@ -18,7 +18,15 @@ InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) { InfraredCall &InfraredCall::set_raw_timings(const std::vector &timings) { this->raw_timings_ = &timings; - this->packed_data_ = nullptr; // Clear packed if vector is set + this->packed_data_ = nullptr; + this->base85_ptr_ = nullptr; + return *this; +} + +InfraredCall &InfraredCall::set_raw_timings_base85(const std::string &base85) { + this->base85_ptr_ = &base85; + this->raw_timings_ = nullptr; + this->packed_data_ = nullptr; return *this; } @@ -26,7 +34,8 @@ InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t this->packed_data_ = data; this->packed_length_ = length; this->packed_count_ = count; - this->raw_timings_ = nullptr; // Clear vector if packed is set + this->raw_timings_ = nullptr; + this->base85_ptr_ = nullptr; return *this; } @@ -92,6 +101,14 @@ void Infrared::control(const InfraredCall &call) { call.get_packed_count()); ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(), call.get_repeat_count()); + } else if (call.is_base85()) { + // Decode base85 directly into transmit buffer (zero heap allocations) + if (!transmit_data->set_data_from_base85(call.get_base85_data())) { + ESP_LOGE(TAG, "Invalid base85 data"); + return; + } + ESP_LOGD(TAG, "Transmitting base85 raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(), + call.get_repeat_count()); } else { // From vector (lambdas/automations) transmit_data->set_data(call.get_raw_timings()); diff --git a/esphome/components/infrared/infrared.h b/esphome/components/infrared/infrared.h index 3a891301f4d..ba426c9daa5 100644 --- a/esphome/components/infrared/infrared.h +++ b/esphome/components/infrared/infrared.h @@ -28,12 +28,29 @@ class InfraredCall { /// Set the carrier frequency in Hz InfraredCall &set_carrier_frequency(uint32_t frequency); - /// Set the raw timings (positive = mark, negative = space) - /// Note: The timings vector must outlive the InfraredCall (zero-copy reference) + + // ===== Raw Timings Methods ===== + // All set_raw_timings_* methods store pointers/references to external data. + // The referenced data must remain valid until perform() completes. + // Safe pattern: call.set_raw_timings_xxx(data); call.perform(); // synchronous + // Unsafe pattern: call.set_raw_timings_xxx(data); defer([call]() { call.perform(); }); // data may be gone! + + /// Set the raw timings from a vector (positive = mark, negative = space) + /// @note Lifetime: Stores a pointer to the vector. The vector must outlive perform(). + /// @note Usage: Primarily for lambdas/automations where the vector is in scope. InfraredCall &set_raw_timings(const std::vector &timings); - /// Set the raw timings from packed protobuf sint32 data (zero-copy from wire) - /// Note: The data must outlive the InfraredCall + + /// Set the raw timings from base85-encoded int32 data + /// @note Lifetime: Stores a pointer to the string. The string must outlive perform(). + /// @note Usage: For web_server where the encoded string is on the stack. + /// @note Decoding happens at perform() time, directly into the transmit buffer. + InfraredCall &set_raw_timings_base85(const std::string &base85); + + /// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded) + /// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform(). + /// @note Usage: For API component where data comes directly from the protobuf message. InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count); + /// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.) InfraredCall &set_repeat_count(uint32_t count); @@ -42,12 +59,18 @@ class InfraredCall { /// Get the carrier frequency const optional &get_carrier_frequency() const { return this->carrier_frequency_; } - /// Get the raw timings (only valid if set via set_raw_timings, not packed) + /// Get the raw timings (only valid if set via set_raw_timings, not packed or base85) const std::vector &get_raw_timings() const { return *this->raw_timings_; } - /// Check if raw timings have been set (either vector or packed) - bool has_raw_timings() const { return this->raw_timings_ != nullptr || this->packed_data_ != nullptr; } + /// Check if raw timings have been set (vector, packed, or base85) + bool has_raw_timings() const { + return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base85_ptr_ != nullptr; + } /// Check if using packed data format bool is_packed() const { return this->packed_data_ != nullptr; } + /// Check if using base85 data format + bool is_base85() const { return this->base85_ptr_ != nullptr; } + /// Get the base85 data string + const std::string &get_base85_data() const { return *this->base85_ptr_; } /// Get packed data (only valid if set via set_raw_timings_packed) const uint8_t *get_packed_data() const { return this->packed_data_; } uint16_t get_packed_length() const { return this->packed_length_; } @@ -59,9 +82,11 @@ class InfraredCall { uint32_t repeat_count_{1}; Infrared *parent_; optional carrier_frequency_; - // Vector-based timings (for lambdas/automations) + // Pointer to vector-based timings (caller-owned, must outlive perform()) const std::vector *raw_timings_{nullptr}; - // Packed protobuf timings (for API zero-copy) + // Pointer to base85-encoded string (caller-owned, must outlive perform()) + const std::string *base85_ptr_{nullptr}; + // Pointer to packed protobuf buffer (caller-owned, must outlive perform()) const uint8_t *packed_data_{nullptr}; uint16_t packed_length_{0}; uint16_t packed_count_{0}; diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 2f1c107bf4c..e3d9463243b 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -159,6 +159,10 @@ void RemoteTransmitData::set_data_from_packed_sint32(const uint8_t *data, size_t } } +bool RemoteTransmitData::set_data_from_base85(const std::string &base85) { + return base85_decode_int32_vector(base85, this->data_); +} + /* RemoteTransmitterBase */ void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) { diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index a11e0271aff..2d7642cc31b 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -36,6 +36,11 @@ class RemoteTransmitData { /// @param len Length of the buffer in bytes /// @param count Number of values (for reserve optimization) void set_data_from_packed_sint32(const uint8_t *data, size_t len, size_t count); + /// Set data from base85-encoded int32 values + /// Decodes directly into internal buffer (zero heap allocations) + /// @param base85 Base85-encoded string (5 chars per int32 value) + /// @return true if successful, false if decode failed or invalid size + bool set_data_from_base85(const std::string &base85); void reset() { this->data_.clear(); this->carrier_frequency_ = 0; From 8263a8273ff034a2f8c6684914c6da6906a45001 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Jan 2026 18:08:26 -1000 Subject: [PATCH 2/2] [debug] Add min_free heap sensor for ESP32 and LibreTiny, add fragmentation for ESP32 (#13231) --- esphome/components/debug/debug_component.h | 10 ++++-- esphome/components/debug/debug_esp32.cpp | 13 +++++++- esphome/components/debug/debug_libretiny.cpp | 3 ++ esphome/components/debug/sensor.py | 31 +++++++++++++++++-- tests/components/debug/common.yaml | 2 ++ tests/components/debug/test.bk72xx-ard.yaml | 5 +++ tests/components/debug/test.esp32-ard.yaml | 7 +++++ tests/components/debug/test.esp32-idf.yaml | 4 +++ tests/components/debug/test.esp32-s2-idf.yaml | 7 +++++ tests/components/debug/test.esp8266-ard.yaml | 5 +++ tests/components/debug/test.ln882x-ard.yaml | 5 +++ tests/components/debug/test.rtl87xx-ard.yaml | 6 ++++ 12 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 tests/components/debug/test.rtl87xx-ard.yaml diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index 5783bc54183..6cf52d890c1 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -74,8 +74,11 @@ class DebugComponent : public PollingComponent { #ifdef USE_SENSOR void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; } void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; } -#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32) void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; } +#endif +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + void set_min_free_sensor(sensor::Sensor *min_free_sensor) { min_free_sensor_ = min_free_sensor; } #endif void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; } #ifdef USE_ESP32 @@ -97,8 +100,11 @@ class DebugComponent : public PollingComponent { sensor::Sensor *free_sensor_{nullptr}; sensor::Sensor *block_sensor_{nullptr}; -#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32) sensor::Sensor *fragmentation_sensor_{nullptr}; +#endif +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + sensor::Sensor *min_free_sensor_{nullptr}; #endif sensor::Sensor *loop_time_sensor_{nullptr}; #ifdef USE_ESP32 diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index ebb6abf4da7..8c41011f7d9 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -234,8 +234,19 @@ size_t DebugComponent::get_device_info_(std::span void DebugComponent::update_platform_() { #ifdef USE_SENSOR + uint32_t max_alloc = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); if (this->block_sensor_ != nullptr) { - this->block_sensor_->publish_state(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)); + this->block_sensor_->publish_state(max_alloc); + } + if (this->min_free_sensor_ != nullptr) { + this->min_free_sensor_->publish_state(heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL)); + } + if (this->fragmentation_sensor_ != nullptr) { + uint32_t free_heap = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); + if (free_heap > 0) { + float fragmentation = 100.0f - (100.0f * max_alloc / free_heap); + this->fragmentation_sensor_->publish_state(fragmentation); + } } if (this->psram_sensor_ != nullptr) { this->psram_sensor_->publish_state(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index 4f07a4cc179..aae27c8ca26 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -51,6 +51,9 @@ void DebugComponent::update_platform_() { if (this->block_sensor_ != nullptr) { this->block_sensor_->publish_state(lt_heap_get_max_alloc()); } + if (this->min_free_sensor_ != nullptr) { + this->min_free_sensor_->publish_state(lt_heap_get_min_free()); + } #endif } diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py index 6a8e2cd828c..0a716d666e7 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -11,6 +11,9 @@ from esphome.const import ( ENTITY_CATEGORY_DIAGNOSTIC, ICON_COUNTER, ICON_TIMER, + PLATFORM_BK72XX, + PLATFORM_LN882X, + PLATFORM_RTL87XX, UNIT_BYTES, UNIT_HERTZ, UNIT_MILLISECOND, @@ -25,6 +28,7 @@ from . import ( # noqa: F401 pylint: disable=unused-import DEPENDENCIES = ["debug"] +CONF_MIN_FREE = "min_free" CONF_PSRAM = "psram" CONFIG_SCHEMA = { @@ -42,8 +46,14 @@ CONFIG_SCHEMA = { entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_FRAGMENTATION): cv.All( - cv.only_on_esp8266, - cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)), + cv.Any( + cv.All( + cv.only_on_esp8266, + cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)), + ), + cv.only_on_esp32, + msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32", + ), sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, icon=ICON_COUNTER, @@ -51,6 +61,19 @@ CONFIG_SCHEMA = { entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ), + cv.Optional(CONF_MIN_FREE): cv.All( + cv.Any( + cv.only_on_esp32, + cv.only_on([PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX]), + msg="This feature is only available on ESP32 and LibreTiny (BK72xx, LN882x, RTL87xx)", + ), + sensor.sensor_schema( + unit_of_measurement=UNIT_BYTES, + icon=ICON_COUNTER, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + ), cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema( unit_of_measurement=UNIT_MILLISECOND, icon=ICON_TIMER, @@ -93,6 +116,10 @@ async def to_code(config): sens = await sensor.new_sensor(fragmentation_conf) cg.add(debug_component.set_fragmentation_sensor(sens)) + if min_free_conf := config.get(CONF_MIN_FREE): + sens = await sensor.new_sensor(min_free_conf) + cg.add(debug_component.set_min_free_sensor(sens)) + if loop_time_conf := config.get(CONF_LOOP_TIME): sens = await sensor.new_sensor(loop_time_conf) cg.add(debug_component.set_loop_time_sensor(sens)) diff --git a/tests/components/debug/common.yaml b/tests/components/debug/common.yaml index d9a61f8df0f..59ba39c3a44 100644 --- a/tests/components/debug/common.yaml +++ b/tests/components/debug/common.yaml @@ -11,6 +11,8 @@ sensor: - platform: debug free: name: "Heap Free" + block: + name: "Heap Block" loop_time: name: "Loop Time" cpu_frequency: diff --git a/tests/components/debug/test.bk72xx-ard.yaml b/tests/components/debug/test.bk72xx-ard.yaml index dade44d145b..fdae374788f 100644 --- a/tests/components/debug/test.bk72xx-ard.yaml +++ b/tests/components/debug/test.bk72xx-ard.yaml @@ -1 +1,6 @@ <<: !include common.yaml + +sensor: + - platform: debug + min_free: + name: "Heap Min Free" diff --git a/tests/components/debug/test.esp32-ard.yaml b/tests/components/debug/test.esp32-ard.yaml index 8e19a4d6277..8f93b0925eb 100644 --- a/tests/components/debug/test.esp32-ard.yaml +++ b/tests/components/debug/test.esp32-ard.yaml @@ -2,3 +2,10 @@ esp32: cpu_frequency: 240MHz + +sensor: + - platform: debug + fragmentation: + name: "Heap Fragmentation" + min_free: + name: "Heap Min Free" diff --git a/tests/components/debug/test.esp32-idf.yaml b/tests/components/debug/test.esp32-idf.yaml index f7483a54b3b..6a9996ad065 100644 --- a/tests/components/debug/test.esp32-idf.yaml +++ b/tests/components/debug/test.esp32-idf.yaml @@ -9,5 +9,9 @@ sensor: name: "Heap Free" psram: name: "Free PSRAM" + fragmentation: + name: "Heap Fragmentation" + min_free: + name: "Heap Min Free" psram: diff --git a/tests/components/debug/test.esp32-s2-idf.yaml b/tests/components/debug/test.esp32-s2-idf.yaml index dade44d145b..80919b0bab8 100644 --- a/tests/components/debug/test.esp32-s2-idf.yaml +++ b/tests/components/debug/test.esp32-s2-idf.yaml @@ -1 +1,8 @@ <<: !include common.yaml + +sensor: + - platform: debug + fragmentation: + name: "Heap Fragmentation" + min_free: + name: "Heap Min Free" diff --git a/tests/components/debug/test.esp8266-ard.yaml b/tests/components/debug/test.esp8266-ard.yaml index dade44d145b..1398087bf06 100644 --- a/tests/components/debug/test.esp8266-ard.yaml +++ b/tests/components/debug/test.esp8266-ard.yaml @@ -1 +1,6 @@ <<: !include common.yaml + +sensor: + - platform: debug + fragmentation: + name: "Heap Fragmentation" diff --git a/tests/components/debug/test.ln882x-ard.yaml b/tests/components/debug/test.ln882x-ard.yaml index dade44d145b..fdae374788f 100644 --- a/tests/components/debug/test.ln882x-ard.yaml +++ b/tests/components/debug/test.ln882x-ard.yaml @@ -1 +1,6 @@ <<: !include common.yaml + +sensor: + - platform: debug + min_free: + name: "Heap Min Free" diff --git a/tests/components/debug/test.rtl87xx-ard.yaml b/tests/components/debug/test.rtl87xx-ard.yaml new file mode 100644 index 00000000000..fdae374788f --- /dev/null +++ b/tests/components/debug/test.rtl87xx-ard.yaml @@ -0,0 +1,6 @@ +<<: !include common.yaml + +sensor: + - platform: debug + min_free: + name: "Heap Min Free"