Compare commits

..

3 Commits

Author SHA1 Message Date
J. Nick Koston
526bd58d1c Update esphome/components/sim800l/sim800l.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-16 13:57:09 -10:00
J. Nick Koston
1ed478fd5f Update esphome/components/pipsolar/output/pipsolar_output.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-16 13:57:04 -10:00
J. Nick Koston
f9d9136415 [dfrobot_sen0395][pipsolar][sim800l][wl_134] Replace sprintf with snprintf/buf_append_printf 2026-01-16 13:52:19 -10:00
20 changed files with 192 additions and 311 deletions

View File

@@ -48,14 +48,14 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (field_length > static_cast<size_t>(end - ptr)) {
if (ptr + field_length > end) {
return count; // Out of bounds
}
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
if (end - ptr < 4) {
if (ptr + 4 > end) {
return count;
}
ptr += 4;
@@ -110,7 +110,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (field_length > static_cast<size_t>(end - ptr)) {
if (ptr + field_length > end) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
@@ -121,7 +121,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit
if (end - ptr < 4) {
if (ptr + 4 > end) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;
}

View File

@@ -75,8 +75,8 @@ class SetLatencyCommand : public Command {
class SensorCfgStartCommand : public Command {
public:
SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) {
char tmp_cmd[20] = {0};
sprintf(tmp_cmd, "sensorCfgStart %d", startup_mode);
char tmp_cmd[20]; // "sensorCfgStart " (15) + "0/1" (1) + null = 17
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "sensorCfgStart %d", startup_mode);
cmd_ = std::string(tmp_cmd);
}
uint8_t on_message(std::string &message) override;
@@ -142,8 +142,8 @@ class SensitivityCommand : public Command {
SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) {
if (sensitivity > 9)
sensitivity_ = sensitivity = 9;
char tmp_cmd[20] = {0};
sprintf(tmp_cmd, "setSensitivity %d", sensitivity);
char tmp_cmd[20]; // "setSensitivity " (15) + "0-9" (1) + null = 17
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "setSensitivity %d", sensitivity);
cmd_ = std::string(tmp_cmd);
};
uint8_t on_message(std::string &message) override;

View File

@@ -1,3 +1,4 @@
#include <cstdio>
#include <cstring>
#include "hmac_sha256.h"
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST)
@@ -25,7 +26,9 @@ void HmacSHA256::calculate() { mbedtls_md_hmac_finish(&this->ctx_, this->digest_
void HmacSHA256::get_bytes(uint8_t *output) { memcpy(output, this->digest_, SHA256_DIGEST_SIZE); }
void HmacSHA256::get_hex(char *output) {
format_hex_to(output, SHA256_DIGEST_SIZE * 2 + 1, this->digest_, SHA256_DIGEST_SIZE);
for (size_t i = 0; i < SHA256_DIGEST_SIZE; i++) {
sprintf(output + (i * 2), "%02x", this->digest_[i]);
}
}
bool HmacSHA256::equals_bytes(const uint8_t *expected) {

View File

@@ -242,7 +242,9 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
return;
}
size_t max_length = this->max_response_buffer_size_;
size_t content_length = container->content_length;
size_t max_length = std::min(content_length, this->max_response_buffer_size_);
#ifdef USE_HTTP_REQUEST_RESPONSE
if (this->capture_response_.value(x...)) {
std::string response_body;

View File

@@ -213,12 +213,18 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
this->feed_wdt();
int read_len = esp_http_client_read(this->client_, (char *) buf, max_len);
this->feed_wdt();
if (read_len > 0) {
this->bytes_read_ += read_len;
int bufsize = std::min(max_len, this->content_length - this->bytes_read_);
if (bufsize == 0) {
this->duration_ms += (millis() - start);
return 0;
}
this->feed_wdt();
int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize);
this->feed_wdt();
this->bytes_read_ += read_len;
this->duration_ms += (millis() - start);
return read_len;

View File

@@ -1,4 +1,3 @@
import logging
from typing import Any
from esphome import automation, pins
@@ -19,16 +18,13 @@ from esphome.const import (
CONF_ROTATION,
CONF_UPDATE_INTERVAL,
)
from esphome.core import ID, EnumValue
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
import esphome.final_validate as fv
from esphome.helpers import add_class_to_obj
from esphome.types import ConfigType
from . import boards, hub75_ns
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@stuartparmenter"]
@@ -124,51 +120,13 @@ PANEL_LAYOUTS = {
}
Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True)
SCAN_WIRINGS = {
SCAN_PATTERNS = {
"STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN,
"SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH,
"SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH,
"SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH,
"SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH,
"FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH,
"FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH,
"FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH,
}
# Deprecated scan wiring names - mapped to new names
DEPRECATED_SCAN_WIRINGS = {
"FOUR_SCAN_16PX_HIGH": "SCAN_1_4_16PX_HIGH",
"FOUR_SCAN_32PX_HIGH": "SCAN_1_8_32PX_HIGH",
"FOUR_SCAN_64PX_HIGH": "SCAN_1_8_64PX_HIGH",
}
def _validate_scan_wiring(value):
"""Validate scan_wiring with deprecation warnings for old names."""
value = cv.string(value).upper().replace(" ", "_")
# Check if using deprecated name
# Remove deprecated names in 2026.7.0
if value in DEPRECATED_SCAN_WIRINGS:
new_name = DEPRECATED_SCAN_WIRINGS[value]
_LOGGER.warning(
"Scan wiring '%s' is deprecated and will be removed in ESPHome 2026.7.0. "
"Please use '%s' instead.",
value,
new_name,
)
value = new_name
# Validate against allowed values
if value not in SCAN_WIRINGS:
raise cv.Invalid(
f"Unknown scan wiring '{value}'. "
f"Valid options are: {', '.join(sorted(SCAN_WIRINGS.keys()))}"
)
# Return as EnumValue like cv.enum does
result = add_class_to_obj(value, EnumValue)
result.enum_value = SCAN_WIRINGS[value]
return result
Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True)
CLOCK_SPEEDS = {
"8MHZ": Hub75ClockSpeed.HZ_8M,
@@ -424,7 +382,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_LAYOUT_COLS): cv.positive_int,
cv.Optional(CONF_LAYOUT): cv.enum(PANEL_LAYOUTS, upper=True, space="_"),
# Panel hardware configuration
cv.Optional(CONF_SCAN_WIRING): _validate_scan_wiring,
cv.Optional(CONF_SCAN_WIRING): cv.enum(
SCAN_PATTERNS, upper=True, space="_"
),
cv.Optional(CONF_SHIFT_DRIVER): cv.enum(SHIFT_DRIVERS, upper=True),
# Display configuration
cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean,
@@ -587,7 +547,7 @@ def _build_config_struct(
async def to_code(config: ConfigType) -> None:
add_idf_component(
name="esphome/esp-hub75",
ref="0.3.0",
ref="0.2.2",
)
# Set compile-time configuration via build flags (so external library sees them)

View File

@@ -19,12 +19,12 @@ 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;
this->base64url_ptr_ = nullptr;
this->base85_ptr_ = nullptr;
return *this;
}
InfraredCall &InfraredCall::set_raw_timings_base64url(const std::string &base64url) {
this->base64url_ptr_ = &base64url;
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->base64url_ptr_ = nullptr;
this->base85_ptr_ = nullptr;
return *this;
}
@@ -101,22 +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_base64url()) {
// Decode base64url (URL-safe) into transmit buffer
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
ESP_LOGE(TAG, "Invalid base64url 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;
}
// Sanity check: validate timing values are within reasonable bounds
constexpr int32_t max_timing_us = 500000; // 500ms absolute max
for (int32_t timing : transmit_data->get_data()) {
int32_t abs_timing = timing < 0 ? -timing : timing;
if (abs_timing > max_timing_us) {
ESP_LOGE(TAG, "Invalid timing value: %d µs (max %d)", timing, max_timing_us);
return;
}
}
ESP_LOGD(TAG, "Transmitting base64url 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)

View File

@@ -40,11 +40,11 @@ class InfraredCall {
/// @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 base64url-encoded little-endian int32 data
/// 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 - base64url is fully URL-safe (uses '-' and '_').
/// @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_base64url(const std::string &base64url);
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().
@@ -59,18 +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)
/// 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 (any format)
/// 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->base64url_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 base64url data format
bool is_base64url() const { return this->base64url_ptr_ != nullptr; }
/// Get the base64url data string
const std::string &get_base64url_data() const { return *this->base64url_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_; }
@@ -84,8 +84,8 @@ class InfraredCall {
optional<uint32_t> carrier_frequency_;
// Pointer to vector-based timings (caller-owned, must outlive perform())
const std::vector<int32_t> *raw_timings_{nullptr};
// Pointer to base64url-encoded string (caller-owned, must outlive perform())
const std::string *base64url_ptr_{nullptr};
// 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};

View File

@@ -561,9 +561,8 @@ const char *OpenTherm::message_id_to_str(MessageId id) {
}
void OpenTherm::debug_data(OpenthermData &data) {
char type_buf[9], id_buf[9], hb_buf[9], lb_buf[9];
ESP_LOGD(TAG, "%s %s %s %s", format_bin_to(type_buf, data.type), format_bin_to(id_buf, data.id),
format_bin_to(hb_buf, data.valueHB), format_bin_to(lb_buf, data.valueLB));
ESP_LOGD(TAG, "%s %s %s %s", format_bin(data.type).c_str(), format_bin(data.id).c_str(),
format_bin(data.valueHB).c_str(), format_bin(data.valueLB).c_str());
ESP_LOGD(TAG, "type: %s; id: %u; HB: %u; LB: %u; uint_16: %u; float: %f",
this->message_type_to_str((MessageType) data.type), data.id, data.valueHB, data.valueLB, data.u16(),
data.f88());

View File

@@ -8,8 +8,8 @@ namespace pipsolar {
static const char *const TAG = "pipsolar.output";
void PipsolarOutput::write_state(float state) {
char tmp[10];
sprintf(tmp, this->set_command_.c_str(), state);
char tmp[16];
snprintf(tmp, sizeof(tmp), this->set_command_.c_str(), state);
if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) {
ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state);

View File

@@ -2,6 +2,8 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace remote_base {
@@ -158,8 +160,8 @@ void RemoteTransmitData::set_data_from_packed_sint32(const uint8_t *data, size_t
}
}
bool RemoteTransmitData::set_data_from_base64url(const std::string &base64url) {
return base64_decode_int32_vector(base64url, this->data_);
bool RemoteTransmitData::set_data_from_base85(const std::string &base85) {
return base85_decode_int32_vector(base85, this->data_);
}
/* RemoteTransmitterBase */

View File

@@ -36,11 +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 base64url-encoded little-endian int32 values
/// Base64url is URL-safe: uses '-' instead of '+', '_' instead of '/'
/// @param base64url Base64url-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_base64url(const std::string &base64url);
bool set_data_from_base85(const std::string &base85);
void reset() {
this->data_.clear();
this->carrier_frequency_ = 0;

View File

@@ -1,4 +1,5 @@
#include "sim800l.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cstring>
@@ -50,8 +51,8 @@ void Sim800LComponent::update() {
} else if (state_ == STATE_RECEIVED_SMS) {
// Serial Buffer should have flushed.
// Send cmd to delete received sms
char delete_cmd[20];
sprintf(delete_cmd, "AT+CMGD=%d", this->parse_index_);
char delete_cmd[20]; // "AT+CMGD=" (8) + uint8_t (max 3) + null = 12 <= 20
buf_append_printf(delete_cmd, sizeof(delete_cmd), 0, "AT+CMGD=%d", this->parse_index_);
this->send_cmd_(delete_cmd);
this->state_ = STATE_CHECK_SMS;
this->expect_ack_ = true;

View File

@@ -658,24 +658,6 @@ 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;
@@ -694,22 +676,34 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
return;
}
SwitchAction action = SWITCH_ACTION_NONE;
// Handle action methods with single defer and response
enum SwitchAction { NONE, TOGGLE, TURN_ON, TURN_OFF };
SwitchAction action = NONE;
if (match.method_equals(ESPHOME_F("toggle"))) {
action = SWITCH_ACTION_TOGGLE;
action = TOGGLE;
} else if (match.method_equals(ESPHOME_F("turn_on"))) {
action = SWITCH_ACTION_TURN_ON;
action = TURN_ON;
} else if (match.method_equals(ESPHOME_F("turn_off"))) {
action = SWITCH_ACTION_TURN_OFF;
action = TURN_OFF;
}
if (action != SWITCH_ACTION_NONE) {
#ifdef USE_ESP8266
execute_switch_action(obj, action);
#else
this->defer([obj, action]() { execute_switch_action(obj, action); });
#endif
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;
}
});
request->send(200);
} else {
request->send(404);
@@ -749,7 +743,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"))) {
DEFER_ACTION(obj, obj->press());
this->defer([obj]() { obj->press(); });
request->send(200);
return;
} else {
@@ -834,7 +828,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"))) {
DEFER_ACTION(obj, obj->toggle().perform());
this->defer([obj]() { obj->toggle().perform(); });
request->send(200);
} else {
bool is_on = match.method_equals(ESPHOME_F("turn_on"));
@@ -865,7 +859,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
return;
}
}
DEFER_ACTION(call, call.perform());
this->defer([call]() mutable { call.perform(); });
request->send(200);
}
return;
@@ -915,7 +909,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"))) {
DEFER_ACTION(obj, obj->toggle().perform());
this->defer([obj]() { obj->toggle().perform(); });
request->send(200);
} else {
bool is_on = match.method_equals(ESPHOME_F("turn_on"));
@@ -944,7 +938,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
parse_string_param_(request, ESPHOME_F("effect"), call, &decltype(call)::set_effect);
}
DEFER_ACTION(call, call.perform());
this->defer([call]() mutable { call.perform(); });
request->send(200);
}
return;
@@ -1033,7 +1027,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);
DEFER_ACTION(call, call.perform());
this->defer([call]() mutable { call.perform(); });
request->send(200);
return;
}
@@ -1092,7 +1086,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);
DEFER_ACTION(call, call.perform());
this->defer([call]() mutable { call.perform(); });
request->send(200);
return;
}
@@ -1165,7 +1159,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date);
DEFER_ACTION(call, call.perform());
this->defer([call]() mutable { call.perform(); });
request->send(200);
return;
}
@@ -1229,7 +1223,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time);
DEFER_ACTION(call, call.perform());
this->defer([call]() mutable { call.perform(); });
request->send(200);
return;
}
@@ -1292,7 +1286,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime);
DEFER_ACTION(call, call.perform());
this->defer([call]() mutable { call.perform(); });
request->send(200);
return;
}
@@ -1352,7 +1346,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);
DEFER_ACTION(call, call.perform());
this->defer([call]() mutable { call.perform(); });
request->send(200);
return;
}
@@ -1410,7 +1404,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);
DEFER_ACTION(call, call.perform());
this->defer([call]() mutable { call.perform(); });
request->send(200);
return;
}
@@ -1479,7 +1473,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);
DEFER_ACTION(call, call.perform());
this->defer([call]() mutable { call.perform(); });
request->send(200);
return;
}
@@ -1595,24 +1589,6 @@ 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;
@@ -1631,22 +1607,34 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
return;
}
LockAction action = LOCK_ACTION_NONE;
// Handle action methods with single defer and response
enum LockAction { NONE, LOCK, UNLOCK, OPEN };
LockAction action = NONE;
if (match.method_equals(ESPHOME_F("lock"))) {
action = LOCK_ACTION_LOCK;
action = LOCK;
} else if (match.method_equals(ESPHOME_F("unlock"))) {
action = LOCK_ACTION_UNLOCK;
action = UNLOCK;
} else if (match.method_equals(ESPHOME_F("open"))) {
action = LOCK_ACTION_OPEN;
action = OPEN;
}
if (action != LOCK_ACTION_NONE) {
#ifdef USE_ESP8266
execute_lock_action(obj, action);
#else
this->defer([obj, action]() { execute_lock_action(obj, action); });
#endif
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;
}
});
request->send(200);
} else {
request->send(404);
@@ -1729,7 +1717,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
DEFER_ACTION(call, call.perform());
this->defer([call]() mutable { call.perform(); });
request->send(200);
return;
}
@@ -1808,7 +1796,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
return;
}
DEFER_ACTION(call, call.perform());
this->defer([call]() mutable { call.perform(); });
request->send(200);
return;
}
@@ -1884,7 +1872,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);
DEFER_ACTION(call, call.perform());
this->defer([call]() mutable { call.perform(); });
request->send(200);
return;
}
@@ -2044,7 +2032,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
return;
}
DEFER_ACTION(obj, obj->perform());
this->defer([obj]() mutable { obj->perform(); });
request->send(200);
return;
}

