mirror of
https://github.com/esphome/esphome.git
synced 2026-02-13 13:07:41 -07:00
Compare commits
5 Commits
api-string
...
bump_espid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d504c684dc | ||
|
|
28d556f78f | ||
|
|
06c5b06f74 | ||
|
|
c4a5df08c6 | ||
|
|
5e50716deb |
@@ -1 +1 @@
|
||||
ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e
|
||||
9c4da80001128f05bb3bb3668a18609eea86e0b56bb04a22e66c7ec6609f92ff
|
||||
|
||||
4
.github/actions/build-image/action.yaml
vendored
4
.github/actions/build-image/action.yaml
vendored
@@ -47,7 +47,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
@@ -73,7 +73,7 @@ runs:
|
||||
|
||||
- name: Build and push to dockerhub by digest
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
@@ -9,8 +9,7 @@ FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS b
|
||||
ARG BUILD_TYPE
|
||||
FROM base-source-${BUILD_TYPE} AS base
|
||||
|
||||
RUN git config --system --add safe.directory "*" \
|
||||
&& git config --system advice.detachedHead false
|
||||
RUN git config --system --add safe.directory "*"
|
||||
|
||||
# Install build tools for Python packages that require compilation
|
||||
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
|
||||
|
||||
@@ -256,7 +256,7 @@ SYMBOL_PATTERNS = {
|
||||
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
|
||||
# Order matters! More specific categories must come before general ones.
|
||||
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
|
||||
"mdns_lib": ["mdns", "packet$"],
|
||||
"mdns_lib": ["mdns"],
|
||||
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
|
||||
"memory_mgmt": [
|
||||
"mem_",
|
||||
@@ -794,6 +794,7 @@ SYMBOL_PATTERNS = {
|
||||
"s_dp",
|
||||
"s_ni",
|
||||
"s_reg_dump",
|
||||
"packet$",
|
||||
"d_mult_table",
|
||||
"K",
|
||||
"fcstab",
|
||||
|
||||
@@ -57,14 +57,8 @@ def maybe_conf(conf, *validators):
|
||||
return validate
|
||||
|
||||
|
||||
def register_action(
|
||||
name: str,
|
||||
action_type: MockObjClass,
|
||||
schema: cv.Schema,
|
||||
*,
|
||||
deferred: bool = False,
|
||||
):
|
||||
return ACTION_REGISTRY.register(name, action_type, schema, deferred=deferred)
|
||||
def register_action(name: str, action_type: MockObjClass, schema: cv.Schema):
|
||||
return ACTION_REGISTRY.register(name, action_type, schema)
|
||||
|
||||
|
||||
def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema):
|
||||
@@ -341,10 +335,7 @@ async def component_is_idle_condition_to_code(
|
||||
|
||||
|
||||
@register_action(
|
||||
"delay",
|
||||
DelayAction,
|
||||
cv.templatable(cv.positive_time_period_milliseconds),
|
||||
deferred=True,
|
||||
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
|
||||
)
|
||||
async def delay_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -454,7 +445,7 @@ _validate_wait_until = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
@register_action("wait_until", WaitUntilAction, _validate_wait_until, deferred=True)
|
||||
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
|
||||
async def wait_until_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
@@ -587,26 +578,6 @@ async def build_condition_list(
|
||||
return conditions
|
||||
|
||||
|
||||
def has_deferred_actions(actions: ConfigType) -> bool:
|
||||
"""Check if a validated action list contains any deferred actions.
|
||||
|
||||
Deferred actions (delay, wait_until, script.wait) store trigger args
|
||||
for later execution, making non-owning types like StringRef unsafe.
|
||||
"""
|
||||
if isinstance(actions, list):
|
||||
return any(has_deferred_actions(item) for item in actions)
|
||||
if isinstance(actions, dict):
|
||||
for key in actions:
|
||||
if key in ACTION_REGISTRY and ACTION_REGISTRY[key].deferred:
|
||||
return True
|
||||
return any(
|
||||
has_deferred_actions(v)
|
||||
for v in actions.values()
|
||||
if isinstance(v, (list, dict))
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
async def build_automation(
|
||||
trigger: MockObj, args: TemplateArgsType, config: ConfigType
|
||||
) -> MockObj:
|
||||
|
||||
@@ -76,7 +76,7 @@ SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = {
|
||||
"bool": cg.bool_,
|
||||
"int": cg.int32,
|
||||
"float": cg.float_,
|
||||
"string": cg.StringRef,
|
||||
"string": cg.std_string,
|
||||
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
|
||||
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
|
||||
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
|
||||
@@ -380,16 +380,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
if is_optional:
|
||||
func_args.append((cg.bool_, "return_response"))
|
||||
|
||||
# Check if action chain has deferred actions that would make
|
||||
# non-owning StringRef dangle (rx_buf_ reused after delay)
|
||||
has_deferred = automation.has_deferred_actions(conf.get(CONF_THEN, []))
|
||||
|
||||
service_arg_names: list[str] = []
|
||||
for name, var_ in conf[CONF_VARIABLES].items():
|
||||
native = SERVICE_ARG_NATIVE_TYPES[var_]
|
||||
# Fall back to std::string for string args if deferred actions exist
|
||||
if has_deferred and native is cg.StringRef:
|
||||
native = cg.std_string
|
||||
service_template_args.append(native)
|
||||
func_args.append((native, name))
|
||||
service_arg_names.append(name)
|
||||
|
||||
@@ -824,7 +824,7 @@ message HomeAssistantStateResponse {
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
|
||||
|
||||
string entity_id = 1;
|
||||
string state = 2 [(null_terminate) = true];
|
||||
string state = 2;
|
||||
string attribute = 3;
|
||||
}
|
||||
|
||||
@@ -882,7 +882,7 @@ message ExecuteServiceArgument {
|
||||
bool bool_ = 1;
|
||||
int32 legacy_int = 2;
|
||||
float float_ = 3;
|
||||
string string_ = 4 [(null_terminate) = true];
|
||||
string string_ = 4;
|
||||
// ESPHome 1.14 (api v1.3) make int a signed value
|
||||
sint32 int_ = 5;
|
||||
repeated bool bool_array = 6 [packed=false, (fixed_vector) = true];
|
||||
|
||||
@@ -1683,18 +1683,31 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
||||
}
|
||||
|
||||
for (auto &it : this->parent_->get_state_subs()) {
|
||||
if (msg.entity_id != it.entity_id) {
|
||||
// Compare entity_id: check length matches and content matches
|
||||
size_t entity_id_len = strlen(it.entity_id);
|
||||
if (entity_id_len != msg.entity_id.size() ||
|
||||
memcmp(it.entity_id, msg.entity_id.c_str(), msg.entity_id.size()) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compare attribute: either both have matching attribute, or both have none
|
||||
// it.attribute can be nullptr (meaning no attribute filter)
|
||||
if (it.attribute != nullptr ? msg.attribute != it.attribute : !msg.attribute.empty()) {
|
||||
size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
|
||||
if (sub_attr_len != msg.attribute.size() ||
|
||||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute.c_str(), sub_attr_len) != 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// msg.state is already null-terminated in-place after protobuf decode
|
||||
it.callback(msg.state);
|
||||
// Create null-terminated state for callback (parse_number needs null-termination)
|
||||
// HA state max length is 255 characters, but attributes can be much longer
|
||||
// Use stack buffer for common case (states), heap fallback for large attributes
|
||||
size_t state_len = msg.state.size();
|
||||
SmallBufferWithHeapFallback<MAX_STATE_LEN + 1> state_buf_alloc(state_len + 1);
|
||||
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
|
||||
if (state_len > 0) {
|
||||
memcpy(state_buf, msg.state.c_str(), state_len);
|
||||
}
|
||||
state_buf[state_len] = '\0';
|
||||
it.callback(StringRef(state_buf, state_len));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1851,8 +1864,6 @@ void APIConnection::on_fatal_error() {
|
||||
this->flags_.remove = true;
|
||||
}
|
||||
|
||||
void __attribute__((flatten)) APIConnection::DeferredBatch::push_item(const BatchItem &item) { items.push_back(item); }
|
||||
|
||||
void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
||||
uint8_t aux_data_index) {
|
||||
// Check if we already have a message of this type for this entity
|
||||
@@ -1869,7 +1880,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_
|
||||
}
|
||||
}
|
||||
// No existing item found (or event), add new one
|
||||
this->push_item({entity, message_type, estimated_size, aux_data_index});
|
||||
items.push_back({entity, message_type, estimated_size, aux_data_index});
|
||||
}
|
||||
|
||||
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
|
||||
@@ -1877,7 +1888,7 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t me
|
||||
// This avoids expensive vector::insert which shifts all elements
|
||||
// Note: We only ever have one high-priority message at a time (ping OR disconnect)
|
||||
// If we're disconnecting, pings are blocked, so this simple swap is sufficient
|
||||
this->push_item({entity, message_type, estimated_size, AUX_DATA_UNUSED});
|
||||
items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED});
|
||||
if (items.size() > 1) {
|
||||
// Swap the new high-priority item to the front
|
||||
std::swap(items.front(), items.back());
|
||||
|
||||
@@ -541,8 +541,6 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
uint8_t aux_data_index = AUX_DATA_UNUSED);
|
||||
// Add item to the front of the batch (for high priority messages like ping)
|
||||
void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size);
|
||||
// Single push_back site to avoid duplicate _M_realloc_insert instantiation
|
||||
void push_item(const BatchItem &item);
|
||||
|
||||
// Clear all items
|
||||
void clear() {
|
||||
|
||||
@@ -138,12 +138,10 @@ APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func
|
||||
|
||||
/// Run through handshake messages (if in that phase)
|
||||
APIError APINoiseFrameHelper::loop() {
|
||||
// Cache ready() outside the loop. On ESP8266 LWIP raw TCP, ready() returns false once
|
||||
// the rx buffer is consumed. Re-checking each iteration would block handshake writes
|
||||
// that must follow reads, deadlocking the handshake. state_action() will return
|
||||
// WOULD_BLOCK when no more data is available to read.
|
||||
bool socket_ready = this->socket_->ready();
|
||||
while (state_ != State::DATA && socket_ready) {
|
||||
// During handshake phase, process as many actions as possible until we can't progress
|
||||
// socket_->ready() stays true until next main loop, but state_action() will return
|
||||
// WOULD_BLOCK when no more data is available to read
|
||||
while (state_ != State::DATA && this->socket_->ready()) {
|
||||
APIError err = state_action_();
|
||||
if (err == APIError::WOULD_BLOCK) {
|
||||
break;
|
||||
@@ -201,10 +199,9 @@ APIError APINoiseFrameHelper::try_read_frame_() {
|
||||
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
|
||||
}
|
||||
|
||||
// Reserve space for body (+1 for null terminator so protobuf StringRef fields
|
||||
// can be safely null-terminated in-place after decode)
|
||||
if (this->rx_buf_.size() != msg_size + 1) {
|
||||
this->rx_buf_.resize(msg_size + 1);
|
||||
// Reserve space for body
|
||||
if (this->rx_buf_.size() != msg_size) {
|
||||
this->rx_buf_.resize(msg_size);
|
||||
}
|
||||
|
||||
if (rx_buf_len_ < msg_size) {
|
||||
|
||||
@@ -163,10 +163,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
|
||||
}
|
||||
// header reading done
|
||||
|
||||
// Reserve space for body (+1 for null terminator so protobuf StringRef fields
|
||||
// can be safely null-terminated in-place after decode)
|
||||
if (this->rx_buf_.size() != this->rx_header_parsed_len_ + 1) {
|
||||
this->rx_buf_.resize(this->rx_header_parsed_len_ + 1);
|
||||
// Reserve space for body
|
||||
if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
|
||||
this->rx_buf_.resize(this->rx_header_parsed_len_);
|
||||
}
|
||||
|
||||
if (rx_buf_len_ < rx_header_parsed_len_) {
|
||||
|
||||
@@ -90,13 +90,4 @@ extend google.protobuf.FieldOptions {
|
||||
// - uint16_t <field>_length_{0};
|
||||
// - uint16_t <field>_count_{0};
|
||||
optional bool packed_buffer = 50015 [default=false];
|
||||
|
||||
// null_terminate: Write a null byte after string data in the decode buffer.
|
||||
// When set on a string field in a SOURCE_CLIENT (decodable) message, the
|
||||
// generated decode() override writes '\0' at data[length] after decoding.
|
||||
// This makes the StringRef safe for c_str() usage without copying.
|
||||
// Safe because: (1) frame helpers reserve +1 byte in rx_buf_, and
|
||||
// (2) the overwritten byte was already consumed during decode.
|
||||
// Only mark fields that actually need null-terminated access.
|
||||
optional bool null_terminate = 50016 [default=false];
|
||||
}
|
||||
|
||||
@@ -953,12 +953,6 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void HomeAssistantStateResponse::decode(const uint8_t *buffer, size_t length) {
|
||||
ProtoDecodableMessage::decode(buffer, length);
|
||||
if (!this->state.empty()) {
|
||||
const_cast<char *>(this->state.c_str())[this->state.size()] = '\0';
|
||||
}
|
||||
}
|
||||
#endif
|
||||
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
@@ -1063,9 +1057,6 @@ void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
|
||||
uint32_t count_string_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 9);
|
||||
this->string_array.init(count_string_array);
|
||||
ProtoDecodableMessage::decode(buffer, length);
|
||||
if (!this->string_.empty()) {
|
||||
const_cast<char *>(this->string_.c_str())[this->string_.size()] = '\0';
|
||||
}
|
||||
}
|
||||
bool ExecuteServiceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
|
||||
@@ -1095,7 +1095,6 @@ class HomeAssistantStateResponse final : public ProtoDecodableMessage {
|
||||
StringRef entity_id{};
|
||||
StringRef state{};
|
||||
StringRef attribute{};
|
||||
void decode(const uint8_t *buffer, size_t length) override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
@@ -117,7 +117,37 @@ void APIServer::setup() {
|
||||
void APIServer::loop() {
|
||||
// Accept new clients only if the socket exists and has incoming connections
|
||||
if (this->socket_ && this->socket_->ready()) {
|
||||
this->accept_new_connections_();
|
||||
while (true) {
|
||||
struct sockaddr_storage source_addr;
|
||||
socklen_t addr_len = sizeof(source_addr);
|
||||
|
||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||
if (!sock)
|
||||
break;
|
||||
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// Check if we're at the connection limit
|
||||
if (this->clients_.size() >= this->max_connections_) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->clients_.empty()) {
|
||||
@@ -148,88 +178,46 @@ void APIServer::loop() {
|
||||
while (client_index < this->clients_.size()) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
// Common case: process active client
|
||||
if (!client->flags_.remove) {
|
||||
// Common case: process active client
|
||||
client->loop();
|
||||
}
|
||||
// Handle disconnection promptly - close socket to free LWIP PCB
|
||||
// resources and prevent retransmit crashes on ESP8266.
|
||||
if (client->flags_.remove) {
|
||||
// Rare case: handle disconnection (don't increment - swapped element needs processing)
|
||||
this->remove_client_(client_index);
|
||||
} else {
|
||||
client_index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::remove_client_(size_t client_index) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before closing socket and removal for the trigger
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN];
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
#endif
|
||||
|
||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||
client->helper_->close();
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||
#endif
|
||||
}
|
||||
|
||||
void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||
while (true) {
|
||||
struct sockaddr_storage source_addr;
|
||||
socklen_t addr_len = sizeof(source_addr);
|
||||
|
||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||
if (!sock)
|
||||
break;
|
||||
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// Check if we're at the connection limit
|
||||
if (this->clients_.size() >= this->max_connections_) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
// Rare case: handle disconnection
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before closing socket and removal for the trigger
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN];
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
#endif
|
||||
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||
client->helper_->close();
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||
#endif
|
||||
// Don't increment client_index since we need to process the swapped element
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -234,11 +234,6 @@ class APIServer : public Component,
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// Accept incoming socket connections. Only called when socket has pending connections.
|
||||
void __attribute__((noinline)) accept_new_connections_();
|
||||
// Remove a disconnected client by index. Swaps with last element and pops.
|
||||
void __attribute__((noinline)) remove_client_(size_t client_index);
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
const psk_t &active_psk, bool make_active);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "user_services.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -12,8 +11,6 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
|
||||
}
|
||||
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
|
||||
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
|
||||
// Zero-copy StringRef version for YAML-generated services (string_ is null-terminated after decode)
|
||||
template<> StringRef get_execute_arg_value<StringRef>(const ExecuteServiceArgument &arg) { return arg.string_; }
|
||||
|
||||
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
|
||||
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
|
||||
@@ -64,8 +61,6 @@ template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SER
|
||||
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
|
||||
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
|
||||
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
|
||||
// Zero-copy StringRef version for YAML-generated services
|
||||
template<> enums::ServiceArgType to_service_arg_type<StringRef>() { return enums::SERVICE_ARG_TYPE_STRING; }
|
||||
|
||||
// Legacy std::vector versions for external components using custom_api_device.h
|
||||
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
|
||||
|
||||
@@ -47,8 +47,8 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
||||
request->send(stream);
|
||||
}
|
||||
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
||||
const auto &ssid = request->arg("ssid");
|
||||
const auto &psk = request->arg("psk");
|
||||
std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr)
|
||||
std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr)
|
||||
ESP_LOGI(TAG,
|
||||
"Requested WiFi Settings Change:\n"
|
||||
" SSID='%s'\n"
|
||||
@@ -56,10 +56,10 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
||||
ssid.c_str(), psk.c_str());
|
||||
#ifdef USE_ESP8266
|
||||
// ESP8266 is single-threaded, call directly
|
||||
wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str());
|
||||
wifi::global_wifi_component->save_wifi_sta(ssid, psk);
|
||||
#else
|
||||
// Defer save to main loop thread to avoid NVS operations from HTTP thread
|
||||
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); });
|
||||
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); });
|
||||
#endif
|
||||
request->redirect(ESPHOME_F("/?save"));
|
||||
}
|
||||
|
||||
@@ -613,9 +613,13 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
||||
return f"{ARDUINO_FRAMEWORK_PKG}@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}"
|
||||
|
||||
|
||||
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
|
||||
def _format_framework_espidf_version(
|
||||
ver: cv.Version, release: str | None = None
|
||||
) -> str:
|
||||
# format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
|
||||
# a PIO platformio/framework-espidf value
|
||||
if release is None:
|
||||
release = ESP_IDF_RELEASE_LOOKUP.get(ver)
|
||||
if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1):
|
||||
ext = "tar.xz"
|
||||
else:
|
||||
@@ -692,6 +696,9 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"latest": cv.Version(5, 5, 2),
|
||||
"dev": cv.Version(5, 5, 2),
|
||||
}
|
||||
ESP_IDF_RELEASE_LOOKUP: dict[cv.Version, str] = {
|
||||
cv.Version(5, 5, 2): "260206",
|
||||
}
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
|
||||
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
|
||||
@@ -755,7 +762,7 @@ def _check_versions(config):
|
||||
platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version)
|
||||
value[CONF_SOURCE] = value.get(
|
||||
CONF_SOURCE,
|
||||
_format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
|
||||
_format_framework_espidf_version(version, value.get(CONF_RELEASE)),
|
||||
)
|
||||
if _is_framework_url(value[CONF_SOURCE]):
|
||||
value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
|
||||
@@ -1467,7 +1474,7 @@ async def to_code(config):
|
||||
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
||||
cg.add_platformio_option(
|
||||
"platform_packages",
|
||||
[_format_framework_espidf_version(idf_ver, None)],
|
||||
[_format_framework_espidf_version(idf_ver)],
|
||||
)
|
||||
# Use stub package to skip downloading precompiled libs
|
||||
stubs_dir = CORE.relative_build_path("arduino_libs_stub")
|
||||
@@ -1511,6 +1518,14 @@ async def to_code(config):
|
||||
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
|
||||
)
|
||||
|
||||
# ESP32-P4: The ESP-IDF 5.5.2.260206 release changed the default of
|
||||
# ESP32P4_SELECTS_REV_LESS_V3 from y to n. PlatformIO always uses
|
||||
# sections.ld.in (for rev <3) rather than sections.rev3.ld.in (for rev >=3),
|
||||
# causing a linker script mismatch with the generated memory.ld.
|
||||
# Restore the previous default until PlatformIO handles this properly.
|
||||
if variant == VARIANT_ESP32P4:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", True)
|
||||
|
||||
# Set minimum chip revision for ESP32 variant
|
||||
# Setting this to 3.0 or higher reduces flash size by excluding workaround code,
|
||||
# and for PSRAM users saves significant IRAM by keeping C library functions in ROM.
|
||||
|
||||
@@ -403,18 +403,21 @@ def final_validation(config):
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True)
|
||||
|
||||
# Check if BLE Server is needed
|
||||
has_ble_server = "esp32_ble_server" in full_config
|
||||
|
||||
# Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client)
|
||||
has_ble_client = (
|
||||
"esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config
|
||||
)
|
||||
|
||||
# ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled
|
||||
# This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1)
|
||||
# Always enable GATTS: ESP-IDF 5.5.2.260206 has a bug in gatt_main.c where a
|
||||
# GATT_TRACE_DEBUG references 'msg_len' outside the GATTS_INCLUDED/GATTC_INCLUDED
|
||||
# guard, causing a compile error when both are disabled.
|
||||
# Additionally, when GATT Client is enabled, GATT Server must also be enabled
|
||||
# as an internal dependency in the Bluedroid stack.
|
||||
# See: https://github.com/espressif/esp-idf/issues/17724
|
||||
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client)
|
||||
# TODO: Revert to conditional once the gatt_main.c bug is fixed upstream:
|
||||
# has_ble_server = "esp32_ble_server" in full_config
|
||||
# add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", True)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client)
|
||||
|
||||
# Handle max_connections: check for deprecated location in esp32_ble_tracker
|
||||
|
||||
@@ -110,8 +110,6 @@ class EthernetComponent : public Component {
|
||||
const char *get_use_address() const;
|
||||
void set_use_address(const char *use_address);
|
||||
void get_eth_mac_address_raw(uint8_t *mac);
|
||||
// Remove before 2026.9.0
|
||||
ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0")
|
||||
std::string get_eth_mac_address_pretty();
|
||||
const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||
eth_duplex_t get_duplex_mode();
|
||||
|
||||
@@ -38,7 +38,8 @@ void PulseMeterSensor::setup() {
|
||||
}
|
||||
|
||||
void PulseMeterSensor::loop() {
|
||||
State state;
|
||||
// Reset the count in get before we pass it back to the ISR as set
|
||||
this->get_->count_ = 0;
|
||||
|
||||
{
|
||||
// Lock the interrupt so the interrupt code doesn't interfere with itself
|
||||
@@ -57,35 +58,31 @@ void PulseMeterSensor::loop() {
|
||||
}
|
||||
this->last_pin_val_ = current;
|
||||
|
||||
// Get the latest state from the ISR and reset the count in the ISR
|
||||
state.last_detected_edge_us_ = this->state_.last_detected_edge_us_;
|
||||
state.last_rising_edge_us_ = this->state_.last_rising_edge_us_;
|
||||
state.count_ = this->state_.count_;
|
||||
this->state_.count_ = 0;
|
||||
// Swap out set and get to get the latest state from the ISR
|
||||
std::swap(this->set_, this->get_);
|
||||
}
|
||||
|
||||
const uint32_t now = micros();
|
||||
|
||||
// If an edge was peeked, repay the debt
|
||||
if (this->peeked_edge_ && state.count_ > 0) {
|
||||
if (this->peeked_edge_ && this->get_->count_ > 0) {
|
||||
this->peeked_edge_ = false;
|
||||
state.count_--;
|
||||
this->get_->count_--; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
}
|
||||
|
||||
// If there is an unprocessed edge, and filter_us_ has passed since, count this edge early.
|
||||
// Wait for the debt to be repaid before counting another unprocessed edge early.
|
||||
if (!this->peeked_edge_ && state.last_rising_edge_us_ != state.last_detected_edge_us_ &&
|
||||
now - state.last_rising_edge_us_ >= this->filter_us_) {
|
||||
// If there is an unprocessed edge, and filter_us_ has passed since, count this edge early
|
||||
if (this->get_->last_rising_edge_us_ != this->get_->last_detected_edge_us_ &&
|
||||
now - this->get_->last_rising_edge_us_ >= this->filter_us_) {
|
||||
this->peeked_edge_ = true;
|
||||
state.last_detected_edge_us_ = state.last_rising_edge_us_;
|
||||
state.count_++;
|
||||
this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_;
|
||||
this->get_->count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
}
|
||||
|
||||
// Check if we detected a pulse this loop
|
||||
if (state.count_ > 0) {
|
||||
if (this->get_->count_ > 0) {
|
||||
// Keep a running total of pulses if a total sensor is configured
|
||||
if (this->total_sensor_ != nullptr) {
|
||||
this->total_pulses_ += state.count_;
|
||||
this->total_pulses_ += this->get_->count_;
|
||||
const uint32_t total = this->total_pulses_;
|
||||
this->total_sensor_->publish_state(total);
|
||||
}
|
||||
@@ -97,15 +94,15 @@ void PulseMeterSensor::loop() {
|
||||
this->meter_state_ = MeterState::RUNNING;
|
||||
} break;
|
||||
case MeterState::RUNNING: {
|
||||
uint32_t delta_us = state.last_detected_edge_us_ - this->last_processed_edge_us_;
|
||||
float pulse_width_us = delta_us / float(state.count_);
|
||||
ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, state.count_,
|
||||
pulse_width_us);
|
||||
uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_;
|
||||
float pulse_width_us = delta_us / float(this->get_->count_);
|
||||
ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us,
|
||||
this->get_->count_, pulse_width_us);
|
||||
this->publish_state((60.0f * 1000000.0f) / pulse_width_us);
|
||||
} break;
|
||||
}
|
||||
|
||||
this->last_processed_edge_us_ = state.last_detected_edge_us_;
|
||||
this->last_processed_edge_us_ = this->get_->last_detected_edge_us_;
|
||||
}
|
||||
// No detected edges this loop
|
||||
else {
|
||||
@@ -144,14 +141,14 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) {
|
||||
// This is an interrupt handler - we can't call any virtual method from this method
|
||||
// Get the current time before we do anything else so the measurements are consistent
|
||||
const uint32_t now = micros();
|
||||
auto &edge_state = sensor->edge_state_;
|
||||
auto &state = sensor->state_;
|
||||
auto &state = sensor->edge_state_;
|
||||
auto &set = *sensor->set_;
|
||||
|
||||
if ((now - edge_state.last_sent_edge_us_) >= sensor->filter_us_) {
|
||||
edge_state.last_sent_edge_us_ = now;
|
||||
state.last_detected_edge_us_ = now;
|
||||
state.last_rising_edge_us_ = now;
|
||||
state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
if ((now - state.last_sent_edge_us_) >= sensor->filter_us_) {
|
||||
state.last_sent_edge_us_ = now;
|
||||
set.last_detected_edge_us_ = now;
|
||||
set.last_rising_edge_us_ = now;
|
||||
set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
}
|
||||
|
||||
// This ISR is bound to rising edges, so the pin is high
|
||||
@@ -163,26 +160,26 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) {
|
||||
// Get the current time before we do anything else so the measurements are consistent
|
||||
const uint32_t now = micros();
|
||||
const bool pin_val = sensor->isr_pin_.digital_read();
|
||||
auto &pulse_state = sensor->pulse_state_;
|
||||
auto &state = sensor->state_;
|
||||
auto &state = sensor->pulse_state_;
|
||||
auto &set = *sensor->set_;
|
||||
|
||||
// Filter length has passed since the last interrupt
|
||||
const bool length = now - pulse_state.last_intr_ >= sensor->filter_us_;
|
||||
const bool length = now - state.last_intr_ >= sensor->filter_us_;
|
||||
|
||||
if (length && pulse_state.latched_ && !sensor->last_pin_val_) { // Long enough low edge
|
||||
pulse_state.latched_ = false;
|
||||
} else if (length && !pulse_state.latched_ && sensor->last_pin_val_) { // Long enough high edge
|
||||
pulse_state.latched_ = true;
|
||||
state.last_detected_edge_us_ = pulse_state.last_intr_;
|
||||
state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
if (length && state.latched_ && !sensor->last_pin_val_) { // Long enough low edge
|
||||
state.latched_ = false;
|
||||
} else if (length && !state.latched_ && sensor->last_pin_val_) { // Long enough high edge
|
||||
state.latched_ = true;
|
||||
set.last_detected_edge_us_ = state.last_intr_;
|
||||
set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
}
|
||||
|
||||
// Due to order of operations this includes
|
||||
// length && latched && rising (just reset from a long low edge)
|
||||
// !latched && (rising || high) (noise on the line resetting the potential rising edge)
|
||||
state.last_rising_edge_us_ = !pulse_state.latched_ && pin_val ? now : state.last_detected_edge_us_;
|
||||
set.last_rising_edge_us_ = !state.latched_ && pin_val ? now : set.last_detected_edge_us_;
|
||||
|
||||
pulse_state.last_intr_ = now;
|
||||
state.last_intr_ = now;
|
||||
sensor->last_pin_val_ = pin_val;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,16 +46,17 @@ class PulseMeterSensor : public sensor::Sensor, public Component {
|
||||
uint32_t total_pulses_ = 0;
|
||||
uint32_t last_processed_edge_us_ = 0;
|
||||
|
||||
// This struct and variable are used to pass data between the ISR and loop.
|
||||
// The data from state_ is read and then count_ in state_ is reset in each loop.
|
||||
// This must be done while guarded by an InterruptLock. Use this variable to send data
|
||||
// from the ISR to the loop not the other way around (except for resetting count_).
|
||||
// This struct (and the two pointers) are used to pass data between the ISR and loop.
|
||||
// These two pointers are exchanged each loop.
|
||||
// Use these to send data from the ISR to the loop not the other way around (except for resetting the values).
|
||||
struct State {
|
||||
uint32_t last_detected_edge_us_ = 0;
|
||||
uint32_t last_rising_edge_us_ = 0;
|
||||
uint32_t count_ = 0;
|
||||
};
|
||||
volatile State state_{};
|
||||
State state_[2];
|
||||
volatile State *set_ = state_;
|
||||
volatile State *get_ = state_ + 1;
|
||||
|
||||
// Only use the following variables in the ISR or while guarded by an InterruptLock
|
||||
ISRInternalGPIOPin isr_pin_;
|
||||
|
||||
@@ -219,7 +219,6 @@ async def script_stop_action_to_code(config, action_id, template_arg, args):
|
||||
"script.wait",
|
||||
ScriptWaitAction,
|
||||
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
|
||||
deferred=True,
|
||||
)
|
||||
async def script_wait_action_to_code(config, action_id, template_arg, args):
|
||||
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
|
||||
|
||||
@@ -90,6 +90,7 @@ void IDFUARTComponent::setup() {
|
||||
return;
|
||||
}
|
||||
this->uart_num_ = static_cast<uart_port_t>(next_uart_num++);
|
||||
this->lock_ = xSemaphoreCreateMutex();
|
||||
|
||||
#if (SOC_UART_LP_NUM >= 1)
|
||||
size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN);
|
||||
@@ -101,7 +102,11 @@ void IDFUARTComponent::setup() {
|
||||
this->rx_buffer_size_ = fifo_len * 2;
|
||||
}
|
||||
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
|
||||
this->load_settings(false);
|
||||
|
||||
xSemaphoreGive(this->lock_);
|
||||
}
|
||||
|
||||
void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
@@ -121,20 +126,13 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
constexpr int event_queue_size = 20;
|
||||
QueueHandle_t *event_queue_ptr = &this->uart_event_queue_;
|
||||
#else
|
||||
constexpr int event_queue_size = 0;
|
||||
QueueHandle_t *event_queue_ptr = nullptr;
|
||||
#endif
|
||||
err = uart_driver_install(this->uart_num_, // UART number
|
||||
this->rx_buffer_size_, // RX ring buffer size
|
||||
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
|
||||
// block task until all data has been sent out
|
||||
event_queue_size, // event queue size/depth
|
||||
event_queue_ptr, // event queue
|
||||
0 // Flags used to allocate the interrupt
|
||||
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
|
||||
// block task until all data has been sent out
|
||||
20, // event queue size/depth
|
||||
&this->uart_event_queue_, // event queue
|
||||
0 // Flags used to allocate the interrupt
|
||||
);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
|
||||
@@ -284,7 +282,9 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) {
|
||||
}
|
||||
|
||||
void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
int32_t write_len = uart_write_bytes(this->uart_num_, data, len);
|
||||
xSemaphoreGive(this->lock_);
|
||||
if (write_len != (int32_t) len) {
|
||||
ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len);
|
||||
this->mark_failed();
|
||||
@@ -299,6 +299,7 @@ void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
|
||||
bool IDFUARTComponent::peek_byte(uint8_t *data) {
|
||||
if (!this->check_read_timeout_())
|
||||
return false;
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
if (this->has_peek_) {
|
||||
*data = this->peek_byte_;
|
||||
} else {
|
||||
@@ -310,6 +311,7 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) {
|
||||
this->peek_byte_ = *data;
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(this->lock_);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -318,6 +320,7 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||
int32_t read_len = 0;
|
||||
if (!this->check_read_timeout_(len))
|
||||
return false;
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
if (this->has_peek_) {
|
||||
length_to_read--;
|
||||
*data = this->peek_byte_;
|
||||
@@ -326,6 +329,7 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||
}
|
||||
if (length_to_read > 0)
|
||||
read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS);
|
||||
xSemaphoreGive(this->lock_);
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
|
||||
@@ -338,7 +342,9 @@ size_t IDFUARTComponent::available() {
|
||||
size_t available = 0;
|
||||
esp_err_t err;
|
||||
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
err = uart_get_buffered_data_len(this->uart_num_, &available);
|
||||
xSemaphoreGive(this->lock_);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err));
|
||||
@@ -352,7 +358,9 @@ size_t IDFUARTComponent::available() {
|
||||
|
||||
void IDFUARTComponent::flush() {
|
||||
ESP_LOGVV(TAG, " Flushing");
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
uart_wait_tx_done(this->uart_num_, portMAX_DELAY);
|
||||
xSemaphoreGive(this->lock_);
|
||||
}
|
||||
|
||||
void IDFUARTComponent::check_logger_conflict() {}
|
||||
@@ -376,13 +384,6 @@ void IDFUARTComponent::start_rx_event_task_() {
|
||||
ESP_LOGV(TAG, "RX event task started");
|
||||
}
|
||||
|
||||
// FreeRTOS task that relays UART ISR events to the main loop.
|
||||
// This task exists because wake_loop_threadsafe() is not ISR-safe (it uses a
|
||||
// UDP loopback socket), so we need a task as an ISR-to-main-loop trampoline.
|
||||
// IMPORTANT: This task must NOT call any UART wrapper methods (read_array,
|
||||
// write_array, peek_byte, etc.) or touch has_peek_/peek_byte_ — all reading
|
||||
// is done by the main loop. This task only reads from the event queue and
|
||||
// calls App.wake_loop_threadsafe().
|
||||
void IDFUARTComponent::rx_event_task_func(void *param) {
|
||||
auto *self = static_cast<IDFUARTComponent *>(param);
|
||||
uart_event_t event;
|
||||
@@ -404,14 +405,8 @@ void IDFUARTComponent::rx_event_task_func(void *param) {
|
||||
|
||||
case UART_FIFO_OVF:
|
||||
case UART_BUFFER_FULL:
|
||||
// Don't call uart_flush_input() here — this task does not own the read side.
|
||||
// ESP-IDF examples flush on overflow because the same task handles both events
|
||||
// and reads, so flush and read are serialized. Here, reads happen on the main
|
||||
// loop, so flushing from this task races with read_array() and can destroy data
|
||||
// mid-read. The driver self-heals without an explicit flush: uart_read_bytes()
|
||||
// calls uart_check_buf_full() after each chunk, which moves stashed FIFO bytes
|
||||
// into the ring buffer and re-enables RX interrupts once space is freed.
|
||||
ESP_LOGW(TAG, "FIFO overflow or ring buffer full");
|
||||
ESP_LOGW(TAG, "FIFO overflow or ring buffer full - clearing");
|
||||
uart_flush_input(self->uart_num_);
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
|
||||
@@ -8,13 +8,6 @@
|
||||
|
||||
namespace esphome::uart {
|
||||
|
||||
/// ESP-IDF UART driver wrapper.
|
||||
///
|
||||
/// Thread safety: All public methods must only be called from the main loop.
|
||||
/// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's
|
||||
/// peek byte state (has_peek_/peek_byte_) is not synchronized. The rx_event_task
|
||||
/// (when enabled) must not call any of these methods — it communicates with the
|
||||
/// main loop exclusively via App.wake_loop_threadsafe().
|
||||
class IDFUARTComponent : public UARTComponent, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -33,9 +26,7 @@ class IDFUARTComponent : public UARTComponent, public Component {
|
||||
void flush() override;
|
||||
|
||||
uint8_t get_hw_serial_number() { return this->uart_num_; }
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; }
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Load the UART with the current settings.
|
||||
@@ -55,20 +46,18 @@ class IDFUARTComponent : public UARTComponent, public Component {
|
||||
protected:
|
||||
void check_logger_conflict() override;
|
||||
uart_port_t uart_num_;
|
||||
QueueHandle_t uart_event_queue_;
|
||||
uart_config_t get_config_();
|
||||
SemaphoreHandle_t lock_;
|
||||
|
||||
bool has_peek_{false};
|
||||
uint8_t peek_byte_;
|
||||
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
// RX notification support — runs on a separate FreeRTOS task.
|
||||
// IMPORTANT: rx_event_task_func must NOT call any UART wrapper methods (read_array,
|
||||
// write_array, etc.) or touch has_peek_/peek_byte_. It must only read from the
|
||||
// event queue and call App.wake_loop_threadsafe().
|
||||
// RX notification support
|
||||
void start_rx_event_task_();
|
||||
static void rx_event_task_func(void *param);
|
||||
|
||||
QueueHandle_t uart_event_queue_;
|
||||
TaskHandle_t rx_event_task_handle_{nullptr};
|
||||
#endif // USE_UART_WAKE_LOOP_ON_RX
|
||||
};
|
||||
|
||||
@@ -148,7 +148,6 @@ class USBClient : public Component {
|
||||
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
|
||||
|
||||
protected:
|
||||
void handle_open_state_();
|
||||
TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe)
|
||||
virtual void disconnect();
|
||||
virtual void on_connected() {}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
#include <atomic>
|
||||
#include <span>
|
||||
namespace esphome {
|
||||
namespace usb_host {
|
||||
|
||||
@@ -143,23 +142,18 @@ static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc
|
||||
} while (next_desc != NULL);
|
||||
}
|
||||
#endif
|
||||
// USB string descriptors: bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType).
|
||||
// Character count = (bLength - 2) / 2, max 126 chars + null terminator.
|
||||
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
|
||||
|
||||
static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
|
||||
if (desc == nullptr || desc->bLength < 2)
|
||||
static std::string get_descriptor_string(const usb_str_desc_t *desc) {
|
||||
char buffer[256];
|
||||
if (desc == nullptr)
|
||||
return "(unspecified)";
|
||||
int char_count = (desc->bLength - 2) / 2;
|
||||
char *p = buffer.data();
|
||||
char *end = p + buffer.size() - 1;
|
||||
for (int i = 0; i != char_count && p < end; i++) {
|
||||
char *p = buffer;
|
||||
for (int i = 0; i != desc->bLength / 2; i++) {
|
||||
auto c = desc->wData[i];
|
||||
if (c < 0x100)
|
||||
*p++ = static_cast<char>(c);
|
||||
}
|
||||
*p = '\0';
|
||||
return buffer.data();
|
||||
return {buffer};
|
||||
}
|
||||
|
||||
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
|
||||
@@ -265,63 +259,60 @@ void USBClient::loop() {
|
||||
ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped);
|
||||
}
|
||||
|
||||
if (this->state_ == USB_CLIENT_OPEN) {
|
||||
this->handle_open_state_();
|
||||
}
|
||||
}
|
||||
|
||||
void USBClient::handle_open_state_() {
|
||||
int err;
|
||||
ESP_LOGD(TAG, "Open device %d", this->device_addr_);
|
||||
err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
|
||||
this->state_ = USB_CLIENT_INIT;
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
|
||||
const usb_device_desc_t *desc;
|
||||
err = usb_host_get_device_descriptor(this->device_handle_, &desc);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
|
||||
this->disconnect();
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
|
||||
if (desc->idVendor != this->vid_ || desc->idProduct != this->pid_) {
|
||||
if (this->vid_ != 0 || this->pid_ != 0) {
|
||||
ESP_LOGD(TAG, "Not our device, closing");
|
||||
this->disconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
usb_device_info_t dev_info;
|
||||
err = usb_host_device_info(this->device_handle_, &dev_info);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err));
|
||||
this->disconnect();
|
||||
return;
|
||||
}
|
||||
this->state_ = USB_CLIENT_CONNECTED;
|
||||
char buf_manuf[DESC_STRING_BUF_SIZE];
|
||||
char buf_product[DESC_STRING_BUF_SIZE];
|
||||
char buf_serial[DESC_STRING_BUF_SIZE];
|
||||
ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s",
|
||||
get_descriptor_string(dev_info.str_desc_manufacturer, buf_manuf),
|
||||
get_descriptor_string(dev_info.str_desc_product, buf_product),
|
||||
get_descriptor_string(dev_info.str_desc_serial_num, buf_serial));
|
||||
switch (this->state_) {
|
||||
case USB_CLIENT_OPEN: {
|
||||
int err;
|
||||
ESP_LOGD(TAG, "Open device %d", this->device_addr_);
|
||||
err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
|
||||
this->state_ = USB_CLIENT_INIT;
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
|
||||
const usb_device_desc_t *desc;
|
||||
err = usb_host_get_device_descriptor(this->device_handle_, &desc);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
|
||||
this->disconnect();
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
|
||||
if (desc->idVendor == this->vid_ && desc->idProduct == this->pid_ || this->vid_ == 0 && this->pid_ == 0) {
|
||||
usb_device_info_t dev_info;
|
||||
err = usb_host_device_info(this->device_handle_, &dev_info);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err));
|
||||
this->disconnect();
|
||||
break;
|
||||
}
|
||||
this->state_ = USB_CLIENT_CONNECTED;
|
||||
ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s",
|
||||
get_descriptor_string(dev_info.str_desc_manufacturer).c_str(),
|
||||
get_descriptor_string(dev_info.str_desc_product).c_str(),
|
||||
get_descriptor_string(dev_info.str_desc_serial_num).c_str());
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
const usb_device_desc_t *device_desc;
|
||||
err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
|
||||
if (err == ESP_OK)
|
||||
usb_client_print_device_descriptor(device_desc);
|
||||
const usb_config_desc_t *config_desc;
|
||||
err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
|
||||
if (err == ESP_OK)
|
||||
usb_client_print_config_descriptor(config_desc, nullptr);
|
||||
const usb_device_desc_t *device_desc;
|
||||
err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
|
||||
if (err == ESP_OK)
|
||||
usb_client_print_device_descriptor(device_desc);
|
||||
const usb_config_desc_t *config_desc;
|
||||
err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
|
||||
if (err == ESP_OK)
|
||||
usb_client_print_config_descriptor(config_desc, nullptr);
|
||||
#endif
|
||||
this->on_connected();
|
||||
this->on_connected();
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Not our device, closing");
|
||||
this->disconnect();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void USBClient::on_opened(uint8_t addr) {
|
||||
|
||||
@@ -198,8 +198,7 @@ EntityMatchResult UrlMatch::match_entity(EntityBase *entity) const {
|
||||
|
||||
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
|
||||
// helper for allowing only unique entries in the queue
|
||||
void __attribute__((flatten))
|
||||
DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) {
|
||||
void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) {
|
||||
DeferredEvent item(source, message_generator);
|
||||
|
||||
// Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
|
||||
@@ -558,9 +557,7 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
|
||||
root[ESPHOME_F("device")] = device_name;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ENTITY_ICON
|
||||
root[ESPHOME_F("icon")] = obj->get_icon_ref();
|
||||
#endif
|
||||
root[ESPHOME_F("entity_category")] = obj->get_entity_category();
|
||||
bool is_disabled = obj->is_disabled_by_default();
|
||||
if (is_disabled)
|
||||
@@ -586,7 +583,8 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c
|
||||
|
||||
// Helper to get request detail parameter
|
||||
static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
|
||||
return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE;
|
||||
auto *param = request->getParam(ESPHOME_F("detail"));
|
||||
return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE;
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
@@ -863,10 +861,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
|
||||
}
|
||||
auto call = is_on ? obj->turn_on() : obj->turn_off();
|
||||
|
||||
parse_num_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed);
|
||||
parse_int_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed);
|
||||
|
||||
if (request->hasArg(ESPHOME_F("oscillation"))) {
|
||||
auto speed = request->arg(ESPHOME_F("oscillation"));
|
||||
if (request->hasParam(ESPHOME_F("oscillation"))) {
|
||||
auto speed = request->getParam(ESPHOME_F("oscillation"))->value();
|
||||
auto val = parse_on_off(speed.c_str());
|
||||
switch (val) {
|
||||
case PARSE_ON:
|
||||
@@ -1042,14 +1040,14 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
|
||||
}
|
||||
|
||||
auto traits = obj->get_traits();
|
||||
if ((request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) ||
|
||||
(request->hasArg(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) {
|
||||
if ((request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) ||
|
||||
(request->hasParam(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
|
||||
parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
||||
parse_num_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
|
||||
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());
|
||||
request->send(200);
|
||||
@@ -1108,7 +1106,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
|
||||
auto call = obj->make_call();
|
||||
parse_num_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
|
||||
parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1176,13 +1174,12 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
|
||||
|
||||
auto call = obj->make_call();
|
||||
|
||||
const auto &value = request->arg(ESPHOME_F("value"));
|
||||
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
|
||||
if (!request->hasParam(ESPHOME_F("value"))) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
call.set_date(value.c_str(), value.length());
|
||||
|
||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date);
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1237,13 +1234,12 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
|
||||
|
||||
auto call = obj->make_call();
|
||||
|
||||
const auto &value = request->arg(ESPHOME_F("value"));
|
||||
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
|
||||
if (!request->hasParam(ESPHOME_F("value"))) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
call.set_time(value.c_str(), value.length());
|
||||
|
||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time);
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1297,13 +1293,12 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
|
||||
|
||||
auto call = obj->make_call();
|
||||
|
||||
const auto &value = request->arg(ESPHOME_F("value"));
|
||||
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
|
||||
if (!request->hasParam(ESPHOME_F("value"))) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
call.set_datetime(value.c_str(), value.length());
|
||||
|
||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime);
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1482,14 +1477,10 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
|
||||
parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode);
|
||||
|
||||
// Parse temperature parameters
|
||||
// static_cast needed to disambiguate overloaded setters (float vs optional<float>)
|
||||
using ClimateCall = decltype(call);
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature_high"), call,
|
||||
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_high));
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature_low"), call,
|
||||
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_low));
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature"), call,
|
||||
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature));
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature_high"), call,
|
||||
&decltype(call)::set_target_temperature_high);
|
||||
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());
|
||||
request->send(200);
|
||||
@@ -1730,12 +1721,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
|
||||
}
|
||||
|
||||
auto traits = obj->get_traits();
|
||||
if (request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) {
|
||||
if (request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) {
|
||||
request->send(409);
|
||||
return;
|
||||
}
|
||||
|
||||
parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
||||
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1879,12 +1870,12 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
|
||||
parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode);
|
||||
|
||||
// Parse temperature parameters
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature);
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature_low"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature_low);
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature_high"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature_high);
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature);
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature_low"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature_low);
|
||||
parse_float_param_(request, ESPHOME_F("target_temperature_high"), base_call,
|
||||
&water_heater::WaterHeaterCall::set_target_temperature_high);
|
||||
|
||||
// Parse away mode parameter
|
||||
parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away);
|
||||
@@ -1988,16 +1979,16 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
|
||||
auto call = obj->make_call();
|
||||
|
||||
// Parse carrier frequency (optional)
|
||||
{
|
||||
auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("carrier_frequency")).c_str());
|
||||
if (request->hasParam(ESPHOME_F("carrier_frequency"))) {
|
||||
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str());
|
||||
if (value.has_value()) {
|
||||
call.set_carrier_frequency(*value);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse repeat count (optional, defaults to 1)
|
||||
{
|
||||
auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("repeat_count")).c_str());
|
||||
if (request->hasParam(ESPHOME_F("repeat_count"))) {
|
||||
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("repeat_count"))->value().c_str());
|
||||
if (value.has_value()) {
|
||||
call.set_repeat_count(*value);
|
||||
}
|
||||
@@ -2005,12 +1996,18 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
|
||||
|
||||
// Parse base64url-encoded raw timings (required)
|
||||
// Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping)
|
||||
const auto &data_arg = request->arg(ESPHOME_F("data"));
|
||||
if (!request->hasParam(ESPHOME_F("data"))) {
|
||||
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing 'data' parameter"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate base64url is not empty (also catches missing parameter since arg() returns empty string)
|
||||
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||
if (data_arg.length() == 0) { // NOLINT(readability-container-size-empty)
|
||||
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing or empty 'data' parameter"));
|
||||
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
|
||||
std::string encoded =
|
||||
request->getParam(ESPHOME_F("data"))->value().c_str(); // NOLINT(readability-redundant-string-cstr)
|
||||
|
||||
// Validate base64url is not empty
|
||||
if (encoded.empty()) {
|
||||
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Empty 'data' parameter"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2018,7 +2015,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
|
||||
// it outlives the call - set_raw_timings_base64url stores a pointer, so the string
|
||||
// must remain valid until perform() completes.
|
||||
// ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context.
|
||||
this->defer([call, encoded = std::string(data_arg.c_str(), data_arg.length())]() mutable {
|
||||
this->defer([call, encoded = std::move(encoded)]() mutable {
|
||||
call.set_raw_timings_base64url(encoded);
|
||||
call.perform();
|
||||
});
|
||||
|
||||
@@ -513,9 +513,11 @@ class WebServer : public Controller,
|
||||
template<typename T, typename Ret>
|
||||
void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float),
|
||||
float scale = 1.0f) {
|
||||
auto value = parse_number<float>(request->arg(param_name).c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value / scale);
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value / scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,19 +525,34 @@ class WebServer : public Controller,
|
||||
template<typename T, typename Ret>
|
||||
void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
|
||||
Ret (T::*setter)(uint32_t), uint32_t scale = 1) {
|
||||
auto value = parse_number<uint32_t>(request->arg(param_name).c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value * scale);
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<uint32_t>(request->getParam(param_name)->value().c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Generic helper to parse and apply a numeric parameter
|
||||
template<typename NumT, typename T, typename Ret>
|
||||
void parse_num_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(NumT)) {
|
||||
auto value = parse_number<NumT>(request->arg(param_name).c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value);
|
||||
// Generic helper to parse and apply a float parameter
|
||||
template<typename T, typename Ret>
|
||||
void parse_float_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float)) {
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generic helper to parse and apply an int parameter
|
||||
template<typename T, typename Ret>
|
||||
void parse_int_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(int)) {
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<int>(request->getParam(param_name)->value().c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,9 +560,10 @@ class WebServer : public Controller,
|
||||
template<typename T, typename Ret>
|
||||
void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
|
||||
Ret (T::*setter)(const std::string &)) {
|
||||
if (request->hasArg(param_name)) {
|
||||
const auto &value = request->arg(param_name);
|
||||
(call.*setter)(std::string(value.c_str(), value.length()));
|
||||
if (request->hasParam(param_name)) {
|
||||
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
|
||||
std::string value = request->getParam(param_name)->value().c_str(); // NOLINT(readability-redundant-string-cstr)
|
||||
(call.*setter)(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,9 +573,8 @@ class WebServer : public Controller,
|
||||
// Invalid values are ignored (setter not called)
|
||||
template<typename T, typename Ret>
|
||||
void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(bool)) {
|
||||
const auto ¶m_value = request->arg(param_name);
|
||||
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||
if (param_value.length() > 0) { // NOLINT(readability-container-size-empty)
|
||||
if (request->hasParam(param_name)) {
|
||||
auto param_value = request->getParam(param_name)->value();
|
||||
// First check on/off (default), then true/false (custom)
|
||||
auto val = parse_on_off(param_value.c_str());
|
||||
if (val == PARSE_NONE) {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#ifdef USE_ESP32
|
||||
#include <memory>
|
||||
#include <cstring>
|
||||
#include <cctype>
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "http_parser.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
namespace esphome::web_server_idf {
|
||||
|
||||
static const char *const TAG = "web_server_idf_utils";
|
||||
|
||||
size_t url_decode(char *str) {
|
||||
char *start = str;
|
||||
char *ptr = str, buf;
|
||||
@@ -50,15 +54,32 @@ optional<std::string> request_get_header(httpd_req_t *req, const char *name) {
|
||||
return {str};
|
||||
}
|
||||
|
||||
optional<std::string> request_get_url_query(httpd_req_t *req) {
|
||||
auto len = httpd_req_get_url_query_len(req);
|
||||
if (len == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string str;
|
||||
str.resize(len);
|
||||
|
||||
auto res = httpd_req_get_url_query_str(req, &str[0], len + 1);
|
||||
if (res != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res));
|
||||
return {};
|
||||
}
|
||||
|
||||
return {str};
|
||||
}
|
||||
|
||||
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key) {
|
||||
if (query_url == nullptr || query_len == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Value can't exceed query_len. Use small stack buffer for typical values,
|
||||
// heap fallback for long ones (e.g. base64 IR data) to limit stack usage
|
||||
// since callers may also have stack buffers for the query string.
|
||||
SmallBufferWithHeapFallback<128, char> val(query_len);
|
||||
// Use stack buffer for typical query strings, heap fallback for large ones
|
||||
SmallBufferWithHeapFallback<256, char> val(query_len);
|
||||
|
||||
if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) {
|
||||
return {};
|
||||
}
|
||||
@@ -67,18 +88,6 @@ optional<std::string> query_key_value(const char *query_url, size_t query_len, c
|
||||
return {val.get()};
|
||||
}
|
||||
|
||||
bool query_has_key(const char *query_url, size_t query_len, const char *key) {
|
||||
if (query_url == nullptr || query_len == 0) {
|
||||
return false;
|
||||
}
|
||||
// Minimal buffer — we only care if the key exists, not the value
|
||||
char buf[1];
|
||||
// httpd_query_key_value returns ESP_OK if found, ESP_ERR_HTTPD_RESULT_TRUNC if found
|
||||
// but value truncated (expected with 1-byte buffer), or other errors for invalid input
|
||||
auto err = httpd_query_key_value(query_url, key, buf, sizeof(buf));
|
||||
return err == ESP_OK || err == ESP_ERR_HTTPD_RESULT_TRUNC;
|
||||
}
|
||||
|
||||
// Helper function for case-insensitive string region comparison
|
||||
bool str_ncmp_ci(const char *s1, const char *s2, size_t n) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
|
||||
@@ -13,8 +13,11 @@ size_t url_decode(char *str);
|
||||
|
||||
bool request_has_header(httpd_req_t *req, const char *name);
|
||||
optional<std::string> request_get_header(httpd_req_t *req, const char *name);
|
||||
optional<std::string> request_get_url_query(httpd_req_t *req);
|
||||
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key);
|
||||
bool query_has_key(const char *query_url, size_t query_len, const char *key);
|
||||
inline optional<std::string> query_key_value(const std::string &query_url, const std::string &key) {
|
||||
return query_key_value(query_url.c_str(), query_url.size(), key.c_str());
|
||||
}
|
||||
|
||||
// Helper function for case-insensitive character comparison
|
||||
inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); }
|
||||
|
||||
@@ -393,7 +393,13 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) {
|
||||
}
|
||||
|
||||
// Look up value from query strings
|
||||
auto val = this->find_query_value_(name);
|
||||
optional<std::string> val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name);
|
||||
if (!val.has_value()) {
|
||||
auto url_query = request_get_url_query(*this);
|
||||
if (url_query.has_value()) {
|
||||
val = query_key_value(url_query.value().c_str(), url_query.value().size(), name);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't cache misses to avoid wasting memory when handlers check for
|
||||
// optional parameters that don't exist in the request
|
||||
@@ -406,50 +412,6 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) {
|
||||
return param;
|
||||
}
|
||||
|
||||
/// Search post_query then URL query with a callback.
|
||||
/// Returns first truthy result, or value-initialized default.
|
||||
/// URL query is accessed directly from req->uri (same pattern as url_to()).
|
||||
template<typename Func>
|
||||
static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func)
|
||||
-> decltype(func(nullptr, size_t{0}, name)) {
|
||||
if (!post_query.empty()) {
|
||||
auto result = func(post_query.c_str(), post_query.size(), name);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
// Use httpd API for query length, then access string directly from URI.
|
||||
// http_parser identifies components by offset/length without modifying the URI string.
|
||||
// This is the same pattern used by url_to().
|
||||
auto len = httpd_req_get_url_query_len(req);
|
||||
if (len == 0) {
|
||||
return {};
|
||||
}
|
||||
const char *query = strchr(req->uri, '?');
|
||||
if (query == nullptr) {
|
||||
return {};
|
||||
}
|
||||
query++; // skip '?'
|
||||
return func(query, len, name);
|
||||
}
|
||||
|
||||
optional<std::string> AsyncWebServerRequest::find_query_value_(const char *name) const {
|
||||
return search_query_sources(this->req_, this->post_query_, name,
|
||||
[](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); });
|
||||
}
|
||||
|
||||
bool AsyncWebServerRequest::hasArg(const char *name) {
|
||||
return search_query_sources(this->req_, this->post_query_, name, query_has_key);
|
||||
}
|
||||
|
||||
std::string AsyncWebServerRequest::arg(const char *name) {
|
||||
auto val = this->find_query_value_(name);
|
||||
if (val.has_value()) {
|
||||
return std::move(val.value());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
|
||||
httpd_resp_set_hdr(*this->req_, name, value);
|
||||
}
|
||||
|
||||
@@ -116,8 +116,7 @@ class AsyncWebServerRequest {
|
||||
/// Write URL (without query string) to buffer, returns StringRef pointing to buffer.
|
||||
/// URL is decoded (e.g., %20 -> space).
|
||||
StringRef url_to(std::span<char, URL_BUF_SIZE> buffer) const;
|
||||
// Remove before 2026.9.0
|
||||
ESPDEPRECATED("Use url_to() instead. Removed in 2026.9.0", "2026.3.0")
|
||||
/// Get URL as std::string. Prefer url_to() to avoid heap allocation.
|
||||
std::string url() const {
|
||||
char buffer[URL_BUF_SIZE];
|
||||
return std::string(this->url_to(buffer));
|
||||
@@ -171,8 +170,14 @@ class AsyncWebServerRequest {
|
||||
AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); }
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
bool hasArg(const char *name);
|
||||
std::string arg(const char *name);
|
||||
bool hasArg(const char *name) { return this->hasParam(name); }
|
||||
std::string arg(const char *name) {
|
||||
auto *param = this->getParam(name);
|
||||
if (param) {
|
||||
return param->value();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::string arg(const std::string &name) { return this->arg(name.c_str()); }
|
||||
|
||||
operator httpd_req_t *() const { return this->req_; }
|
||||
@@ -187,7 +192,6 @@ class AsyncWebServerRequest {
|
||||
// is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid
|
||||
// duplicate storage. Only successful lookups are cached to prevent cache pollution when
|
||||
// handlers check for optional parameters that don't exist.
|
||||
optional<std::string> find_query_value_(const char *name) const;
|
||||
std::vector<AsyncWebParameter *> params_;
|
||||
std::string post_query_;
|
||||
AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}
|
||||
|
||||
@@ -487,19 +487,6 @@ bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t
|
||||
return false;
|
||||
}
|
||||
|
||||
void __attribute__((flatten)) WiFiComponent::set_sta_priority(bssid_t bssid, int8_t priority) {
|
||||
for (auto &it : this->sta_priorities_) {
|
||||
if (it.bssid == bssid) {
|
||||
it.priority = priority;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->sta_priorities_.push_back(WiFiSTAPriority{
|
||||
.bssid = bssid,
|
||||
.priority = priority,
|
||||
});
|
||||
}
|
||||
|
||||
void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
// Skip logging during roaming scans to avoid log buffer overflow
|
||||
|
||||
@@ -488,11 +488,20 @@ class WiFiComponent : public Component {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
void set_sta_priority(bssid_t bssid, int8_t priority);
|
||||
void set_sta_priority(const bssid_t bssid, int8_t priority) {
|
||||
for (auto &it : this->sta_priorities_) {
|
||||
if (it.bssid == bssid) {
|
||||
it.priority = priority;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->sta_priorities_.push_back(WiFiSTAPriority{
|
||||
.bssid = bssid,
|
||||
.priority = priority,
|
||||
});
|
||||
}
|
||||
|
||||
network::IPAddresses wifi_sta_ip_addresses();
|
||||
// Remove before 2026.9.0
|
||||
ESPDEPRECATED("Use wifi_ssid_to() instead. Removed in 2026.9.0", "2026.3.0")
|
||||
std::string wifi_ssid();
|
||||
/// Write SSID to buffer without heap allocation.
|
||||
/// Returns pointer to buffer, or empty string if not connected.
|
||||
|
||||
@@ -1083,9 +1083,6 @@ template<std::size_t N> std::string format_hex(const std::array<uint8_t, N> &dat
|
||||
* Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator.
|
||||
* Optionally includes the total byte count in parentheses at the end.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data Pointer to the byte array to format.
|
||||
* @param length Number of bytes in the array.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
@@ -1111,9 +1108,6 @@ std::string format_hex_pretty(const uint8_t *data, size_t length, char separator
|
||||
*
|
||||
* Similar to the byte array version, but formats 16-bit words as 4-digit hex values.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data Pointer to the 16-bit word array to format.
|
||||
* @param length Number of 16-bit words in the array.
|
||||
* @param separator Character to use between hex words (default: '.').
|
||||
@@ -1137,9 +1131,6 @@ std::string format_hex_pretty(const uint16_t *data, size_t length, char separato
|
||||
* Convenience overload for std::vector<uint8_t>. Formats each byte as a two-digit
|
||||
* uppercase hex value with customizable separator.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data Vector of bytes to format.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||
@@ -1163,9 +1154,6 @@ std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator =
|
||||
* Convenience overload for std::vector<uint16_t>. Each 16-bit word is formatted
|
||||
* as a 4-digit uppercase hex value in big-endian order.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data Vector of 16-bit words to format.
|
||||
* @param separator Character to use between hex words (default: '.').
|
||||
* @param show_length Whether to append the word count in parentheses (default: true).
|
||||
@@ -1188,9 +1176,6 @@ std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator
|
||||
* Treats each character in the string as a byte and formats it in hex.
|
||||
* Useful for debugging binary data stored in std::string containers.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data String whose bytes should be formatted as hex.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||
@@ -1213,9 +1198,6 @@ std::string format_hex_pretty(const std::string &data, char separator = '.', boo
|
||||
* Converts the integer to big-endian byte order and formats each byte as hex.
|
||||
* The most significant byte appears first in the output string.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.).
|
||||
* @param val The unsigned integer value to format.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
|
||||
@@ -81,19 +81,6 @@ class StringRef {
|
||||
|
||||
operator std::string() const { return str(); }
|
||||
|
||||
/// Compare with a null-terminated C string (compatible with std::string::compare)
|
||||
int compare(const char *s) const {
|
||||
size_t s_len = std::strlen(s);
|
||||
int result = std::memcmp(base_, s, std::min(len_, s_len));
|
||||
if (result != 0)
|
||||
return result;
|
||||
if (len_ < s_len)
|
||||
return -1;
|
||||
if (len_ > s_len)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Find first occurrence of substring, returns std::string::npos if not found.
|
||||
/// Note: Requires the underlying string to be null-terminated.
|
||||
size_type find(const char *s, size_type pos = 0) const {
|
||||
|
||||
@@ -24,14 +24,11 @@ class RegistryEntry:
|
||||
fun: Callable[..., Any],
|
||||
type_id: "MockObjClass",
|
||||
schema: "Schema",
|
||||
*,
|
||||
deferred: bool = False,
|
||||
):
|
||||
self.name = name
|
||||
self.fun = fun
|
||||
self.type_id = type_id
|
||||
self.raw_schema = schema
|
||||
self.deferred = deferred
|
||||
|
||||
@property
|
||||
def coroutine_fun(self):
|
||||
@@ -52,16 +49,9 @@ class Registry(dict[str, RegistryEntry]):
|
||||
self.base_schema = base_schema or {}
|
||||
self.type_id_key = type_id_key
|
||||
|
||||
def register(
|
||||
self,
|
||||
name: str,
|
||||
type_id: "MockObjClass",
|
||||
schema: "Schema",
|
||||
*,
|
||||
deferred: bool = False,
|
||||
):
|
||||
def register(self, name: str, type_id: "MockObjClass", schema: "Schema"):
|
||||
def decorator(fun: Callable[..., Any]):
|
||||
self[name] = RegistryEntry(name, fun, type_id, schema, deferred=deferred)
|
||||
self[name] = RegistryEntry(name, fun, type_id, schema)
|
||||
return fun
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -136,7 +136,7 @@ extends = common:arduino
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2.260206/esp-idf-v5.5.2.tar.xz
|
||||
|
||||
framework = arduino, espidf ; Arduino as an ESP-IDF component
|
||||
lib_deps =
|
||||
@@ -171,7 +171,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
|
||||
extends = common:idf
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2.260206/esp-idf-v5.5.2.tar.xz
|
||||
|
||||
framework = espidf
|
||||
lib_deps =
|
||||
|
||||
@@ -2020,8 +2020,6 @@ def build_message_type(
|
||||
|
||||
# Collect fixed_vector fields for custom decode generation
|
||||
fixed_vector_fields = []
|
||||
# Collect fields with (null_terminate) = true option
|
||||
null_terminate_fields = []
|
||||
|
||||
for field in desc.field:
|
||||
# Skip deprecated fields completely
|
||||
@@ -2064,10 +2062,6 @@ def build_message_type(
|
||||
|
||||
ti = create_field_type_info(field, needs_decode, needs_encode)
|
||||
|
||||
# Collect fields with (null_terminate) = true for post-decode null-termination
|
||||
if needs_decode and get_field_opt(field, pb.null_terminate, False):
|
||||
null_terminate_fields.append(ti.field_name)
|
||||
|
||||
# Skip field declarations for fields that are in the base class
|
||||
# but include their encode/decode logic
|
||||
if field.name not in common_field_names:
|
||||
@@ -2174,8 +2168,8 @@ def build_message_type(
|
||||
prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;"
|
||||
protected_content.insert(0, prot)
|
||||
|
||||
# Generate custom decode() override for messages with FixedVector or null_terminate fields
|
||||
if fixed_vector_fields or null_terminate_fields:
|
||||
# Generate custom decode() override for messages with FixedVector fields
|
||||
if fixed_vector_fields:
|
||||
# Generate the decode() implementation in cpp
|
||||
o = f"void {desc.name}::decode(const uint8_t *buffer, size_t length) {{\n"
|
||||
# Count and init each FixedVector field
|
||||
@@ -2184,13 +2178,6 @@ def build_message_type(
|
||||
o += f" this->{field_name}.init(count_{field_name});\n"
|
||||
# Call parent decode to populate the fields
|
||||
o += " ProtoDecodableMessage::decode(buffer, length);\n"
|
||||
# Null-terminate fields marked with (null_terminate) = true in-place.
|
||||
# Safe: decode is complete, byte after string was already parsed (next field tag)
|
||||
# or is the +1 reserved byte at end of rx_buf_.
|
||||
for field_name in null_terminate_fields:
|
||||
o += f" if (!this->{field_name}.empty()) {{\n"
|
||||
o += f" const_cast<char *>(this->{field_name}.c_str())[this->{field_name}.size()] = '\\0';\n"
|
||||
o += " }\n"
|
||||
o += "}\n"
|
||||
cpp += o
|
||||
# Generate the decode() declaration in header (public method)
|
||||
|
||||
@@ -369,7 +369,7 @@ def get_logger_tags():
|
||||
"api.service",
|
||||
]
|
||||
for file in CORE_COMPONENTS_PATH.rglob("*.cpp"):
|
||||
data = file.read_text(encoding="utf-8")
|
||||
data = file.read_text()
|
||||
match = pattern.search(data)
|
||||
if match:
|
||||
tags.append(match.group(1))
|
||||
|
||||
@@ -270,14 +270,6 @@ async def test_alarm_control_panel_state_transitions(
|
||||
# The chime_sensor has chime: true, so opening it while disarmed
|
||||
# should trigger on_chime callback
|
||||
|
||||
# Set up future for the on_ready from opening the chime sensor
|
||||
# (alarm becomes "not ready" when chime sensor opens).
|
||||
# We must wait for this BEFORE creating the close future, otherwise
|
||||
# the open event's log can arrive late and resolve the close future,
|
||||
# causing the test to proceed before the chime close is processed.
|
||||
ready_after_chime_open: asyncio.Future[bool] = loop.create_future()
|
||||
ready_futures.append(ready_after_chime_open)
|
||||
|
||||
# We're currently DISARMED - open the chime sensor
|
||||
client.switch_command(chime_switch_info.key, True)
|
||||
|
||||
@@ -287,18 +279,11 @@ async def test_alarm_control_panel_state_transitions(
|
||||
except TimeoutError:
|
||||
pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}")
|
||||
|
||||
# Wait for the on_ready from the chime sensor opening
|
||||
try:
|
||||
await asyncio.wait_for(ready_after_chime_open, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"on_ready callback not fired when chime sensor opened. "
|
||||
f"Log lines: {log_lines[-20:]}"
|
||||
)
|
||||
|
||||
# Now create the future for the close event and close the sensor.
|
||||
# Since we waited for the open event above, the close event's
|
||||
# on_ready log cannot be confused with the open event's.
|
||||
# Close the chime sensor and wait for alarm to become ready again
|
||||
# We need to wait for this transition before testing door sensor,
|
||||
# otherwise there's a race where the door sensor state change could
|
||||
# arrive before the chime sensor state change, leaving the alarm in
|
||||
# a continuous "not ready" state with no on_ready callback fired.
|
||||
ready_after_chime_close: asyncio.Future[bool] = loop.create_future()
|
||||
ready_futures.append(ready_after_chime_close)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user