mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 03:16:37 +00:00
Merge branch 'esp8266-native-build-surgery' into esp8266-native-toolchain-plumbing
This commit is contained in:
@@ -1790,10 +1790,12 @@ void APIConnection::complete_authentication_() {
|
|||||||
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||||
// Copy client name with truncation if needed (set_client_name handles truncation)
|
// Copy client name with truncation if needed (set_client_name handles truncation)
|
||||||
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
|
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
|
||||||
this->client_api_version_major_ = msg.api_version_major;
|
this->client_api_version_major_ =
|
||||||
this->client_api_version_minor_ = msg.api_version_minor;
|
static_cast<uint8_t>(std::min<uint32_t>(msg.api_version_major, std::numeric_limits<uint8_t>::max()));
|
||||||
|
this->client_api_version_minor_ =
|
||||||
|
static_cast<uint8_t>(std::min<uint32_t>(msg.api_version_minor, std::numeric_limits<uint8_t>::max()));
|
||||||
char peername[socket::SOCKADDR_STR_LEN];
|
char peername[socket::SOCKADDR_STR_LEN];
|
||||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(),
|
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %u.%u", this->helper_->get_client_name(),
|
||||||
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
|
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
|
||||||
|
|
||||||
HelloResponse resp;
|
HelloResponse resp;
|
||||||
@@ -2224,7 +2226,7 @@ bool APIConnection::try_to_clear_buffer_slow_(bool log_out_of_space) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn,
|
bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn,
|
||||||
const void *msg) {
|
const void *msg) {
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
// Skip dump for log messages (recursive logging risk) and camera frames (high-frequency noise)
|
// Skip dump for log messages (recursive logging risk) and camera frames (high-frequency noise)
|
||||||
@@ -2253,7 +2255,7 @@ uint16_t APIConnection::encode_to_buffer_slow(uint32_t calculated_size, MessageE
|
|||||||
APIConnection *conn, uint32_t remaining_size) {
|
APIConnection *conn, uint32_t remaining_size) {
|
||||||
return encode_to_buffer(calculated_size, encode_fn, msg, conn, remaining_size);
|
return encode_to_buffer(calculated_size, encode_fn, msg, conn, remaining_size);
|
||||||
}
|
}
|
||||||
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) {
|
||||||
const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE);
|
const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE);
|
||||||
|
|
||||||
if (!this->try_to_clear_buffer(!is_log_message)) {
|
if (!this->try_to_clear_buffer(!is_log_message)) {
|
||||||
@@ -2283,12 +2285,12 @@ void APIConnection::on_fatal_error() {
|
|||||||
this->flags_.remove = true;
|
this->flags_.remove = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool APIConnection::schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
|
bool APIConnection::schedule_message_front_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size) {
|
||||||
this->deferred_batch_.add_item_front(entity, message_type, estimated_size);
|
this->deferred_batch_.add_item_front(entity, message_type, estimated_size);
|
||||||
return this->schedule_batch_();
|
return this->schedule_batch_();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool APIConnection::send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
bool APIConnection::send_message_smart_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
|
||||||
uint8_t aux_data_index) {
|
uint8_t aux_data_index) {
|
||||||
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
|
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
|
||||||
auto &shared_buf = this->parent_->get_shared_buffer_ref();
|
auto &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||||
|
|||||||
@@ -326,8 +326,10 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
bool is_marked_for_removal() const { return this->flags_.remove; }
|
bool is_marked_for_removal() const { return this->flags_.remove; }
|
||||||
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
|
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
|
||||||
|
|
||||||
// Get client API version for feature detection
|
// Get client API version for feature detection.
|
||||||
bool client_supports_api_version(uint16_t major, uint16_t minor) const {
|
// Stored versions saturate at 255 (see send_hello_response_), so requesting
|
||||||
|
// a minimum above that can never match.
|
||||||
|
bool client_supports_api_version(uint8_t major, uint8_t minor) const {
|
||||||
return this->client_api_version_major_ > major ||
|
return this->client_api_version_major_ > major ||
|
||||||
(this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor);
|
(this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor);
|
||||||
}
|
}
|
||||||
@@ -374,7 +376,7 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
return true;
|
return true;
|
||||||
return this->try_to_clear_buffer_slow_(log_out_of_space);
|
return this->try_to_clear_buffer_slow_(log_out_of_space);
|
||||||
}
|
}
|
||||||
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type);
|
bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type);
|
||||||
|
|
||||||
const char *get_name() const { return this->helper_->get_client_name(); }
|
const char *get_name() const { return this->helper_->get_client_name(); }
|
||||||
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
|
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
|
||||||
@@ -423,7 +425,7 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Non-template buffer management for send_message
|
// Non-template buffer management for send_message
|
||||||
bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg);
|
bool send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn, const void *msg);
|
||||||
|
|
||||||
// Core batch encoding logic. ALWAYS_INLINE so encode_fn devirtualizes at hot call sites.
|
// Core batch encoding logic. ALWAYS_INLINE so encode_fn devirtualizes at hot call sites.
|
||||||
// Defined in api_connection_buffer.h (needs APIServer complete).
|
// Defined in api_connection_buffer.h (needs APIServer complete).
|
||||||
@@ -664,10 +666,9 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
|
|
||||||
struct BatchItem {
|
struct BatchItem {
|
||||||
EntityBase *entity; // 4 bytes - Entity pointer
|
EntityBase *entity; // 4 bytes - Entity pointer
|
||||||
uint8_t message_type; // 1 byte - Message type for protocol and dispatch
|
uint16_t message_type; // 2 bytes - Message type for protocol and dispatch
|
||||||
uint8_t estimated_size; // 1 byte - Estimated message size (max 255 bytes)
|
uint8_t estimated_size; // 1 byte - Estimated message size (max 255 bytes)
|
||||||
uint8_t aux_data_index{AUX_DATA_UNUSED}; // 1 byte - For events: index into entity's event_types
|
uint8_t aux_data_index{AUX_DATA_UNUSED}; // 1 byte - For events: index into entity's event_types
|
||||||
// 1 byte padding
|
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<BatchItem> items;
|
std::vector<BatchItem> items;
|
||||||
@@ -677,7 +678,7 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
// connections that do, buffers are released after initial sync anyway
|
// connections that do, buffers are released after initial sync anyway
|
||||||
|
|
||||||
// Add item to the batch (with deduplication)
|
// Add item to the batch (with deduplication)
|
||||||
void add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
void add_item(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
|
||||||
uint8_t aux_data_index = AUX_DATA_UNUSED) {
|
uint8_t aux_data_index = AUX_DATA_UNUSED) {
|
||||||
// Dedup: O(n) scan but optimized for RAM over performance
|
// Dedup: O(n) scan but optimized for RAM over performance
|
||||||
// Skip deduplication for events - they are edge-triggered, every occurrence matters
|
// Skip deduplication for events - they are edge-triggered, every occurrence matters
|
||||||
@@ -693,7 +694,7 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
this->items.push_back({entity, message_type, estimated_size, aux_data_index});
|
this->items.push_back({entity, message_type, estimated_size, aux_data_index});
|
||||||
}
|
}
|
||||||
// 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, uint16_t message_type, uint8_t estimated_size) {
|
||||||
// Swap to front avoids expensive vector::insert which shifts all elements
|
// Swap to front avoids expensive vector::insert which shifts all elements
|
||||||
this->items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED});
|
this->items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED});
|
||||||
if (this->items.size() > 1) {
|
if (this->items.size() > 1) {
|
||||||
@@ -758,13 +759,15 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
#endif
|
#endif
|
||||||
} flags_{}; // 2 bytes total
|
} flags_{}; // 2 bytes total
|
||||||
|
|
||||||
// 2-byte types immediately after flags_ (no padding between them)
|
// 2-byte type immediately after flags_ (no padding between them)
|
||||||
uint16_t client_api_version_major_{0};
|
uint16_t batch_message_type_{0}; // Current message type during batch encoding
|
||||||
uint16_t client_api_version_minor_{0};
|
|
||||||
// 1-byte types to fill remaining space before next 4-byte boundary
|
// 1-byte types to fill remaining space before next 4-byte boundary
|
||||||
|
// Client API versions are clamped to 255 on receive (see send_hello_response_)
|
||||||
|
uint8_t client_api_version_major_{0};
|
||||||
|
uint8_t client_api_version_minor_{0};
|
||||||
ActiveIterator active_iterator_{ActiveIterator::NONE};
|
ActiveIterator active_iterator_{ActiveIterator::NONE};
|
||||||
uint8_t batch_message_type_{0}; // Current message type during batch encoding
|
// Total: 2 (flags) + 2 + 1 + 1 + 1 + 1 (batch_header_size_ below) = 8 bytes,
|
||||||
// Total: 2 (flags) + 2 + 2 + 1 + 1 = 8 bytes, aligned to 4-byte boundary
|
// aligned to 4-byte boundary
|
||||||
|
|
||||||
// Actual header size used by encode_to_buffer for the current message.
|
// Actual header size used by encode_to_buffer for the current message.
|
||||||
// Read by process_batch_multi_ to pass into MessageInfo.
|
// Read by process_batch_multi_ to pass into MessageInfo.
|
||||||
@@ -813,7 +816,7 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
// 2. It's an EventResponse (events are edge-triggered - every occurrence matters)
|
// 2. It's an EventResponse (events are edge-triggered - every occurrence matters)
|
||||||
// 3. OR: User has opted into immediate sending (should_try_send_immediately = true
|
// 3. OR: User has opted into immediate sending (should_try_send_immediately = true
|
||||||
// AND batch_delay = 0)
|
// AND batch_delay = 0)
|
||||||
inline bool should_send_immediately_(uint8_t message_type) const {
|
inline bool should_send_immediately_(uint16_t message_type) const {
|
||||||
return (
|
return (
|
||||||
#ifdef USE_UPDATE
|
#ifdef USE_UPDATE
|
||||||
message_type == UpdateStateResponse::MESSAGE_TYPE ||
|
message_type == UpdateStateResponse::MESSAGE_TYPE ||
|
||||||
@@ -827,11 +830,11 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
// Helper method to send a message either immediately or via batching
|
// Helper method to send a message either immediately or via batching
|
||||||
// Tries immediate send if should_send_immediately_() returns true and buffer has space
|
// Tries immediate send if should_send_immediately_() returns true and buffer has space
|
||||||
// Falls back to batching if immediate send fails or isn't applicable
|
// Falls back to batching if immediate send fails or isn't applicable
|
||||||
bool send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
bool send_message_smart_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
|
||||||
uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED);
|
uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED);
|
||||||
|
|
||||||
// Helper function to schedule a deferred message with known message type
|
// Helper function to schedule a deferred message with known message type
|
||||||
bool schedule_message_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
bool schedule_message_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
|
||||||
uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED) {
|
uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED) {
|
||||||
this->deferred_batch_.add_item(entity, message_type, estimated_size, aux_data_index);
|
this->deferred_batch_.add_item(entity, message_type, estimated_size, aux_data_index);
|
||||||
return this->schedule_batch_();
|
return this->schedule_batch_();
|
||||||
@@ -839,7 +842,7 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
|
|
||||||
// Helper function to schedule a high priority message at the front of the batch
|
// Helper function to schedule a high priority message at the front of the batch
|
||||||
// Out-of-line: callers (on_shutdown, check_keepalive_) are cold paths
|
// Out-of-line: callers (on_shutdown, check_keepalive_) are cold paths
|
||||||
bool schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size);
|
bool schedule_message_front_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size);
|
||||||
|
|
||||||
// Helper function to log client messages with name and peername
|
// Helper function to log client messages with name and peername
|
||||||
void log_client_(int level, const LogString *message);
|
void log_client_(int level, const LogString *message);
|
||||||
|
|||||||
@@ -49,16 +49,16 @@ struct ReadPacketBuffer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Packed message info structure to minimize memory usage
|
// Packed message info structure to minimize memory usage
|
||||||
// Note: message_type is uint8_t — all current protobuf message types fit in 8 bits.
|
// message_type matches the wire formats: noise carries a fixed 16-bit type
|
||||||
// The noise wire format encodes types as 16-bit, but the high byte is always 0.
|
// field, plaintext a type varint. The proto codegen caps message IDs at 16383
|
||||||
// If message types ever exceed 255, this and encrypt_noise_message_ must be updated.
|
// so the plaintext type varint fits the 2 bytes budgeted in HEADER_PADDING.
|
||||||
struct MessageInfo {
|
struct MessageInfo {
|
||||||
uint16_t offset; // Offset in buffer where message starts
|
uint16_t offset; // Offset in buffer where message starts
|
||||||
uint16_t payload_size; // Size of the message payload
|
uint16_t payload_size; // Size of the message payload
|
||||||
uint8_t message_type; // Message type (0-255)
|
uint16_t message_type; // Message type (0-16383)
|
||||||
uint8_t header_size; // Actual header size used (avoids recomputation in write path)
|
uint8_t header_size; // Actual header size used (avoids recomputation in write path)
|
||||||
|
|
||||||
MessageInfo(uint8_t type, uint16_t off, uint16_t size, uint8_t hdr)
|
MessageInfo(uint16_t type, uint16_t off, uint16_t size, uint8_t hdr)
|
||||||
: offset(off), payload_size(size), message_type(type), header_size(hdr) {}
|
: offset(off), payload_size(size), message_type(type), header_size(hdr) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ class APIFrameHelper {
|
|||||||
}
|
}
|
||||||
// Write a single protobuf message - the hot path (87-100% of all writes).
|
// Write a single protobuf message - the hot path (87-100% of all writes).
|
||||||
// Caller must ensure state is DATA before calling.
|
// Caller must ensure state is DATA before calling.
|
||||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
|
||||||
// Write multiple protobuf messages in a single batched operation.
|
// Write multiple protobuf messages in a single batched operation.
|
||||||
// Caller must ensure state is DATA and messages is not empty.
|
// Caller must ensure state is DATA and messages is not empty.
|
||||||
// messages contains (message_type, offset, length) for each message in the buffer.
|
// messages contains (message_type, offset, length) for each message in the buffer.
|
||||||
@@ -187,15 +187,15 @@ class APIFrameHelper {
|
|||||||
// Distinguishes protocols via frame_footer_size_ (noise always has a non-zero MAC
|
// Distinguishes protocols via frame_footer_size_ (noise always has a non-zero MAC
|
||||||
// footer, plaintext has footer=0). If a protocol with a plaintext footer is ever
|
// footer, plaintext has footer=0). If a protocol with a plaintext footer is ever
|
||||||
// added, this should become a virtual method.
|
// added, this should become a virtual method.
|
||||||
uint8_t frame_header_size(uint16_t payload_size, uint8_t message_type) const {
|
uint8_t frame_header_size(uint16_t payload_size, uint16_t message_type) const {
|
||||||
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
|
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
|
||||||
return this->frame_footer_size_
|
return this->frame_footer_size_
|
||||||
? this->frame_header_padding_
|
? this->frame_header_padding_
|
||||||
: static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type));
|
: static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint16(message_type));
|
||||||
#elif defined(USE_API_NOISE)
|
#elif defined(USE_API_NOISE)
|
||||||
return this->frame_header_padding_;
|
return this->frame_header_padding_;
|
||||||
#else // USE_API_PLAINTEXT only
|
#else // USE_API_PLAINTEXT only
|
||||||
return static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type));
|
return static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint16(message_type));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
// Get the frame footer size required by this protocol
|
// Get the frame footer size required by this protocol
|
||||||
|
|||||||
@@ -442,7 +442,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
|||||||
}
|
}
|
||||||
// Encrypt a single noise message in place and return the encrypted frame length.
|
// Encrypt a single noise message in place and return the encrypted frame length.
|
||||||
// Returns APIError::OK on success.
|
// Returns APIError::OK on success.
|
||||||
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
|
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint16_t message_type,
|
||||||
uint16_t &encrypted_len_out) {
|
uint16_t &encrypted_len_out) {
|
||||||
// The noise frame header is written after encryption, when the size is known
|
// The noise frame header is written after encryption, when the size is known
|
||||||
|
|
||||||
@@ -472,7 +472,7 @@ APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_
|
|||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
|
||||||
#ifdef ESPHOME_DEBUG_API
|
#ifdef ESPHOME_DEBUG_API
|
||||||
assert(this->state_ == State::DATA);
|
assert(this->state_ == State::DATA);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
|||||||
#endif
|
#endif
|
||||||
APIError loop() override;
|
APIError loop() override;
|
||||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
|
||||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -44,7 +44,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
|||||||
APIError state_action_handshake_write_();
|
APIError state_action_handshake_write_();
|
||||||
APIError try_read_frame_();
|
APIError try_read_frame_();
|
||||||
APIError write_frame_(const uint8_t *data, uint16_t len);
|
APIError write_frame_(const uint8_t *data, uint16_t len);
|
||||||
APIError encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
|
APIError encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint16_t message_type,
|
||||||
uint16_t &encrypted_len_out);
|
uint16_t &encrypted_len_out);
|
||||||
APIError init_handshake_();
|
APIError init_handshake_();
|
||||||
APIError check_handshake_finished_();
|
APIError check_handshake_finished_();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include "api_pb2.h"
|
||||||
#include "proto.h"
|
#include "proto.h"
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
@@ -252,24 +253,21 @@ ESPHOME_ALWAYS_INLINE static inline void encode_varint_16(uint16_t value, uint8_
|
|||||||
*p = static_cast<uint8_t>(value);
|
*p = static_cast<uint8_t>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode an 8-bit varint (1-2 bytes) using pre-computed length.
|
// The generator rejects message IDs above MAX_MESSAGE_TYPE, so the type varint
|
||||||
ESPHOME_ALWAYS_INLINE static inline void encode_varint_8(uint8_t value, uint8_t varint_len, uint8_t *p) {
|
// can never outgrow the 2 bytes HEADER_PADDING budgets for it. Without this
|
||||||
if (varint_len == 2) {
|
// bound, write_plaintext_header's header_offset would underflow for the first
|
||||||
*p++ = static_cast<uint8_t>(value | 0x80);
|
// message in a batch and the header write would land outside the buffer.
|
||||||
*p = static_cast<uint8_t>(value >> 7);
|
static_assert(1 + 3 + ProtoSize::varint16(MAX_MESSAGE_TYPE) <= APIPlaintextFrameHelper::HEADER_PADDING,
|
||||||
} else {
|
"HEADER_PADDING cannot fit the type varint of the largest message ID");
|
||||||
*p = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write plaintext header into pre-allocated padding before payload.
|
// Write plaintext header into pre-allocated padding before payload.
|
||||||
// padding_size: bytes reserved before payload (HEADER_PADDING for first/single msg,
|
// padding_size: bytes reserved before payload (HEADER_PADDING for first/single msg,
|
||||||
// actual header size for contiguous batch messages).
|
// actual header size for contiguous batch messages).
|
||||||
// Returns the total header length (indicator + varints).
|
// Returns the total header length (indicator + varints).
|
||||||
ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_start, uint16_t payload_size,
|
ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_start, uint16_t payload_size,
|
||||||
uint8_t message_type, uint8_t padding_size) {
|
uint16_t message_type, uint8_t padding_size) {
|
||||||
uint8_t size_varint_len = ProtoSize::varint16(payload_size);
|
uint8_t size_varint_len = ProtoSize::varint16(payload_size);
|
||||||
uint8_t type_varint_len = ProtoSize::varint8(message_type);
|
uint8_t type_varint_len = ProtoSize::varint16(message_type);
|
||||||
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
||||||
|
|
||||||
// The header is right-justified within the padding so it sits immediately before payload.
|
// The header is right-justified within the padding so it sits immediately before payload.
|
||||||
@@ -292,12 +290,12 @@ ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_
|
|||||||
|
|
||||||
// Encode varints directly into buffer using pre-computed lengths
|
// Encode varints directly into buffer using pre-computed lengths
|
||||||
encode_varint_16(payload_size, size_varint_len, buf_start + header_offset + 1);
|
encode_varint_16(payload_size, size_varint_len, buf_start + header_offset + 1);
|
||||||
encode_varint_8(message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len);
|
encode_varint_16(message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len);
|
||||||
|
|
||||||
return total_header_len;
|
return total_header_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
|
||||||
#ifdef ESPHOME_DEBUG_API
|
#ifdef ESPHOME_DEBUG_API
|
||||||
assert(this->state_ == State::DATA);
|
assert(this->state_ == State::DATA);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
|
|||||||
// Plaintext header structure (worst case):
|
// Plaintext header structure (worst case):
|
||||||
// Pos 0: indicator (0x00)
|
// Pos 0: indicator (0x00)
|
||||||
// Pos 1-3: payload size varint (up to 3 bytes)
|
// Pos 1-3: payload size varint (up to 3 bytes)
|
||||||
// Pos 4-5: message type varint (up to 2 bytes)
|
// Pos 4-5: message type varint (up to 2 bytes; covers message IDs up to
|
||||||
|
// 16383, enforced by the proto codegen)
|
||||||
// Pos 6+: actual payload data
|
// Pos 6+: actual payload data
|
||||||
static constexpr uint8_t HEADER_PADDING = 1 + 3 + 2; // indicator + size varint + type varint
|
static constexpr uint8_t HEADER_PADDING = 1 + 3 + 2; // indicator + size varint + type varint
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
|
|||||||
APIError init() override;
|
APIError init() override;
|
||||||
APIError loop() override;
|
APIError loop() override;
|
||||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
|
||||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
// After try_read_frame_ returned PROTOCOL_SWITCH_TO_NOISE: copy out the
|
// After try_read_frame_ returned PROTOCOL_SWITCH_TO_NOISE: copy out the
|
||||||
|
|||||||
+144
-140
File diff suppressed because it is too large
Load Diff
@@ -684,11 +684,6 @@ class ProtoSize {
|
|||||||
return value < VARINT_THRESHOLD_1_BYTE ? 1 : (value < VARINT_THRESHOLD_2_BYTE ? 2 : 3);
|
return value < VARINT_THRESHOLD_1_BYTE ? 1 : (value < VARINT_THRESHOLD_2_BYTE ? 2 : 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Varint encoded length for an 8-bit value (1 or 2 bytes).
|
|
||||||
static constexpr inline uint8_t ESPHOME_ALWAYS_INLINE varint8(uint8_t value) {
|
|
||||||
return value < VARINT_THRESHOLD_1_BYTE ? 1 : 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
|
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -143,6 +143,13 @@ def validate_max_connections_deprecated(config: ConfigType) -> ConfigType:
|
|||||||
# BLE uses the airtime wifi does not claim.
|
# BLE uses the airtime wifi does not claim.
|
||||||
IDF_SCAN_WINDOW_FIX_VERSION = cv.Version(5, 5, 5)
|
IDF_SCAN_WINDOW_FIX_VERSION = cv.Version(5, 5, 5)
|
||||||
|
|
||||||
|
# Above this the scanner holds the shared radio long enough that wifi drops
|
||||||
|
# packets and connections on some access points (others cope fine, which is
|
||||||
|
# why this is a warning and not an error); old proxy configs with 1100 ms
|
||||||
|
# windows are a recurring cause of instability (esphome/esphome#18655). Only
|
||||||
|
# wifi shares the radio; long windows are fine on ethernet builds.
|
||||||
|
MAX_RECOMMENDED_WIFI_SCAN_WINDOW = TimePeriod(milliseconds=600)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TrackerData:
|
class TrackerData:
|
||||||
@@ -209,6 +216,45 @@ def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def _warn_long_scan_window_with_wifi(config: ConfigType) -> ConfigType:
|
||||||
|
"""Warn when the scan window is long enough to starve wifi.
|
||||||
|
|
||||||
|
Runs after _raise_defaulted_scan_window so it sees the final window.
|
||||||
|
software_coexistence is only present when wifi is configured, so ethernet
|
||||||
|
builds never warn: BLE has the radio to itself there. Presence is what
|
||||||
|
matters, not the value; with the arbiter disabled a long window starves
|
||||||
|
wifi outright.
|
||||||
|
"""
|
||||||
|
params = config[CONF_SCAN_PARAMETERS]
|
||||||
|
window = params[CONF_WINDOW]
|
||||||
|
if CONF_SOFTWARE_COEXISTENCE not in config:
|
||||||
|
return config
|
||||||
|
if window <= MAX_RECOMMENDED_WIFI_SCAN_WINDOW:
|
||||||
|
return config
|
||||||
|
if _get_data().scan_window_defaulted:
|
||||||
|
# The window was raised to match the interval, so point at the key the
|
||||||
|
# user actually set.
|
||||||
|
_LOGGER.warning(
|
||||||
|
"BLE scan interval of %s sets the scan window to the same value, "
|
||||||
|
"which starves wifi on the same radio and can cause wifi disconnects "
|
||||||
|
"depending on the access point; keep the interval at or below %s "
|
||||||
|
"(for example interval: 320ms). Long windows are only a problem with "
|
||||||
|
"wifi, they are fine on ethernet",
|
||||||
|
params[CONF_INTERVAL],
|
||||||
|
MAX_RECOMMENDED_WIFI_SCAN_WINDOW,
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
_LOGGER.warning(
|
||||||
|
"BLE scan window of %s with wifi on the same radio starves wifi and "
|
||||||
|
"can cause wifi disconnects depending on the access point; keep the "
|
||||||
|
"window at or below %s (for example interval: 320ms, window: 300ms). "
|
||||||
|
"Long windows are only a problem with wifi, they are fine on ethernet",
|
||||||
|
window,
|
||||||
|
MAX_RECOMMENDED_WIFI_SCAN_WINDOW,
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
# 320 ms is the ESP-IDF reference scan interval; the shared schema also
|
# 320 ms is the ESP-IDF reference scan interval; the shared schema also
|
||||||
# tightens validation to the controller's 2.5 ms .. 10240 ms range and rejects
|
# tightens validation to the controller's 2.5 ms .. 10240 ms range and rejects
|
||||||
# window/interval pairs that collapse to the same 0.625 ms unit count.
|
# window/interval pairs that collapse to the same 0.625 ms unit count.
|
||||||
@@ -271,6 +317,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
).extend(cv.COMPONENT_SCHEMA),
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
validate_max_connections_deprecated,
|
validate_max_connections_deprecated,
|
||||||
_raise_defaulted_scan_window,
|
_raise_defaulted_scan_window,
|
||||||
|
_warn_long_scan_window_with_wifi,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -475,6 +475,19 @@ TYPE_INFO: dict[int, TypeInfo] = {}
|
|||||||
# TYPE_DOUBLE = 1, TYPE_FIXED64 = 6, TYPE_SFIXED64 = 16, TYPE_SINT64 = 18
|
# TYPE_DOUBLE = 1, TYPE_FIXED64 = 6, TYPE_SFIXED64 = 16, TYPE_SINT64 = 18
|
||||||
UNSUPPORTED_TYPES = {1: "double", 6: "fixed64", 16: "sfixed64", 18: "sint64"}
|
UNSUPPORTED_TYPES = {1: "double", 6: "fixed64", 16: "sfixed64", 18: "sint64"}
|
||||||
|
|
||||||
|
# The plaintext frame header budgets 2 varint bytes for the message type
|
||||||
|
# (APIPlaintextFrameHelper::HEADER_PADDING), which caps message IDs at 16383.
|
||||||
|
MAX_MESSAGE_ID = 16383
|
||||||
|
|
||||||
|
|
||||||
|
def validate_message_id(message_id: int, message_name: str) -> None:
|
||||||
|
"""Reject message IDs whose plaintext type varint would not fit in 2 bytes."""
|
||||||
|
if message_id > MAX_MESSAGE_ID:
|
||||||
|
raise ValueError(
|
||||||
|
f"Message ID {message_id} for {message_name} exceeds the plaintext "
|
||||||
|
f"2-byte type varint maximum ({MAX_MESSAGE_ID})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_field_type(field_type: int, field_name: str = "") -> None:
|
def validate_field_type(field_type: int, field_name: str = "") -> None:
|
||||||
"""Validate that the field type is supported by ESPHome API.
|
"""Validate that the field type is supported by ESPHome API.
|
||||||
@@ -2549,14 +2562,10 @@ def build_message_type(
|
|||||||
|
|
||||||
# Add MESSAGE_TYPE method if this is a service message
|
# Add MESSAGE_TYPE method if this is a service message
|
||||||
if message_id is not None:
|
if message_id is not None:
|
||||||
# Validate that message_id fits in uint8_t
|
validate_message_id(message_id, desc.name)
|
||||||
if message_id > 255:
|
|
||||||
raise ValueError(
|
|
||||||
f"Message ID {message_id} for {desc.name} exceeds uint8_t maximum (255)"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add static constexpr for message type
|
# Add static constexpr for message type
|
||||||
public_content.append(f"static constexpr uint8_t MESSAGE_TYPE = {message_id};")
|
public_content.append(f"static constexpr uint16_t MESSAGE_TYPE = {message_id};")
|
||||||
|
|
||||||
# Add estimated size constant
|
# Add estimated size constant
|
||||||
estimated_size = calculate_message_estimated_size(desc)
|
estimated_size = calculate_message_estimated_size(desc)
|
||||||
@@ -3212,8 +3221,12 @@ def main() -> None:
|
|||||||
#include "api_pb2_includes.h"
|
#include "api_pb2_includes.h"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
content += """
|
content += f"""
|
||||||
namespace esphome::api {
|
namespace esphome::api {{
|
||||||
|
|
||||||
|
// Upper bound on message IDs, enforced by the code generator: the plaintext
|
||||||
|
// frame header budgets 2 varint bytes for the type (HEADER_PADDING).
|
||||||
|
static constexpr uint16_t MAX_MESSAGE_TYPE = {MAX_MESSAGE_ID};
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ arbiter a full-duty scan would starve wifi, so the 30 ms default is kept.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -221,3 +222,54 @@ def test_connection_scan_window_codegen(
|
|||||||
assert window_call in main_cpp
|
assert window_call in main_cpp
|
||||||
assert ("set_connection_scan_window(48)" in main_cpp) == connection_call
|
assert ("set_connection_scan_window(48)" in main_cpp) == connection_call
|
||||||
assert ("'connection_scan_window' has no effect" in caplog.text) == warns
|
assert ("'connection_scan_window' has no effect" in caplog.text) == warns
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("wifi", "params", "expect_warning"),
|
||||||
|
[
|
||||||
|
(True, {"interval": "1100ms", "window": "1100ms"}, True),
|
||||||
|
(True, {"interval": "1100ms", "window": "601ms"}, True),
|
||||||
|
(True, {"interval": "1100ms", "window": "600ms"}, False),
|
||||||
|
(False, {"interval": "1100ms", "window": "1100ms"}, False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_long_window_with_wifi_warns(
|
||||||
|
stage_esp32: Callable[..., None],
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
wifi: bool,
|
||||||
|
params: ConfigType,
|
||||||
|
expect_warning: bool,
|
||||||
|
) -> None:
|
||||||
|
"""A scan window above 600 ms warns only when wifi shares the radio."""
|
||||||
|
stage_esp32("5.5.5", wifi=wifi)
|
||||||
|
with caplog.at_level(logging.WARNING):
|
||||||
|
_scan_params({"scan_parameters": params})
|
||||||
|
assert ("starves wifi" in caplog.text) is expect_warning
|
||||||
|
|
||||||
|
|
||||||
|
def test_long_window_warns_with_coexistence_disabled(
|
||||||
|
stage_esp32: Callable[..., None],
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Disabling the arbiter is the worst case for a long window, so it still warns."""
|
||||||
|
stage_esp32("5.5.5", wifi=True)
|
||||||
|
with caplog.at_level(logging.WARNING):
|
||||||
|
_scan_params(
|
||||||
|
{
|
||||||
|
CONF_SOFTWARE_COEXISTENCE: False,
|
||||||
|
"scan_parameters": {"interval": "1100ms", "window": "1100ms"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert "BLE scan window of 1100ms" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_raised_window_warning_points_at_interval(
|
||||||
|
stage_esp32: Callable[..., None],
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""When the window was raised to a long interval, the warning names the interval."""
|
||||||
|
stage_esp32("5.5.5", wifi=True)
|
||||||
|
with caplog.at_level(logging.WARNING):
|
||||||
|
_scan_params({"scan_parameters": {"interval": "1s"}})
|
||||||
|
assert "BLE scan interval of 1s" in caplog.text
|
||||||
|
assert "BLE scan window of" not in caplog.text
|
||||||
|
|||||||
@@ -15,7 +15,12 @@ import pytest
|
|||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parents[4] / "script" / "api_protobuf"))
|
sys.path.insert(0, str(Path(__file__).parents[4] / "script" / "api_protobuf"))
|
||||||
|
|
||||||
from api_protobuf import _make_ifdef_line, get_varint64_ifdef # noqa: E402
|
from api_protobuf import ( # noqa: E402
|
||||||
|
MAX_MESSAGE_ID,
|
||||||
|
_make_ifdef_line,
|
||||||
|
get_varint64_ifdef,
|
||||||
|
validate_message_id,
|
||||||
|
)
|
||||||
from google.protobuf import descriptor_pb2 # noqa: E402
|
from google.protobuf import descriptor_pb2 # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
@@ -91,3 +96,14 @@ def test_make_ifdef_line_conjunction_and_negation() -> None:
|
|||||||
assert (
|
assert (
|
||||||
_make_ifdef_line("USE_X && !USE_Y") == "#if defined(USE_X) && !defined(USE_Y)"
|
_make_ifdef_line("USE_X && !USE_Y") == "#if defined(USE_X) && !defined(USE_Y)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_message_id_at_maximum_is_accepted() -> None:
|
||||||
|
# 16383 is the largest ID whose plaintext type varint fits the 2 bytes
|
||||||
|
# budgeted in HEADER_PADDING.
|
||||||
|
validate_message_id(MAX_MESSAGE_ID, "MaxMessage")
|
||||||
|
|
||||||
|
|
||||||
|
def test_message_id_above_maximum_is_rejected() -> None:
|
||||||
|
with pytest.raises(ValueError, match="exceeds the plaintext"):
|
||||||
|
validate_message_id(MAX_MESSAGE_ID + 1, "TooBigMessage")
|
||||||
|
|||||||
Reference in New Issue
Block a user