View File

@@ -42,14 +42,6 @@ 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

View File

@@ -1,4 +1,5 @@
#include "wl_134.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -78,8 +79,8 @@ Wl134Component::Rfid134Error Wl134Component::read_packet_() {
reading.id, reading.country, reading.isData ? "true" : "false", reading.isAnimal ? "true" : "false",
reading.reserved0, reading.reserved1);
char buf[20];
sprintf(buf, "%03d%012lld", reading.country, reading.id);
char buf[20]; // "%03d" (3) + "%012" PRId64 (12) + null = 16 max
buf_append_printf(buf, sizeof(buf), 0, "%03d%012" PRId64, reading.country, reading.id);
this->publish_state(buf);
if (this->do_reset_) {
this->set_timeout(1000, [this]() { this->publish_state(""); });

View File

@@ -404,31 +404,15 @@ std::string format_hex_pretty(const std::string &data, char separator, bool show
return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length);
}
char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) {
if (buffer_size == 0) {
return buffer;
}
// Calculate max bytes we can format: each byte needs 8 chars
size_t max_bytes = (buffer_size - 1) / 8;
if (max_bytes == 0 || length == 0) {
buffer[0] = '\0';
return buffer;
}
size_t bytes_to_format = std::min(length, max_bytes);
for (size_t byte_idx = 0; byte_idx < bytes_to_format; byte_idx++) {
for (size_t bit_idx = 0; bit_idx < 8; bit_idx++) {
buffer[byte_idx * 8 + bit_idx] = ((data[byte_idx] >> (7 - bit_idx)) & 1) + '0';
}
}
buffer[bytes_to_format * 8] = '\0';
return buffer;
}
std::string format_bin(const uint8_t *data, size_t length) {
std::string result;
result.resize(length * 8);
format_bin_to(&result[0], length * 8 + 1, data, length);
for (size_t byte_idx = 0; byte_idx < length; byte_idx++) {
for (size_t bit_idx = 0; bit_idx < 8; bit_idx++) {
result[byte_idx * 8 + bit_idx] = ((data[byte_idx] >> (7 - bit_idx)) & 1) + '0';
}
}
return result;
}
@@ -640,44 +624,53 @@ std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
return ret;
}
/// Decode base64/base64url string directly into vector of little-endian int32 values
/// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted)
/// @param out Output vector (cleared and filled with decoded int32 values)
/// @return true if successful, false if decode failed or invalid size
bool base64_decode_int32_vector(const std::string &base64, std::vector<int32_t> &out) {
// Decode in chunks to minimize stack usage
constexpr size_t chunk_bytes = 48; // 12 int32 values
constexpr size_t chunk_chars = 64; // 48 * 4/3 = 64 chars
uint8_t chunk[chunk_bytes];
/// 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;
const uint8_t *input = reinterpret_cast<const uint8_t *>(base64.data());
size_t remaining = base64.size();
size_t pos = 0;
while (remaining > 0) {
size_t chars_to_decode = std::min(remaining, chunk_chars);
size_t decoded_len = base64_decode(input + pos, chars_to_decode, chunk, chunk_bytes);
if (decoded_len == 0)
while (ptr < end) {
int32_t value;
if (!base85_decode_int32(ptr, value))
return false;
// Parse little-endian int32 values
for (size_t i = 0; i + 3 < decoded_len; i += 4) {
int32_t timing = static_cast<int32_t>(encode_uint32(chunk[i + 3], chunk[i + 2], chunk[i + 1], chunk[i]));
out.push_back(timing);
}
// Check for incomplete int32 in last chunk
if (remaining <= chunk_chars && (decoded_len % 4) != 0)
return false;
pos += chars_to_decode;
remaining -= chars_to_decode;
out.push_back(value);
ptr += 5;
}
return !out.empty();
return true;
}
// Colors

View File

@@ -1096,66 +1096,9 @@ std::string format_hex_pretty(T val, char separator = '.', bool show_length = tr
return format_hex_pretty(reinterpret_cast<uint8_t *>(&val), sizeof(T), separator, show_length);
}
/// Calculate buffer size needed for format_bin_to: "01234567...\0" = bytes * 8 + 1
constexpr size_t format_bin_size(size_t byte_count) { return byte_count * 8 + 1; }
/** Format byte array as binary string to buffer.
*
* Each byte is formatted as 8 binary digits (MSB first).
* Truncates output if data exceeds buffer capacity.
*
* @param buffer Output buffer to write to.
* @param buffer_size Size of the output buffer.
* @param data Pointer to the byte array to format.
* @param length Number of bytes in the array.
* @return Pointer to buffer.
*
* Buffer size needed: length * 8 + 1 (use format_bin_size()).
*
* Example:
* @code
* char buf[9]; // format_bin_size(1)
* format_bin_to(buf, sizeof(buf), data, 1); // "10101011"
* @endcode
*/
char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length);
/// Format byte array as binary to buffer. Automatically deduces buffer size.
template<size_t N> inline char *format_bin_to(char (&buffer)[N], const uint8_t *data, size_t length) {
static_assert(N >= 9, "Buffer must hold at least one binary byte (9 chars)");
return format_bin_to(buffer, N, data, length);
}
/** Format an unsigned integer in binary to buffer, MSB first.
*
* @tparam N Buffer size (must be >= sizeof(T) * 8 + 1).
* @tparam T Unsigned integer type.
* @param buffer Output buffer to write to.
* @param val The unsigned integer value to format.
* @return Pointer to buffer.
*
* Example:
* @code
* char buf[9]; // format_bin_size(sizeof(uint8_t))
* format_bin_to(buf, uint8_t{0xAA}); // "10101010"
* char buf16[17]; // format_bin_size(sizeof(uint16_t))
* format_bin_to(buf16, uint16_t{0x1234}); // "0001001000110100"
* @endcode
*/
template<size_t N, typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0>
inline char *format_bin_to(char (&buffer)[N], T val) {
static_assert(N >= sizeof(T) * 8 + 1, "Buffer too small for type");
val = convert_big_endian(val);
return format_bin_to(buffer, reinterpret_cast<const uint8_t *>(&val), sizeof(T));
}
/// Format the byte array \p data of length \p len in binary.
/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead.
/// Causes heap fragmentation on long-running devices.
std::string format_bin(const uint8_t *data, size_t length);
/// Format an unsigned integer in binary, starting with the most significant byte.
/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead.
/// Causes heap fragmentation on long-running devices.
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_bin(T val) {
val = convert_big_endian(val);
return format_bin(reinterpret_cast<uint8_t *>(&val), sizeof(T));
@@ -1194,11 +1137,13 @@ 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);
/// Decode base64/base64url string directly into vector of little-endian int32 values
/// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted)
/// @param out Output vector (cleared and filled with decoded int32 values)
/// @return true if successful, false if decode failed or invalid size
bool base64_decode_int32_vector(const std::string &base64, std::vector<int32_t> &out);
/// 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);
///@}

