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; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index cf984ea2472..0e71d822333 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -658,6 +658,24 @@ std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std #endif #ifdef USE_SWITCH +enum SwitchAction : uint8_t { SWITCH_ACTION_NONE, SWITCH_ACTION_TOGGLE, SWITCH_ACTION_TURN_ON, SWITCH_ACTION_TURN_OFF }; + +static void execute_switch_action(switch_::Switch *obj, SwitchAction action) { + switch (action) { + case SWITCH_ACTION_TOGGLE: + obj->toggle(); + break; + case SWITCH_ACTION_TURN_ON: + obj->turn_on(); + break; + case SWITCH_ACTION_TURN_OFF: + obj->turn_off(); + break; + default: + break; + } +} + void WebServer::on_switch_update(switch_::Switch *obj) { if (!this->include_internal_ && obj->is_internal()) return; @@ -676,34 +694,22 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM return; } - // Handle action methods with single defer and response - enum SwitchAction { NONE, TOGGLE, TURN_ON, TURN_OFF }; - SwitchAction action = NONE; + SwitchAction action = SWITCH_ACTION_NONE; if (match.method_equals(ESPHOME_F("toggle"))) { - action = TOGGLE; + action = SWITCH_ACTION_TOGGLE; } else if (match.method_equals(ESPHOME_F("turn_on"))) { - action = TURN_ON; + action = SWITCH_ACTION_TURN_ON; } else if (match.method_equals(ESPHOME_F("turn_off"))) { - action = TURN_OFF; + action = SWITCH_ACTION_TURN_OFF; } - if (action != NONE) { - this->defer([obj, action]() { - switch (action) { - case TOGGLE: - obj->toggle(); - break; - case TURN_ON: - obj->turn_on(); - break; - case TURN_OFF: - obj->turn_off(); - break; - default: - break; - } - }); + if (action != SWITCH_ACTION_NONE) { +#ifdef USE_ESP8266 + execute_switch_action(obj, action); +#else + this->defer([obj, action]() { execute_switch_action(obj, action); }); +#endif request->send(200); } else { request->send(404); @@ -743,7 +749,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM std::string data = this->button_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("press"))) { - this->defer([obj]() { obj->press(); }); + DEFER_ACTION(obj, obj->press()); request->send(200); return; } else { @@ -828,7 +834,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc std::string data = this->fan_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("toggle"))) { - this->defer([obj]() { obj->toggle().perform(); }); + DEFER_ACTION(obj, obj->toggle().perform()); request->send(200); } else { bool is_on = match.method_equals(ESPHOME_F("turn_on")); @@ -859,7 +865,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc return; } } - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); } return; @@ -909,7 +915,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa std::string data = this->light_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("toggle"))) { - this->defer([obj]() { obj->toggle().perform(); }); + DEFER_ACTION(obj, obj->toggle().perform()); request->send(200); } else { bool is_on = match.method_equals(ESPHOME_F("turn_on")); @@ -938,7 +944,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa parse_string_param_(request, ESPHOME_F("effect"), call, &decltype(call)::set_effect); } - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); } return; @@ -1027,7 +1033,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1086,7 +1092,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM auto call = obj->make_call(); parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1159,7 +1165,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1223,7 +1229,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1286,7 +1292,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1346,7 +1352,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1404,7 +1410,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM auto call = obj->make_call(); parse_string_param_(request, ESPHOME_F("option"), call, &decltype(call)::set_option); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1473,7 +1479,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low); parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1589,6 +1595,24 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con #endif #ifdef USE_LOCK +enum LockAction : uint8_t { LOCK_ACTION_NONE, LOCK_ACTION_LOCK, LOCK_ACTION_UNLOCK, LOCK_ACTION_OPEN }; + +static void execute_lock_action(lock::Lock *obj, LockAction action) { + switch (action) { + case LOCK_ACTION_LOCK: + obj->lock(); + break; + case LOCK_ACTION_UNLOCK: + obj->unlock(); + break; + case LOCK_ACTION_OPEN: + obj->open(); + break; + default: + break; + } +} + void WebServer::on_lock_update(lock::Lock *obj) { if (!this->include_internal_ && obj->is_internal()) return; @@ -1607,34 +1631,22 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat return; } - // Handle action methods with single defer and response - enum LockAction { NONE, LOCK, UNLOCK, OPEN }; - LockAction action = NONE; + LockAction action = LOCK_ACTION_NONE; if (match.method_equals(ESPHOME_F("lock"))) { - action = LOCK; + action = LOCK_ACTION_LOCK; } else if (match.method_equals(ESPHOME_F("unlock"))) { - action = UNLOCK; + action = LOCK_ACTION_UNLOCK; } else if (match.method_equals(ESPHOME_F("open"))) { - action = OPEN; + action = LOCK_ACTION_OPEN; } - if (action != NONE) { - this->defer([obj, action]() { - switch (action) { - case LOCK: - obj->lock(); - break; - case UNLOCK: - obj->unlock(); - break; - case OPEN: - obj->open(); - break; - default: - break; - } - }); + if (action != LOCK_ACTION_NONE) { +#ifdef USE_ESP8266 + execute_lock_action(obj, action); +#else + this->defer([obj, action]() { execute_lock_action(obj, action); }); +#endif request->send(200); } else { request->send(404); @@ -1717,7 +1729,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1796,7 +1808,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques return; } - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1872,7 +1884,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons // Parse on/off parameter parse_bool_param_(request, ESPHOME_F("is_on"), base_call, &water_heater::WaterHeaterCall::set_on); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -2032,7 +2044,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM return; } - this->defer([obj]() mutable { obj->perform(); }); + DEFER_ACTION(obj, obj->perform()); request->send(200); return; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index b1a495ebeff..c434d664cf4 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -42,6 +42,14 @@ using ParamNameType = const __FlashStringHelper *; using ParamNameType = const char *; #endif +// ESP8266 is single-threaded, so actions can execute directly in request context. +// Multi-core platforms need to defer to main loop thread for thread safety. +#ifdef USE_ESP8266 +#define DEFER_ACTION(capture, action) action +#else +#define DEFER_ACTION(capture, action) this->defer([capture]() mutable { action; }) +#endif + /// Result of matching a URL against an entity struct EntityMatchResult { bool matched; ///< True if entity matched the URL diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 2c83b36ef43..c3b3fd92e83 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -645,6 +645,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 55daccaa7d7..191ae2f6de6 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1,8 +1,11 @@ #pragma once +#include #include #include +#include #include +#include #include #include #include @@ -18,6 +21,7 @@ #ifdef USE_ESP8266 #include +#include #endif #ifdef USE_RP2040 @@ -661,6 +665,53 @@ std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, /// @warning Allocates heap memory. Use snprintf() with a stack buffer instead. std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...); +#ifdef USE_ESP8266 +// ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM) +// Format strings must be wrapped with PSTR() macro +/// Safely append formatted string to buffer, returning new position (capped at size). +/// @param buf Output buffer +/// @param size Total buffer size +/// @param pos Current position in buffer +/// @param fmt Format string (must be in PROGMEM on ESP8266) +/// @return New position after appending (capped at size on overflow) +inline size_t buf_append_printf_p(char *buf, size_t size, size_t pos, PGM_P fmt, ...) { + if (pos >= size) { + return size; + } + va_list args; + va_start(args, fmt); + int written = vsnprintf_P(buf + pos, size - pos, fmt, args); + va_end(args); + if (written < 0) { + return pos; // encoding error + } + return std::min(pos + static_cast(written), size); +} +#define buf_append_printf(buf, size, pos, fmt, ...) buf_append_printf_p(buf, size, pos, PSTR(fmt), ##__VA_ARGS__) +#else +/// Safely append formatted string to buffer, returning new position (capped at size). +/// Handles snprintf edge cases: negative returns (encoding errors) and truncation. +/// @param buf Output buffer +/// @param size Total buffer size +/// @param pos Current position in buffer +/// @param fmt printf-style format string +/// @return New position after appending (capped at size on overflow) +__attribute__((format(printf, 4, 5))) inline size_t buf_append_printf(char *buf, size_t size, size_t pos, + const char *fmt, ...) { + if (pos >= size) { + return size; + } + va_list args; + va_start(args, fmt); + int written = vsnprintf(buf + pos, size - pos, fmt, args); + va_end(args); + if (written < 0) { + return pos; // encoding error + } + return std::min(pos + static_cast(written), size); +} +#endif + /// Concatenate a name with a separator and suffix using an efficient stack-based approach. /// This avoids multiple heap allocations during string construction. /// Maximum name length supported is 120 characters for friendly names. @@ -1240,6 +1291,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