mirror of
https://github.com/esphome/esphome.git
synced 2026-02-15 22:09:36 -07:00
Compare commits
9 Commits
wifi-memcp
...
api-string
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a51fcf9be2 | ||
|
|
b04e427f01 | ||
|
|
0b2f79480b | ||
|
|
e0c03b2dfa | ||
|
|
7dff631dcb | ||
|
|
36aba385af | ||
|
|
136d17366f | ||
|
|
db7870ef5f | ||
|
|
bbc88d92ea |
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
|
- name: Build and push to ghcr by digest
|
||||||
id: build-ghcr
|
id: build-ghcr
|
||||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILD_SUMMARY: false
|
DOCKER_BUILD_SUMMARY: false
|
||||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||||
@@ -73,7 +73,7 @@ runs:
|
|||||||
|
|
||||||
- name: Build and push to dockerhub by digest
|
- name: Build and push to dockerhub by digest
|
||||||
id: build-dockerhub
|
id: build-dockerhub
|
||||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILD_SUMMARY: false
|
DOCKER_BUILD_SUMMARY: false
|
||||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS b
|
|||||||
ARG BUILD_TYPE
|
ARG BUILD_TYPE
|
||||||
FROM base-source-${BUILD_TYPE} AS base
|
FROM base-source-${BUILD_TYPE} AS base
|
||||||
|
|
||||||
RUN git config --system --add safe.directory "*"
|
RUN git config --system --add safe.directory "*" \
|
||||||
|
&& git config --system advice.detachedHead false
|
||||||
|
|
||||||
# Install build tools for Python packages that require compilation
|
# Install build tools for Python packages that require compilation
|
||||||
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
|
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
|
||||||
|
|||||||
@@ -57,8 +57,14 @@ def maybe_conf(conf, *validators):
|
|||||||
return validate
|
return validate
|
||||||
|
|
||||||
|
|
||||||
def register_action(name: str, action_type: MockObjClass, schema: cv.Schema):
|
def register_action(
|
||||||
return ACTION_REGISTRY.register(name, action_type, schema)
|
name: str,
|
||||||
|
action_type: MockObjClass,
|
||||||
|
schema: cv.Schema,
|
||||||
|
*,
|
||||||
|
deferred: bool = False,
|
||||||
|
):
|
||||||
|
return ACTION_REGISTRY.register(name, action_type, schema, deferred=deferred)
|
||||||
|
|
||||||
|
|
||||||
def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema):
|
def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema):
|
||||||
@@ -335,7 +341,10 @@ async def component_is_idle_condition_to_code(
|
|||||||
|
|
||||||
|
|
||||||
@register_action(
|
@register_action(
|
||||||
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
|
"delay",
|
||||||
|
DelayAction,
|
||||||
|
cv.templatable(cv.positive_time_period_milliseconds),
|
||||||
|
deferred=True,
|
||||||
)
|
)
|
||||||
async def delay_action_to_code(
|
async def delay_action_to_code(
|
||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
@@ -445,7 +454,7 @@ _validate_wait_until = cv.maybe_simple_value(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
|
@register_action("wait_until", WaitUntilAction, _validate_wait_until, deferred=True)
|
||||||
async def wait_until_action_to_code(
|
async def wait_until_action_to_code(
|
||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
action_id: ID,
|
action_id: ID,
|
||||||
@@ -578,6 +587,26 @@ async def build_condition_list(
|
|||||||
return conditions
|
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(
|
async def build_automation(
|
||||||
trigger: MockObj, args: TemplateArgsType, config: ConfigType
|
trigger: MockObj, args: TemplateArgsType, config: ConfigType
|
||||||
) -> MockObj:
|
) -> MockObj:
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = {
|
|||||||
"bool": cg.bool_,
|
"bool": cg.bool_,
|
||||||
"int": cg.int32,
|
"int": cg.int32,
|
||||||
"float": cg.float_,
|
"float": cg.float_,
|
||||||
"string": cg.std_string,
|
"string": cg.StringRef,
|
||||||
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
|
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
|
||||||
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
|
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
|
||||||
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
|
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
|
||||||
@@ -380,9 +380,16 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
if is_optional:
|
if is_optional:
|
||||||
func_args.append((cg.bool_, "return_response"))
|
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] = []
|
service_arg_names: list[str] = []
|
||||||
for name, var_ in conf[CONF_VARIABLES].items():
|
for name, var_ in conf[CONF_VARIABLES].items():
|
||||||
native = SERVICE_ARG_NATIVE_TYPES[var_]
|
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)
|
service_template_args.append(native)
|
||||||
func_args.append((native, name))
|
func_args.append((native, name))
|
||||||
service_arg_names.append(name)
|
service_arg_names.append(name)
|
||||||
|
|||||||
@@ -824,7 +824,7 @@ message HomeAssistantStateResponse {
|
|||||||
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
|
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
|
||||||
|
|
||||||
string entity_id = 1;
|
string entity_id = 1;
|
||||||
string state = 2;
|
string state = 2 [(null_terminate) = true];
|
||||||
string attribute = 3;
|
string attribute = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -882,7 +882,7 @@ message ExecuteServiceArgument {
|
|||||||
bool bool_ = 1;
|
bool bool_ = 1;
|
||||||
int32 legacy_int = 2;
|
int32 legacy_int = 2;
|
||||||
float float_ = 3;
|
float float_ = 3;
|
||||||
string string_ = 4;
|
string string_ = 4 [(null_terminate) = true];
|
||||||
// ESPHome 1.14 (api v1.3) make int a signed value
|
// ESPHome 1.14 (api v1.3) make int a signed value
|
||||||
sint32 int_ = 5;
|
sint32 int_ = 5;
|
||||||
repeated bool bool_array = 6 [packed=false, (fixed_vector) = true];
|
repeated bool bool_array = 6 [packed=false, (fixed_vector) = true];
|
||||||
|
|||||||
@@ -1683,31 +1683,18 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (auto &it : this->parent_->get_state_subs()) {
|
for (auto &it : this->parent_->get_state_subs()) {
|
||||||
// Compare entity_id: check length matches and content matches
|
if (msg.entity_id != it.entity_id) {
|
||||||
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare attribute: either both have matching attribute, or both have none
|
// Compare attribute: either both have matching attribute, or both have none
|
||||||
size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
|
// it.attribute can be nullptr (meaning no attribute filter)
|
||||||
if (sub_attr_len != msg.attribute.size() ||
|
if (it.attribute != nullptr ? msg.attribute != it.attribute : !msg.attribute.empty()) {
|
||||||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute.c_str(), sub_attr_len) != 0)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create null-terminated state for callback (parse_number needs null-termination)
|
// msg.state is already null-terminated in-place after protobuf decode
|
||||||
// HA state max length is 255 characters, but attributes can be much longer
|
it.callback(msg.state);
|
||||||
// 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
|
#endif
|
||||||
@@ -1864,6 +1851,8 @@ void APIConnection::on_fatal_error() {
|
|||||||
this->flags_.remove = true;
|
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,
|
void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
||||||
uint8_t aux_data_index) {
|
uint8_t aux_data_index) {
|
||||||
// Check if we already have a message of this type for this entity
|
// Check if we already have a message of this type for this entity
|
||||||
@@ -1880,7 +1869,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// No existing item found (or event), add new one
|
// No existing item found (or event), add new one
|
||||||
items.push_back({entity, message_type, estimated_size, aux_data_index});
|
this->push_item({entity, message_type, estimated_size, aux_data_index});
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
|
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
|
||||||
@@ -1888,7 +1877,7 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t me
|
|||||||
// This avoids expensive vector::insert which shifts all elements
|
// This avoids expensive vector::insert which shifts all elements
|
||||||
// Note: We only ever have one high-priority message at a time (ping OR disconnect)
|
// 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
|
// If we're disconnecting, pings are blocked, so this simple swap is sufficient
|
||||||
items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED});
|
this->push_item({entity, message_type, estimated_size, AUX_DATA_UNUSED});
|
||||||
if (items.size() > 1) {
|
if (items.size() > 1) {
|
||||||
// Swap the new high-priority item to the front
|
// Swap the new high-priority item to the front
|
||||||
std::swap(items.front(), items.back());
|
std::swap(items.front(), items.back());
|
||||||
|
|||||||
@@ -541,6 +541,8 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
uint8_t aux_data_index = AUX_DATA_UNUSED);
|
uint8_t aux_data_index = AUX_DATA_UNUSED);
|
||||||
// Add item to the front of the batch (for high priority messages like ping)
|
// 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);
|
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
|
// Clear all items
|
||||||
void clear() {
|
void clear() {
|
||||||
|
|||||||
@@ -138,10 +138,12 @@ APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func
|
|||||||
|
|
||||||
/// Run through handshake messages (if in that phase)
|
/// Run through handshake messages (if in that phase)
|
||||||
APIError APINoiseFrameHelper::loop() {
|
APIError APINoiseFrameHelper::loop() {
|
||||||
// During handshake phase, process as many actions as possible until we can't progress
|
// Cache ready() outside the loop. On ESP8266 LWIP raw TCP, ready() returns false once
|
||||||
// socket_->ready() stays true until next main loop, but state_action() will return
|
// the rx buffer is consumed. Re-checking each iteration would block handshake writes
|
||||||
// WOULD_BLOCK when no more data is available to read
|
// that must follow reads, deadlocking the handshake. state_action() will return
|
||||||
while (state_ != State::DATA && this->socket_->ready()) {
|
// WOULD_BLOCK when no more data is available to read.
|
||||||
|
bool socket_ready = this->socket_->ready();
|
||||||
|
while (state_ != State::DATA && socket_ready) {
|
||||||
APIError err = state_action_();
|
APIError err = state_action_();
|
||||||
if (err == APIError::WOULD_BLOCK) {
|
if (err == APIError::WOULD_BLOCK) {
|
||||||
break;
|
break;
|
||||||
@@ -199,9 +201,10 @@ APIError APINoiseFrameHelper::try_read_frame_() {
|
|||||||
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
|
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reserve space for body
|
// Reserve space for body (+1 for null terminator so protobuf StringRef fields
|
||||||
if (this->rx_buf_.size() != msg_size) {
|
// can be safely null-terminated in-place after decode)
|
||||||
this->rx_buf_.resize(msg_size);
|
if (this->rx_buf_.size() != msg_size + 1) {
|
||||||
|
this->rx_buf_.resize(msg_size + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rx_buf_len_ < msg_size) {
|
if (rx_buf_len_ < msg_size) {
|
||||||
|
|||||||
@@ -163,9 +163,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
|
|||||||
}
|
}
|
||||||
// header reading done
|
// header reading done
|
||||||
|
|
||||||
// Reserve space for body
|
// Reserve space for body (+1 for null terminator so protobuf StringRef fields
|
||||||
if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
|
// can be safely null-terminated in-place after decode)
|
||||||
this->rx_buf_.resize(this->rx_header_parsed_len_);
|
if (this->rx_buf_.size() != this->rx_header_parsed_len_ + 1) {
|
||||||
|
this->rx_buf_.resize(this->rx_header_parsed_len_ + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rx_buf_len_ < rx_header_parsed_len_) {
|
if (rx_buf_len_ < rx_header_parsed_len_) {
|
||||||
|
|||||||
@@ -90,4 +90,13 @@ extend google.protobuf.FieldOptions {
|
|||||||
// - uint16_t <field>_length_{0};
|
// - uint16_t <field>_length_{0};
|
||||||
// - uint16_t <field>_count_{0};
|
// - uint16_t <field>_count_{0};
|
||||||
optional bool packed_buffer = 50015 [default=false];
|
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,6 +953,12 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
|
|||||||
}
|
}
|
||||||
return true;
|
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
|
#endif
|
||||||
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
@@ -1057,6 +1063,9 @@ void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
|
|||||||
uint32_t count_string_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 9);
|
uint32_t count_string_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 9);
|
||||||
this->string_array.init(count_string_array);
|
this->string_array.init(count_string_array);
|
||||||
ProtoDecodableMessage::decode(buffer, length);
|
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) {
|
bool ExecuteServiceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
|
|||||||
@@ -1095,6 +1095,7 @@ class HomeAssistantStateResponse final : public ProtoDecodableMessage {
|
|||||||
StringRef entity_id{};
|
StringRef entity_id{};
|
||||||
StringRef state{};
|
StringRef state{};
|
||||||
StringRef attribute{};
|
StringRef attribute{};
|
||||||
|
void decode(const uint8_t *buffer, size_t length) override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *dump_to(DumpBuffer &out) const override;
|
const char *dump_to(DumpBuffer &out) const override;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -148,12 +148,16 @@ void APIServer::loop() {
|
|||||||
while (client_index < this->clients_.size()) {
|
while (client_index < this->clients_.size()) {
|
||||||
auto &client = this->clients_[client_index];
|
auto &client = this->clients_[client_index];
|
||||||
|
|
||||||
|
// Common case: process active client
|
||||||
|
if (!client->flags_.remove) {
|
||||||
|
client->loop();
|
||||||
|
}
|
||||||
|
// Handle disconnection promptly - close socket to free LWIP PCB
|
||||||
|
// resources and prevent retransmit crashes on ESP8266.
|
||||||
if (client->flags_.remove) {
|
if (client->flags_.remove) {
|
||||||
// Rare case: handle disconnection (don't increment - swapped element needs processing)
|
// Rare case: handle disconnection (don't increment - swapped element needs processing)
|
||||||
this->remove_client_(client_index);
|
this->remove_client_(client_index);
|
||||||
} else {
|
} else {
|
||||||
// Common case: process active client
|
|
||||||
client->loop();
|
|
||||||
client_index++;
|
client_index++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,7 +199,7 @@ void APIServer::remove_client_(size_t client_index) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIServer::accept_new_connections_() {
|
void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||||
while (true) {
|
while (true) {
|
||||||
struct sockaddr_storage source_addr;
|
struct sockaddr_storage source_addr;
|
||||||
socklen_t addr_len = sizeof(source_addr);
|
socklen_t addr_len = sizeof(source_addr);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "user_services.h"
|
#include "user_services.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/string_ref.h"
|
||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
@@ -11,6 +12,8 @@ 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<> 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_; }
|
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
|
// 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) {
|
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
|
||||||
@@ -61,6 +64,8 @@ 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<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<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
|
||||||
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
|
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
|
// 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; }
|
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ async def script_stop_action_to_code(config, action_id, template_arg, args):
|
|||||||
"script.wait",
|
"script.wait",
|
||||||
ScriptWaitAction,
|
ScriptWaitAction,
|
||||||
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
|
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):
|
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])
|
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ class USBClient : public Component {
|
|||||||
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
|
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void handle_open_state_();
|
||||||
TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe)
|
TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe)
|
||||||
virtual void disconnect();
|
virtual void disconnect();
|
||||||
virtual void on_connected() {}
|
virtual void on_connected() {}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <span>
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace usb_host {
|
namespace usb_host {
|
||||||
|
|
||||||
@@ -142,18 +143,23 @@ static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc
|
|||||||
} while (next_desc != NULL);
|
} while (next_desc != NULL);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
static std::string get_descriptor_string(const usb_str_desc_t *desc) {
|
// USB string descriptors: bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType).
|
||||||
char buffer[256];
|
// Character count = (bLength - 2) / 2, max 126 chars + null terminator.
|
||||||
if (desc == nullptr)
|
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)
|
||||||
return "(unspecified)";
|
return "(unspecified)";
|
||||||
char *p = buffer;
|
int char_count = (desc->bLength - 2) / 2;
|
||||||
for (int i = 0; i != desc->bLength / 2; i++) {
|
char *p = buffer.data();
|
||||||
|
char *end = p + buffer.size() - 1;
|
||||||
|
for (int i = 0; i != char_count && p < end; i++) {
|
||||||
auto c = desc->wData[i];
|
auto c = desc->wData[i];
|
||||||
if (c < 0x100)
|
if (c < 0x100)
|
||||||
*p++ = static_cast<char>(c);
|
*p++ = static_cast<char>(c);
|
||||||
}
|
}
|
||||||
*p = '\0';
|
*p = '\0';
|
||||||
return {buffer};
|
return buffer.data();
|
||||||
}
|
}
|
||||||
|
|
||||||
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
|
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
|
||||||
@@ -259,60 +265,63 @@ void USBClient::loop() {
|
|||||||
ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped);
|
ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this->state_) {
|
if (this->state_ == USB_CLIENT_OPEN) {
|
||||||
case USB_CLIENT_OPEN: {
|
this->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) {
|
void USBClient::handle_open_state_() {
|
||||||
ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
|
int err;
|
||||||
this->state_ = USB_CLIENT_INIT;
|
ESP_LOGD(TAG, "Open device %d", this->device_addr_);
|
||||||
break;
|
err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_);
|
||||||
}
|
if (err != ESP_OK) {
|
||||||
ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
|
ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
|
||||||
const usb_device_desc_t *desc;
|
this->state_ = USB_CLIENT_INIT;
|
||||||
err = usb_host_get_device_descriptor(this->device_handle_, &desc);
|
return;
|
||||||
if (err != ESP_OK) {
|
}
|
||||||
ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
|
ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
|
||||||
this->disconnect();
|
const usb_device_desc_t *desc;
|
||||||
} else {
|
err = usb_host_get_device_descriptor(this->device_handle_, &desc);
|
||||||
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
|
if (err != ESP_OK) {
|
||||||
if (desc->idVendor == this->vid_ && desc->idProduct == this->pid_ || this->vid_ == 0 && this->pid_ == 0) {
|
ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
|
||||||
usb_device_info_t dev_info;
|
this->disconnect();
|
||||||
err = usb_host_device_info(this->device_handle_, &dev_info);
|
return;
|
||||||
if (err != ESP_OK) {
|
}
|
||||||
ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err));
|
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
|
||||||
this->disconnect();
|
if (desc->idVendor != this->vid_ || desc->idProduct != this->pid_) {
|
||||||
break;
|
if (this->vid_ != 0 || this->pid_ != 0) {
|
||||||
}
|
ESP_LOGD(TAG, "Not our device, closing");
|
||||||
this->state_ = USB_CLIENT_CONNECTED;
|
this->disconnect();
|
||||||
ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s",
|
return;
|
||||||
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());
|
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));
|
||||||
|
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
const usb_device_desc_t *device_desc;
|
const usb_device_desc_t *device_desc;
|
||||||
err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
|
err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
|
||||||
if (err == ESP_OK)
|
if (err == ESP_OK)
|
||||||
usb_client_print_device_descriptor(device_desc);
|
usb_client_print_device_descriptor(device_desc);
|
||||||
const usb_config_desc_t *config_desc;
|
const usb_config_desc_t *config_desc;
|
||||||
err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
|
err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
|
||||||
if (err == ESP_OK)
|
if (err == ESP_OK)
|
||||||
usb_client_print_config_descriptor(config_desc, nullptr);
|
usb_client_print_config_descriptor(config_desc, nullptr);
|
||||||
#endif
|
#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) {
|
void USBClient::on_opened(uint8_t addr) {
|
||||||
|
|||||||
@@ -198,7 +198,8 @@ EntityMatchResult UrlMatch::match_entity(EntityBase *entity) const {
|
|||||||
|
|
||||||
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
|
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
|
||||||
// helper for allowing only unique entries in the queue
|
// helper for allowing only unique entries in the queue
|
||||||
void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) {
|
void __attribute__((flatten))
|
||||||
|
DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) {
|
||||||
DeferredEvent item(source, 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
|
// Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <type_traits>
|
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
|
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
|
||||||
@@ -488,6 +487,19 @@ bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t
|
|||||||
return false;
|
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) {
|
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
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
// Skip logging during roaming scans to avoid log buffer overflow
|
// Skip logging during roaming scans to avoid log buffer overflow
|
||||||
@@ -1320,61 +1332,20 @@ void WiFiComponent::start_scanning() {
|
|||||||
// Using insertion sort instead of std::stable_sort saves flash memory
|
// Using insertion sort instead of std::stable_sort saves flash memory
|
||||||
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
|
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
|
||||||
// IMPORTANT: This sort is stable (preserves relative order of equal elements)
|
// IMPORTANT: This sort is stable (preserves relative order of equal elements)
|
||||||
//
|
|
||||||
// Uses raw memcpy instead of copy assignment to avoid CompactString's
|
|
||||||
// destructor/constructor overhead (heap delete[]/new[] for long SSIDs).
|
|
||||||
// Copy assignment calls ~CompactString() then placement-new for every shift,
|
|
||||||
// which means delete[]/new[] per shift for heap-allocated SSIDs. With 70+
|
|
||||||
// networks (e.g., captive portal showing full scan results), this caused
|
|
||||||
// event loop blocking from hundreds of heap operations in a tight loop.
|
|
||||||
//
|
|
||||||
// This is safe because we're permuting elements within the same array —
|
|
||||||
// each slot is overwritten exactly once, so no ownership duplication occurs.
|
|
||||||
// All members of WiFiScanResult are either trivially copyable (bssid, channel,
|
|
||||||
// rssi, priority, flags) or CompactString, which stores either inline data or
|
|
||||||
// a heap pointer — never a self-referential pointer (unlike std::string's SSO
|
|
||||||
// on some implementations). This was not possible before PR#13472 replaced
|
|
||||||
// std::string with CompactString, since std::string's internal layout is
|
|
||||||
// implementation-defined and may use self-referential pointers.
|
|
||||||
//
|
|
||||||
// TODO: If C++ standardizes std::trivially_relocatable, add the assertion for
|
|
||||||
// WiFiScanResult/CompactString here to formally express the memcpy safety guarantee.
|
|
||||||
template<typename VectorType> static void insertion_sort_scan_results(VectorType &results) {
|
template<typename VectorType> static void insertion_sort_scan_results(VectorType &results) {
|
||||||
// memcpy-based sort requires no self-referential pointers or virtual dispatch.
|
|
||||||
// These static_asserts guard the assumptions. If any fire, the memcpy sort
|
|
||||||
// must be reviewed for safety before updating the expected values.
|
|
||||||
//
|
|
||||||
// No vtable pointers (memcpy would corrupt vptr)
|
|
||||||
static_assert(!std::is_polymorphic<WiFiScanResult>::value, "WiFiScanResult must not have vtable");
|
|
||||||
static_assert(!std::is_polymorphic<CompactString>::value, "CompactString must not have vtable");
|
|
||||||
// Standard layout ensures predictable memory layout with no virtual bases
|
|
||||||
// and no mixed-access-specifier reordering
|
|
||||||
static_assert(std::is_standard_layout<WiFiScanResult>::value, "WiFiScanResult must be standard layout");
|
|
||||||
static_assert(std::is_standard_layout<CompactString>::value, "CompactString must be standard layout");
|
|
||||||
// Size checks catch added/removed fields that may need safety review
|
|
||||||
static_assert(sizeof(WiFiScanResult) == 32, "WiFiScanResult size changed - verify memcpy sort is still safe");
|
|
||||||
static_assert(sizeof(CompactString) == 20, "CompactString size changed - verify memcpy sort is still safe");
|
|
||||||
// Alignment must match for reinterpret_cast of key_buf to be valid
|
|
||||||
static_assert(alignof(WiFiScanResult) <= alignof(std::max_align_t), "WiFiScanResult alignment exceeds max_align_t");
|
|
||||||
const size_t size = results.size();
|
const size_t size = results.size();
|
||||||
constexpr size_t elem_size = sizeof(WiFiScanResult);
|
|
||||||
// Suppress warnings for intentional memcpy on non-trivially-copyable type.
|
|
||||||
// Safety is guaranteed by the static_asserts above and the permutation invariant.
|
|
||||||
// NOLINTNEXTLINE(bugprone-undefined-memory-manipulation)
|
|
||||||
auto *memcpy_fn = &memcpy;
|
|
||||||
for (size_t i = 1; i < size; i++) {
|
for (size_t i = 1; i < size; i++) {
|
||||||
alignas(WiFiScanResult) uint8_t key_buf[elem_size];
|
// Make a copy to avoid issues with move semantics during comparison
|
||||||
memcpy_fn(key_buf, &results[i], elem_size);
|
WiFiScanResult key = results[i];
|
||||||
const auto &key = *reinterpret_cast<const WiFiScanResult *>(key_buf);
|
|
||||||
int32_t j = i - 1;
|
int32_t j = i - 1;
|
||||||
|
|
||||||
// Move elements that are worse than key to the right
|
// Move elements that are worse than key to the right
|
||||||
// For stability, we only move if key is strictly better than results[j]
|
// For stability, we only move if key is strictly better than results[j]
|
||||||
while (j >= 0 && wifi_scan_result_is_better(key, results[j])) {
|
while (j >= 0 && wifi_scan_result_is_better(key, results[j])) {
|
||||||
memcpy_fn(&results[j + 1], &results[j], elem_size);
|
results[j + 1] = results[j];
|
||||||
j--;
|
j--;
|
||||||
}
|
}
|
||||||
memcpy_fn(&results[j + 1], key_buf, elem_size);
|
results[j + 1] = key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
|
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <type_traits>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#ifdef USE_LIBRETINY
|
#ifdef USE_LIBRETINY
|
||||||
@@ -220,14 +219,6 @@ class CompactString {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes");
|
static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes");
|
||||||
// CompactString is not trivially copyable (non-trivial destructor/copy for heap case).
|
|
||||||
// However, its layout has no self-referential pointers: storage_[] contains either inline
|
|
||||||
// data or an external heap pointer — never a pointer to itself. This is unlike libstdc++
|
|
||||||
// std::string SSO where _M_p points to _M_local_buf within the same object.
|
|
||||||
// This property allows memcpy-based permutation sorting where each element ends up in
|
|
||||||
// exactly one slot (no ownership duplication). These asserts document that layout property.
|
|
||||||
static_assert(std::is_standard_layout<CompactString>::value, "CompactString must be standard layout");
|
|
||||||
static_assert(!std::is_polymorphic<CompactString>::value, "CompactString must not have vtable");
|
|
||||||
|
|
||||||
class WiFiAP {
|
class WiFiAP {
|
||||||
friend class WiFiComponent;
|
friend class WiFiComponent;
|
||||||
@@ -497,18 +488,7 @@ class WiFiComponent : public Component {
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
void set_sta_priority(const bssid_t bssid, int8_t priority) {
|
void 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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
network::IPAddresses wifi_sta_ip_addresses();
|
network::IPAddresses wifi_sta_ip_addresses();
|
||||||
// Remove before 2026.9.0
|
// Remove before 2026.9.0
|
||||||
|
|||||||
@@ -81,6 +81,19 @@ class StringRef {
|
|||||||
|
|
||||||
operator std::string() const { return str(); }
|
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.
|
/// Find first occurrence of substring, returns std::string::npos if not found.
|
||||||
/// Note: Requires the underlying string to be null-terminated.
|
/// Note: Requires the underlying string to be null-terminated.
|
||||||
size_type find(const char *s, size_type pos = 0) const {
|
size_type find(const char *s, size_type pos = 0) const {
|
||||||
|
|||||||
@@ -24,11 +24,14 @@ class RegistryEntry:
|
|||||||
fun: Callable[..., Any],
|
fun: Callable[..., Any],
|
||||||
type_id: "MockObjClass",
|
type_id: "MockObjClass",
|
||||||
schema: "Schema",
|
schema: "Schema",
|
||||||
|
*,
|
||||||
|
deferred: bool = False,
|
||||||
):
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.fun = fun
|
self.fun = fun
|
||||||
self.type_id = type_id
|
self.type_id = type_id
|
||||||
self.raw_schema = schema
|
self.raw_schema = schema
|
||||||
|
self.deferred = deferred
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def coroutine_fun(self):
|
def coroutine_fun(self):
|
||||||
@@ -49,9 +52,16 @@ class Registry(dict[str, RegistryEntry]):
|
|||||||
self.base_schema = base_schema or {}
|
self.base_schema = base_schema or {}
|
||||||
self.type_id_key = type_id_key
|
self.type_id_key = type_id_key
|
||||||
|
|
||||||
def register(self, name: str, type_id: "MockObjClass", schema: "Schema"):
|
def register(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
type_id: "MockObjClass",
|
||||||
|
schema: "Schema",
|
||||||
|
*,
|
||||||
|
deferred: bool = False,
|
||||||
|
):
|
||||||
def decorator(fun: Callable[..., Any]):
|
def decorator(fun: Callable[..., Any]):
|
||||||
self[name] = RegistryEntry(name, fun, type_id, schema)
|
self[name] = RegistryEntry(name, fun, type_id, schema, deferred=deferred)
|
||||||
return fun
|
return fun
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|||||||
@@ -2020,6 +2020,8 @@ def build_message_type(
|
|||||||
|
|
||||||
# Collect fixed_vector fields for custom decode generation
|
# Collect fixed_vector fields for custom decode generation
|
||||||
fixed_vector_fields = []
|
fixed_vector_fields = []
|
||||||
|
# Collect fields with (null_terminate) = true option
|
||||||
|
null_terminate_fields = []
|
||||||
|
|
||||||
for field in desc.field:
|
for field in desc.field:
|
||||||
# Skip deprecated fields completely
|
# Skip deprecated fields completely
|
||||||
@@ -2062,6 +2064,10 @@ def build_message_type(
|
|||||||
|
|
||||||
ti = create_field_type_info(field, needs_decode, needs_encode)
|
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
|
# Skip field declarations for fields that are in the base class
|
||||||
# but include their encode/decode logic
|
# but include their encode/decode logic
|
||||||
if field.name not in common_field_names:
|
if field.name not in common_field_names:
|
||||||
@@ -2168,8 +2174,8 @@ def build_message_type(
|
|||||||
prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;"
|
prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;"
|
||||||
protected_content.insert(0, prot)
|
protected_content.insert(0, prot)
|
||||||
|
|
||||||
# Generate custom decode() override for messages with FixedVector fields
|
# Generate custom decode() override for messages with FixedVector or null_terminate fields
|
||||||
if fixed_vector_fields:
|
if fixed_vector_fields or null_terminate_fields:
|
||||||
# Generate the decode() implementation in cpp
|
# Generate the decode() implementation in cpp
|
||||||
o = f"void {desc.name}::decode(const uint8_t *buffer, size_t length) {{\n"
|
o = f"void {desc.name}::decode(const uint8_t *buffer, size_t length) {{\n"
|
||||||
# Count and init each FixedVector field
|
# Count and init each FixedVector field
|
||||||
@@ -2178,6 +2184,13 @@ def build_message_type(
|
|||||||
o += f" this->{field_name}.init(count_{field_name});\n"
|
o += f" this->{field_name}.init(count_{field_name});\n"
|
||||||
# Call parent decode to populate the fields
|
# Call parent decode to populate the fields
|
||||||
o += " ProtoDecodableMessage::decode(buffer, length);\n"
|
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"
|
o += "}\n"
|
||||||
cpp += o
|
cpp += o
|
||||||
# Generate the decode() declaration in header (public method)
|
# Generate the decode() declaration in header (public method)
|
||||||
|
|||||||
@@ -270,6 +270,14 @@ async def test_alarm_control_panel_state_transitions(
|
|||||||
# The chime_sensor has chime: true, so opening it while disarmed
|
# The chime_sensor has chime: true, so opening it while disarmed
|
||||||
# should trigger on_chime callback
|
# 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
|
# We're currently DISARMED - open the chime sensor
|
||||||
client.switch_command(chime_switch_info.key, True)
|
client.switch_command(chime_switch_info.key, True)
|
||||||
|
|
||||||
@@ -279,11 +287,18 @@ async def test_alarm_control_panel_state_transitions(
|
|||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}")
|
pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}")
|
||||||
|
|
||||||
# Close the chime sensor and wait for alarm to become ready again
|
# Wait for the on_ready from the chime sensor opening
|
||||||
# We need to wait for this transition before testing door sensor,
|
try:
|
||||||
# otherwise there's a race where the door sensor state change could
|
await asyncio.wait_for(ready_after_chime_open, timeout=2.0)
|
||||||
# arrive before the chime sensor state change, leaving the alarm in
|
except TimeoutError:
|
||||||
# a continuous "not ready" state with no on_ready callback fired.
|
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.
|
||||||
ready_after_chime_close: asyncio.Future[bool] = loop.create_future()
|
ready_after_chime_close: asyncio.Future[bool] = loop.create_future()
|
||||||
ready_futures.append(ready_after_chime_close)
|
ready_futures.append(ready_after_chime_close)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user