Merge remote-tracking branch 'upstream/esp8266_web_server_defer' into integration

This commit is contained in:
J. Nick Koston
2026-01-15 19:02:07 -10:00
8 changed files with 251 additions and 72 deletions
+19 -2
View File
@@ -18,7 +18,15 @@ InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) {
InfraredCall &InfraredCall::set_raw_timings(const std::vector<int32_t> &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());
+34 -9
View File
@@ -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<int32_t> &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<uint32_t> &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<int32_t> &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<uint32_t> carrier_frequency_;
// Vector-based timings (for lambdas/automations)
// Pointer to vector-based timings (caller-owned, must outlive perform())
const std::vector<int32_t> *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};
@@ -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) {
@@ -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;
+73 -61
View File
@@ -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;
}
@@ -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
+49
View File
@@ -645,6 +645,55 @@ std::vector<uint8_t> 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<char, BASE85_INT32_ENCODED_SIZE> output) {
uint32_t v = static_cast<uint32_t>(value);
// Encode least significant digit first, then reverse
for (int i = 4; i >= 0; i--) {
output[i] = static_cast<char>('!' + (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<uint8_t>(input[0] - '!');
uint8_t c1 = static_cast<uint8_t>(input[1] - '!');
uint8_t c2 = static_cast<uint8_t>(input[2] - '!');
uint8_t c3 = static_cast<uint8_t>(input[3] - '!');
uint8_t c4 = static_cast<uint8_t>(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<int32_t>(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<int32_t> &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) {
+59
View File
@@ -1,8 +1,11 @@
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <functional>
#include <iterator>
@@ -18,6 +21,7 @@
#ifdef USE_ESP8266
#include <Esp.h>
#include <pgmspace.h>
#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<size_t>(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<size_t>(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<uint8_t> 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<char, BASE85_INT32_ENCODED_SIZE> output);
bool base85_decode_int32(const char *input, int32_t &out);
bool base85_decode_int32_vector(const std::string &base85, std::vector<int32_t> &out);
///@}
/// @name Colors