From 584c169e4cc7baf68123d501a9b320446124da6c Mon Sep 17 00:00:00 2001 From: kbx81 Date: Thu, 15 Jan 2026 03:36:48 -0600 Subject: [PATCH 1/4] [infrared, remote_base] Optimize IR transmit path for `web_server` base64 data --- esphome/components/infrared/infrared.cpp | 24 +++++++++++++++++-- esphome/components/infrared/infrared.h | 17 ++++++++++--- .../components/remote_base/remote_base.cpp | 10 ++++++++ esphome/components/remote_base/remote_base.h | 4 ++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index 5f8d63926a..a78d3812a7 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->base64_data_.clear(); + return *this; +} + +InfraredCall &InfraredCall::set_raw_timings_base64(std::string &&base64) { + this->base64_data_ = std::move(base64); + 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->base64_data_.clear(); return *this; } @@ -92,6 +101,17 @@ 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_base64()) { + // Decode base64 and parse directly into transmit buffer + constexpr size_t max_ir_bytes = 1024; + uint8_t decoded[max_ir_bytes]; + size_t decoded_len = base64_decode(call.get_base64_data(), decoded, sizeof(decoded)); + if (decoded_len == 0 || decoded_len % 4 != 0) { + ESP_LOGE(TAG, "Invalid base64 data"); + return; + } + transmit_data->set_data_from_le_int32_buffer(decoded, decoded_len); + ESP_LOGD(TAG, "Transmitting base64 raw timings: count=%zu, repeat=%u", decoded_len / 4, 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 3a891301f4..98fbbec7e6 100644 --- a/esphome/components/infrared/infrared.h +++ b/esphome/components/infrared/infrared.h @@ -31,6 +31,9 @@ class InfraredCall { /// Set the raw timings (positive = mark, negative = space) /// Note: The timings vector must outlive the InfraredCall (zero-copy reference) InfraredCall &set_raw_timings(const std::vector &timings); + /// Set the raw timings from base64-encoded little-endian int32 data + /// The base64 string is stored and decoded directly into transmit buffer at perform() time + InfraredCall &set_raw_timings_base64(std::string &&base64); /// Set the raw timings from packed protobuf sint32 data (zero-copy from wire) /// Note: The data must outlive the InfraredCall InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count); @@ -42,12 +45,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 base64) 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 base64) + bool has_raw_timings() const { + return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || !this->base64_data_.empty(); + } /// Check if using packed data format bool is_packed() const { return this->packed_data_ != nullptr; } + /// Check if using base64 data format + bool is_base64() const { return !this->base64_data_.empty(); } + /// Get the base64 data string + const std::string &get_base64_data() const { return this->base64_data_; } /// 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_; } @@ -61,6 +70,8 @@ class InfraredCall { optional carrier_frequency_; // Vector-based timings (for lambdas/automations) const std::vector *raw_timings_{nullptr}; + // Base64-encoded data (for web_server - decoded directly into transmit buffer) + std::string base64_data_; // Packed protobuf timings (for API zero-copy) const uint8_t *packed_data_{nullptr}; uint16_t packed_length_{0}; diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 2f1c107bf4..a5c2ad94aa 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -159,6 +159,16 @@ void RemoteTransmitData::set_data_from_packed_sint32(const uint8_t *data, size_t } } +void RemoteTransmitData::set_data_from_le_int32_buffer(const uint8_t *data, size_t len) { + this->data_.clear(); + this->data_.reserve(len / 4); + // Parse little-endian int32 values + for (size_t i = 0; i + 3 < len; i += 4) { + int32_t timing = static_cast(encode_uint32(data[i + 3], data[i + 2], data[i + 1], data[i])); + this->data_.push_back(timing); + } +} + /* 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 a11e0271af..f66caa06b4 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -36,6 +36,10 @@ 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 little-endian int32 buffer (e.g., decoded from base64) + /// @param data Pointer to little-endian int32 values + /// @param len Length of the buffer in bytes (must be multiple of 4) + void set_data_from_le_int32_buffer(const uint8_t *data, size_t len); void reset() { this->data_.clear(); this->carrier_frequency_ = 0; From b2940490fb48681ce8855057df44d04ef12439fa Mon Sep 17 00:00:00 2001 From: kbx81 Date: Thu, 15 Jan 2026 18:05:23 -0600 Subject: [PATCH 2/4] optimize --- esphome/components/infrared/infrared.cpp | 17 +++++++---------- esphome/components/infrared/infrared.h | 15 ++++++++------- esphome/components/remote_base/remote_base.cpp | 12 ++++++++++++ esphome/components/remote_base/remote_base.h | 5 +++++ 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index a78d3812a7..982f875e2b 100644 --- a/esphome/components/infrared/infrared.cpp +++ b/esphome/components/infrared/infrared.cpp @@ -19,12 +19,12 @@ 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; - this->base64_data_.clear(); + this->base64_ptr_ = nullptr; return *this; } -InfraredCall &InfraredCall::set_raw_timings_base64(std::string &&base64) { - this->base64_data_ = std::move(base64); +InfraredCall &InfraredCall::set_raw_timings_base64(const std::string &base64) { + this->base64_ptr_ = &base64; this->raw_timings_ = nullptr; this->packed_data_ = nullptr; return *this; @@ -35,7 +35,7 @@ InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t this->packed_length_ = length; this->packed_count_ = count; this->raw_timings_ = nullptr; - this->base64_data_.clear(); + this->base64_ptr_ = nullptr; return *this; } @@ -103,15 +103,12 @@ void Infrared::control(const InfraredCall &call) { call.get_repeat_count()); } else if (call.is_base64()) { // Decode base64 and parse directly into transmit buffer - constexpr size_t max_ir_bytes = 1024; - uint8_t decoded[max_ir_bytes]; - size_t decoded_len = base64_decode(call.get_base64_data(), decoded, sizeof(decoded)); - if (decoded_len == 0 || decoded_len % 4 != 0) { + if (!transmit_data->set_data_from_base64_le_int32(call.get_base64_data())) { ESP_LOGE(TAG, "Invalid base64 data"); return; } - transmit_data->set_data_from_le_int32_buffer(decoded, decoded_len); - ESP_LOGD(TAG, "Transmitting base64 raw timings: count=%zu, repeat=%u", decoded_len / 4, call.get_repeat_count()); + ESP_LOGD(TAG, "Transmitting base64 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 98fbbec7e6..932db41ef8 100644 --- a/esphome/components/infrared/infrared.h +++ b/esphome/components/infrared/infrared.h @@ -32,8 +32,9 @@ class InfraredCall { /// Note: The timings vector must outlive the InfraredCall (zero-copy reference) InfraredCall &set_raw_timings(const std::vector &timings); /// Set the raw timings from base64-encoded little-endian int32 data - /// The base64 string is stored and decoded directly into transmit buffer at perform() time - InfraredCall &set_raw_timings_base64(std::string &&base64); + /// Note: The string must outlive the InfraredCall (zero-copy pointer) + /// Decoded directly into transmit buffer at perform() time + InfraredCall &set_raw_timings_base64(const std::string &base64); /// Set the raw timings from packed protobuf sint32 data (zero-copy from wire) /// Note: The data must outlive the InfraredCall InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count); @@ -49,14 +50,14 @@ class InfraredCall { const std::vector &get_raw_timings() const { return *this->raw_timings_; } /// Check if raw timings have been set (vector, packed, or base64) bool has_raw_timings() const { - return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || !this->base64_data_.empty(); + return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64_ptr_ != nullptr; } /// Check if using packed data format bool is_packed() const { return this->packed_data_ != nullptr; } /// Check if using base64 data format - bool is_base64() const { return !this->base64_data_.empty(); } + bool is_base64() const { return this->base64_ptr_ != nullptr; } /// Get the base64 data string - const std::string &get_base64_data() const { return this->base64_data_; } + const std::string &get_base64_data() const { return *this->base64_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_; } @@ -70,8 +71,8 @@ class InfraredCall { optional carrier_frequency_; // Vector-based timings (for lambdas/automations) const std::vector *raw_timings_{nullptr}; - // Base64-encoded data (for web_server - decoded directly into transmit buffer) - std::string base64_data_; + // Base64-encoded data pointer (for web_server - decoded directly into transmit buffer) + const std::string *base64_ptr_{nullptr}; // Packed protobuf timings (for API zero-copy) const uint8_t *packed_data_{nullptr}; uint16_t packed_length_{0}; diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index a5c2ad94aa..43699a548c 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -169,6 +169,18 @@ void RemoteTransmitData::set_data_from_le_int32_buffer(const uint8_t *data, size } } +bool RemoteTransmitData::set_data_from_base64_le_int32(const std::string &base64) { + // Decode base64 into stack buffer, then parse into data_ + constexpr size_t max_ir_bytes = 1024; + uint8_t decoded[max_ir_bytes]; + size_t decoded_len = base64_decode(base64, decoded, sizeof(decoded)); + if (decoded_len == 0 || decoded_len % 4 != 0) { + return false; + } + this->set_data_from_le_int32_buffer(decoded, decoded_len); + return true; +} + /* 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 f66caa06b4..654cacdc8f 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -40,6 +40,11 @@ class RemoteTransmitData { /// @param data Pointer to little-endian int32 values /// @param len Length of the buffer in bytes (must be multiple of 4) void set_data_from_le_int32_buffer(const uint8_t *data, size_t len); + /// Set data from base64-encoded little-endian int32 values + /// Decodes and parses directly into internal buffer (zero intermediate copies) + /// @param base64 Base64-encoded string of little-endian int32 values + /// @return true if successful, false if decode failed or invalid size + bool set_data_from_base64_le_int32(const std::string &base64); void reset() { this->data_.clear(); this->carrier_frequency_ = 0; From 2eabc1b96b839c4afc48a3d23441ebfcbff0187d Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 15 Jan 2026 20:22:05 -0600 Subject: [PATCH 3/4] [helpers] Add base85 support (#13254) Co-authored-by: J. Nick Koston --- esphome/core/helpers.cpp | 49 ++++++++++++++++++++++++++++++++++++++++ esphome/core/helpers.h | 8 +++++++ 2 files changed, 57 insertions(+) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 309407fbec..b5bf849c30 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -617,6 +617,55 @@ std::vector base64_decode(const std::string &encoded_string) { return ret; } +/// Encode int32 to 5 base85 characters + null terminator +/// Standard ASCII85 alphabet: '!' (33) = 0 through 'u' (117) = 84 +inline void base85_encode_int32(int32_t value, std::span output) { + uint32_t v = static_cast(value); + // Encode least significant digit first, then reverse + for (int i = 4; i >= 0; i--) { + output[i] = static_cast('!' + (v % 85)); + v /= 85; + } + output[5] = '\0'; +} + +/// Decode 5 base85 characters to int32 +inline bool base85_decode_int32(const char *input, int32_t &out) { + uint8_t c0 = static_cast(input[0] - '!'); + uint8_t c1 = static_cast(input[1] - '!'); + uint8_t c2 = static_cast(input[2] - '!'); + uint8_t c3 = static_cast(input[3] - '!'); + uint8_t c4 = static_cast(input[4] - '!'); + + // Each digit must be 0-84. Since uint8_t wraps, chars below '!' become > 84 + if (c0 > 84 || c1 > 84 || c2 > 84 || c3 > 84 || c4 > 84) + return false; + + // 85^4 = 52200625, 85^3 = 614125, 85^2 = 7225, 85^1 = 85 + out = static_cast(c0 * 52200625u + c1 * 614125u + c2 * 7225u + c3 * 85u + c4); + return true; +} + +/// Decode base85 string directly into vector (no intermediate buffer) +bool base85_decode_int32_vector(const std::string &base85, std::vector &out) { + size_t len = base85.size(); + if (len % 5 != 0) + return false; + + out.clear(); + const char *ptr = base85.data(); + const char *end = ptr + len; + + while (ptr < end) { + int32_t value; + if (!base85_decode_int32(ptr, value)) + return false; + out.push_back(value); + ptr += 5; + } + return true; +} + // Colors float gamma_correct(float value, float gamma) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 2e9c0e6b13..d5a04b7eb1 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1086,6 +1086,14 @@ std::vector base64_decode(const std::string &encoded_string); size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len); size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len); +/// Size of buffer needed for base85 encoded int32 (5 chars + null terminator) +static constexpr size_t BASE85_INT32_ENCODED_SIZE = 6; + +void base85_encode_int32(int32_t value, std::span output); + +bool base85_decode_int32(const char *input, int32_t &out); +bool base85_decode_int32_vector(const std::string &base85, std::vector &out); + ///@} /// @name Colors From 6f4019687b7e30b3b33424016c099d52c6e421cd Mon Sep 17 00:00:00 2001 From: kbx81 Date: Thu, 15 Jan 2026 20:53:00 -0600 Subject: [PATCH 4/4] base85 --- esphome/components/infrared/infrared.cpp | 18 +++++------ esphome/components/infrared/infrared.h | 22 ++++++------- .../components/remote_base/remote_base.cpp | 31 +++++++++---------- esphome/components/remote_base/remote_base.h | 12 +++---- 4 files changed, 38 insertions(+), 45 deletions(-) diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index 982f875e2b..294d69e523 100644 --- a/esphome/components/infrared/infrared.cpp +++ b/esphome/components/infrared/infrared.cpp @@ -19,12 +19,12 @@ 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; - this->base64_ptr_ = nullptr; + this->base85_ptr_ = nullptr; return *this; } -InfraredCall &InfraredCall::set_raw_timings_base64(const std::string &base64) { - this->base64_ptr_ = &base64; +InfraredCall &InfraredCall::set_raw_timings_base85(const std::string &base85) { + this->base85_ptr_ = &base85; this->raw_timings_ = nullptr; this->packed_data_ = nullptr; return *this; @@ -35,7 +35,7 @@ InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t this->packed_length_ = length; this->packed_count_ = count; this->raw_timings_ = nullptr; - this->base64_ptr_ = nullptr; + this->base85_ptr_ = nullptr; return *this; } @@ -101,13 +101,13 @@ 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_base64()) { - // Decode base64 and parse directly into transmit buffer - if (!transmit_data->set_data_from_base64_le_int32(call.get_base64_data())) { - ESP_LOGE(TAG, "Invalid base64 data"); + } 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 base64 raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(), + 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) diff --git a/esphome/components/infrared/infrared.h b/esphome/components/infrared/infrared.h index 932db41ef8..12feb2c692 100644 --- a/esphome/components/infrared/infrared.h +++ b/esphome/components/infrared/infrared.h @@ -31,10 +31,10 @@ class InfraredCall { /// Set the raw timings (positive = mark, negative = space) /// Note: The timings vector must outlive the InfraredCall (zero-copy reference) InfraredCall &set_raw_timings(const std::vector &timings); - /// Set the raw timings from base64-encoded little-endian int32 data + /// Set the raw timings from base85-encoded int32 data /// Note: The string must outlive the InfraredCall (zero-copy pointer) /// Decoded directly into transmit buffer at perform() time - InfraredCall &set_raw_timings_base64(const std::string &base64); + InfraredCall &set_raw_timings_base85(const std::string &base85); /// Set the raw timings from packed protobuf sint32 data (zero-copy from wire) /// Note: The data must outlive the InfraredCall InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count); @@ -46,18 +46,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 or base64) + /// 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 (vector, packed, or base64) + /// 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->base64_ptr_ != nullptr; + 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 base64 data format - bool is_base64() const { return this->base64_ptr_ != nullptr; } - /// Get the base64 data string - const std::string &get_base64_data() const { return *this->base64_ptr_; } + /// 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_; } @@ -71,8 +71,8 @@ class InfraredCall { optional carrier_frequency_; // Vector-based timings (for lambdas/automations) const std::vector *raw_timings_{nullptr}; - // Base64-encoded data pointer (for web_server - decoded directly into transmit buffer) - const std::string *base64_ptr_{nullptr}; + // Base85-encoded data pointer (for web_server - decoded directly into transmit buffer) + const std::string *base85_ptr_{nullptr}; // Packed protobuf timings (for API zero-copy) const uint8_t *packed_data_{nullptr}; uint16_t packed_length_{0}; diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 43699a548c..b721cfd39a 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -159,25 +159,22 @@ void RemoteTransmitData::set_data_from_packed_sint32(const uint8_t *data, size_t } } -void RemoteTransmitData::set_data_from_le_int32_buffer(const uint8_t *data, size_t len) { - this->data_.clear(); - this->data_.reserve(len / 4); - // Parse little-endian int32 values - for (size_t i = 0; i + 3 < len; i += 4) { - int32_t timing = static_cast(encode_uint32(data[i + 3], data[i + 2], data[i + 1], data[i])); - this->data_.push_back(timing); - } -} - -bool RemoteTransmitData::set_data_from_base64_le_int32(const std::string &base64) { - // Decode base64 into stack buffer, then parse into data_ - constexpr size_t max_ir_bytes = 1024; - uint8_t decoded[max_ir_bytes]; - size_t decoded_len = base64_decode(base64, decoded, sizeof(decoded)); - if (decoded_len == 0 || decoded_len % 4 != 0) { +bool RemoteTransmitData::set_data_from_base85(const std::string &base85) { + size_t len = base85.size(); + if (len % 5 != 0) return false; + + this->data_.clear(); // Retains capacity! + const char *ptr = base85.data(); + const char *end = ptr + len; + + while (ptr < end) { + int32_t value; + if (!base85_decode_int32(ptr, value)) + return false; + this->data_.push_back(value); + ptr += 5; } - this->set_data_from_le_int32_buffer(decoded, decoded_len); return true; } diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 654cacdc8f..2d7642cc31 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -36,15 +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 little-endian int32 buffer (e.g., decoded from base64) - /// @param data Pointer to little-endian int32 values - /// @param len Length of the buffer in bytes (must be multiple of 4) - void set_data_from_le_int32_buffer(const uint8_t *data, size_t len); - /// Set data from base64-encoded little-endian int32 values - /// Decodes and parses directly into internal buffer (zero intermediate copies) - /// @param base64 Base64-encoded string of little-endian int32 values + /// 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_base64_le_int32(const std::string &base64); + bool set_data_from_base85(const std::string &base85); void reset() { this->data_.clear(); this->carrier_frequency_ = 0;