View File

@@ -28,8 +28,8 @@ dependencies:
rules:
- if: "target in [esp32s2, esp32s3, esp32p4]"
esphome/esp-hub75:
version: 0.3.0
version: 0.2.2
rules:
- if: "target in [esp32, esp32s2, esp32s3, esp32c6, esp32p4]"
- if: "target in [esp32, esp32s2, esp32s3, esp32p4]"
esp32async/asynctcp:
version: 3.4.91

View File

@@ -682,7 +682,6 @@ def lint_trailing_whitespace(fname, match):
# Heap-allocating helpers that cause fragmentation on long-running embedded devices.
# These return std::string and should be replaced with stack-based alternatives.
HEAP_ALLOCATING_HELPERS = {
"format_bin": "format_bin_to() with a stack buffer",
"format_hex": "format_hex_to() with a stack buffer",
"format_hex_pretty": "format_hex_pretty_to() with a stack buffer",
"format_mac_address_pretty": "format_mac_addr_upper() with a stack buffer",
@@ -700,7 +699,6 @@ HEAP_ALLOCATING_HELPERS = {
# get_mac_address(?!_) ensures we don't match get_mac_address_into_buffer, etc.
# CPP_RE_EOL captures rest of line so NOLINT comments are detected
r"[^\w]("
r"format_bin(?!_)|"
r"format_hex(?!_)|"
r"format_hex_pretty(?!_)|"
r"format_mac_address_pretty|"