mirror of
https://github.com/esphome/esphome.git
synced 2026-06-01 01:19:45 +08:00
[api] Single-pass protobuf encode for BLE proxy advertisements (#14575)
This commit is contained in:
@@ -120,16 +120,16 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const {
|
|||||||
#endif
|
#endif
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
for (const auto &it : this->devices) {
|
for (const auto &it : this->devices) {
|
||||||
buffer.encode_message(20, it);
|
buffer.encode_sub_message(20, it);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_AREAS
|
#ifdef USE_AREAS
|
||||||
for (const auto &it : this->areas) {
|
for (const auto &it : this->areas) {
|
||||||
buffer.encode_message(21, it);
|
buffer.encode_sub_message(21, it);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_AREAS
|
#ifdef USE_AREAS
|
||||||
buffer.encode_message(22, this->area, false);
|
buffer.encode_optional_sub_message(22, this->area);
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ZWAVE_PROXY
|
#ifdef USE_ZWAVE_PROXY
|
||||||
buffer.encode_uint32(23, this->zwave_proxy_feature_flags);
|
buffer.encode_uint32(23, this->zwave_proxy_feature_flags);
|
||||||
@@ -920,13 +920,13 @@ uint32_t HomeassistantServiceMap::calculate_size() const {
|
|||||||
void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const {
|
void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const {
|
||||||
buffer.encode_string(1, this->service);
|
buffer.encode_string(1, this->service);
|
||||||
for (auto &it : this->data) {
|
for (auto &it : this->data) {
|
||||||
buffer.encode_message(2, it);
|
buffer.encode_sub_message(2, it);
|
||||||
}
|
}
|
||||||
for (auto &it : this->data_template) {
|
for (auto &it : this->data_template) {
|
||||||
buffer.encode_message(3, it);
|
buffer.encode_sub_message(3, it);
|
||||||
}
|
}
|
||||||
for (auto &it : this->variables) {
|
for (auto &it : this->variables) {
|
||||||
buffer.encode_message(4, it);
|
buffer.encode_sub_message(4, it);
|
||||||
}
|
}
|
||||||
buffer.encode_bool(5, this->is_event);
|
buffer.encode_bool(5, this->is_event);
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
@@ -1126,7 +1126,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const {
|
|||||||
buffer.encode_string(1, this->name);
|
buffer.encode_string(1, this->name);
|
||||||
buffer.encode_fixed32(2, this->key);
|
buffer.encode_fixed32(2, this->key);
|
||||||
for (auto &it : this->args) {
|
for (auto &it : this->args) {
|
||||||
buffer.encode_message(3, it);
|
buffer.encode_sub_message(3, it);
|
||||||
}
|
}
|
||||||
buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response));
|
buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response));
|
||||||
}
|
}
|
||||||
@@ -2133,7 +2133,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const {
|
|||||||
buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category));
|
buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category));
|
||||||
buffer.encode_bool(8, this->supports_pause);
|
buffer.encode_bool(8, this->supports_pause);
|
||||||
for (auto &it : this->supported_formats) {
|
for (auto &it : this->supported_formats) {
|
||||||
buffer.encode_message(9, it);
|
buffer.encode_sub_message(9, it);
|
||||||
}
|
}
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
buffer.encode_uint32(10, this->device_id);
|
buffer.encode_uint32(10, this->device_id);
|
||||||
@@ -2264,7 +2264,7 @@ uint32_t BluetoothLERawAdvertisement::calculate_size() const {
|
|||||||
}
|
}
|
||||||
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const {
|
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const {
|
||||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||||
buffer.encode_message(1, this->advertisements[i]);
|
buffer.encode_sub_message(1, this->advertisements[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
||||||
@@ -2343,7 +2343,7 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const {
|
|||||||
buffer.encode_uint32(2, this->handle);
|
buffer.encode_uint32(2, this->handle);
|
||||||
buffer.encode_uint32(3, this->properties);
|
buffer.encode_uint32(3, this->properties);
|
||||||
for (auto &it : this->descriptors) {
|
for (auto &it : this->descriptors) {
|
||||||
buffer.encode_message(4, it);
|
buffer.encode_sub_message(4, it);
|
||||||
}
|
}
|
||||||
buffer.encode_uint32(5, this->short_uuid);
|
buffer.encode_uint32(5, this->short_uuid);
|
||||||
}
|
}
|
||||||
@@ -2370,7 +2370,7 @@ void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const {
|
|||||||
}
|
}
|
||||||
buffer.encode_uint32(2, this->handle);
|
buffer.encode_uint32(2, this->handle);
|
||||||
for (auto &it : this->characteristics) {
|
for (auto &it : this->characteristics) {
|
||||||
buffer.encode_message(3, it);
|
buffer.encode_sub_message(3, it);
|
||||||
}
|
}
|
||||||
buffer.encode_uint32(4, this->short_uuid);
|
buffer.encode_uint32(4, this->short_uuid);
|
||||||
}
|
}
|
||||||
@@ -2392,7 +2392,7 @@ uint32_t BluetoothGATTService::calculate_size() const {
|
|||||||
void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const {
|
void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const {
|
||||||
buffer.encode_uint64(1, this->address);
|
buffer.encode_uint64(1, this->address);
|
||||||
for (auto &it : this->services) {
|
for (auto &it : this->services) {
|
||||||
buffer.encode_message(2, it);
|
buffer.encode_sub_message(2, it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uint32_t BluetoothGATTGetServicesResponse::calculate_size() const {
|
uint32_t BluetoothGATTGetServicesResponse::calculate_size() const {
|
||||||
@@ -2673,7 +2673,7 @@ void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const {
|
|||||||
buffer.encode_bool(1, this->start);
|
buffer.encode_bool(1, this->start);
|
||||||
buffer.encode_string(2, this->conversation_id);
|
buffer.encode_string(2, this->conversation_id);
|
||||||
buffer.encode_uint32(3, this->flags);
|
buffer.encode_uint32(3, this->flags);
|
||||||
buffer.encode_message(4, this->audio_settings, false);
|
buffer.encode_optional_sub_message(4, this->audio_settings);
|
||||||
buffer.encode_string(5, this->wake_word_phrase);
|
buffer.encode_string(5, this->wake_word_phrase);
|
||||||
}
|
}
|
||||||
uint32_t VoiceAssistantRequest::calculate_size() const {
|
uint32_t VoiceAssistantRequest::calculate_size() const {
|
||||||
@@ -2906,7 +2906,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL
|
|||||||
}
|
}
|
||||||
void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const {
|
void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const {
|
||||||
for (auto &it : this->available_wake_words) {
|
for (auto &it : this->available_wake_words) {
|
||||||
buffer.encode_message(1, it);
|
buffer.encode_sub_message(1, it);
|
||||||
}
|
}
|
||||||
for (const auto &it : *this->active_wake_words) {
|
for (const auto &it : *this->active_wake_words) {
|
||||||
buffer.encode_string(2, it, true);
|
buffer.encode_string(2, it, true);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "proto.h"
|
#include "proto.h"
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
|
#include <cstring>
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
@@ -87,6 +88,76 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Single-pass encode for repeated submessage elements (non-template core).
|
||||||
|
// Writes field tag, reserves 1 byte for length varint, encodes the submessage body,
|
||||||
|
// then backpatches the actual length. For the common case (body < 128 bytes), this is
|
||||||
|
// just a single byte write with no memmove — all current repeated submessage types
|
||||||
|
// (BLE advertisements at ~47B, GATT descriptors at ~24B, service args, etc.) take
|
||||||
|
// this fast path.
|
||||||
|
//
|
||||||
|
// The memmove fallback for body >= 128 bytes exists only for correctness (e.g., a GATT
|
||||||
|
// characteristic with many descriptors). It is safe because calculate_size() already
|
||||||
|
// reserved space for the full multi-byte varint — the shift fills that reserved space:
|
||||||
|
//
|
||||||
|
// calculate_size() allocates per element: tag + varint_size(body) + body_size
|
||||||
|
//
|
||||||
|
// After encode, before memmove (1 byte reserved, body written):
|
||||||
|
// [tag][__][body ..... body][??]
|
||||||
|
// ^ ^-- unused byte (v2 space from calculate_size)
|
||||||
|
// len_pos
|
||||||
|
//
|
||||||
|
// After memmove(body_start+1, body_start, body_size):
|
||||||
|
// [tag][__][__][body ..... body]
|
||||||
|
// ^ ^-- body shifted forward, fills v2 space exactly
|
||||||
|
// len_pos
|
||||||
|
//
|
||||||
|
// After writing 2-byte varint at len_pos:
|
||||||
|
// [tag][v1][v2][body ..... body]
|
||||||
|
// ^-- pos_ = element end, within buffer
|
||||||
|
void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value,
|
||||||
|
void (*encode_fn)(const void *, ProtoWriteBuffer &)) {
|
||||||
|
this->encode_field_raw(field_id, 2);
|
||||||
|
// Reserve 1 byte for length varint (optimistic: submessage < 128 bytes)
|
||||||
|
uint8_t *len_pos = this->pos_;
|
||||||
|
this->debug_check_bounds_(1);
|
||||||
|
this->pos_++;
|
||||||
|
uint8_t *body_start = this->pos_;
|
||||||
|
encode_fn(value, *this);
|
||||||
|
uint32_t body_size = static_cast<uint32_t>(this->pos_ - body_start);
|
||||||
|
if (body_size < 128) [[likely]] {
|
||||||
|
// Common case: 1-byte varint, just backpatch
|
||||||
|
*len_pos = static_cast<uint8_t>(body_size);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Compute extra bytes needed for varint beyond the 1 already reserved
|
||||||
|
uint8_t extra = ProtoSize::varint(body_size) - 1;
|
||||||
|
// Shift body forward to make room for the extra varint bytes
|
||||||
|
this->debug_check_bounds_(extra);
|
||||||
|
std::memmove(body_start + extra, body_start, body_size);
|
||||||
|
uint8_t *end = this->pos_ + extra;
|
||||||
|
// Write the full varint at len_pos
|
||||||
|
this->pos_ = len_pos;
|
||||||
|
this->encode_varint_raw(body_size);
|
||||||
|
this->pos_ = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-template core for encode_optional_sub_message.
|
||||||
|
void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
|
||||||
|
void (*encode_fn)(const void *, ProtoWriteBuffer &)) {
|
||||||
|
if (nested_size == 0)
|
||||||
|
return;
|
||||||
|
this->encode_field_raw(field_id, 2);
|
||||||
|
this->encode_varint_raw(nested_size);
|
||||||
|
#ifdef ESPHOME_DEBUG_API
|
||||||
|
uint8_t *start = this->pos_;
|
||||||
|
encode_fn(value, *this);
|
||||||
|
if (static_cast<uint32_t>(this->pos_ - start) != nested_size)
|
||||||
|
this->debug_check_encode_size_(field_id, nested_size, this->pos_ - start);
|
||||||
|
#else
|
||||||
|
encode_fn(value, *this);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef ESPHOME_DEBUG_API
|
#ifdef ESPHOME_DEBUG_API
|
||||||
void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) {
|
void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) {
|
||||||
if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) {
|
if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) {
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ class ProtoVarInt {
|
|||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forward declarations for decode_to_message, encode_message and encode_packed_sint32
|
// Forward declarations for decode_to_message and related encoding helpers
|
||||||
class ProtoDecodableMessage;
|
class ProtoDecodableMessage;
|
||||||
class ProtoMessage;
|
class ProtoMessage;
|
||||||
class ProtoSize;
|
class ProtoSize;
|
||||||
@@ -363,12 +363,18 @@ class ProtoWriteBuffer {
|
|||||||
}
|
}
|
||||||
/// Encode a packed repeated sint32 field (zero-copy from vector)
|
/// Encode a packed repeated sint32 field (zero-copy from vector)
|
||||||
void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values);
|
void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values);
|
||||||
/// Encode a nested message field (force=true for repeated, false for singular)
|
/// Single-pass encode for repeated submessage elements.
|
||||||
/// Templated so concrete message type is preserved for direct encode/calculate_size calls.
|
/// Thin template wrapper; all buffer work is in the non-template core.
|
||||||
template<typename T> void encode_message(uint32_t field_id, const T &value, bool force = true);
|
template<typename T> void encode_sub_message(uint32_t field_id, const T &value);
|
||||||
// Non-template core for encode_message — all buffer work happens here
|
/// Encode an optional singular submessage field — skips if empty.
|
||||||
void encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value,
|
/// Thin template wrapper; all buffer work is in the non-template core.
|
||||||
void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force);
|
template<typename T> void encode_optional_sub_message(uint32_t field_id, const T &value);
|
||||||
|
|
||||||
|
// Non-template core for encode_sub_message — backpatch approach.
|
||||||
|
void encode_sub_message(uint32_t field_id, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &));
|
||||||
|
// Non-template core for encode_optional_sub_message.
|
||||||
|
void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
|
||||||
|
void (*encode_fn)(const void *, ProtoWriteBuffer &));
|
||||||
std::vector<uint8_t> *get_buffer() const { return buffer_; }
|
std::vector<uint8_t> *get_buffer() const { return buffer_; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -690,26 +696,14 @@ template<typename T> void proto_encode_msg(const void *msg, ProtoWriteBuffer &bu
|
|||||||
static_cast<const T *>(msg)->encode(buf);
|
static_cast<const T *>(msg)->encode(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementation of encode_message - must be after ProtoMessage is defined
|
// Thin template wrapper; delegates to non-template core in proto.cpp.
|
||||||
template<typename T> inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const T &value, bool force) {
|
template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) {
|
||||||
this->encode_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>, force);
|
this->encode_sub_message(field_id, &value, &proto_encode_msg<T>);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-template core for encode_message
|
// Thin template wrapper; delegates to non-template core.
|
||||||
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value,
|
template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) {
|
||||||
void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force) {
|
this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>);
|
||||||
if (msg_length_bytes == 0 && !force)
|
|
||||||
return;
|
|
||||||
this->encode_field_raw(field_id, 2);
|
|
||||||
this->encode_varint_raw(msg_length_bytes);
|
|
||||||
#ifdef ESPHOME_DEBUG_API
|
|
||||||
uint8_t *start = this->pos_;
|
|
||||||
encode_fn(value, *this);
|
|
||||||
if (static_cast<uint32_t>(this->pos_ - start) != msg_length_bytes)
|
|
||||||
this->debug_check_encode_size_(field_id, msg_length_bytes, this->pos_ - start);
|
|
||||||
#else
|
|
||||||
encode_fn(value, *this);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
|
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
|
||||||
|
|||||||
@@ -690,15 +690,12 @@ class MessageType(TypeInfo):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def encode_func(self) -> str:
|
def encode_func(self) -> str:
|
||||||
return "encode_message"
|
return "encode_optional_sub_message"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def encode_content(self) -> str:
|
def encode_content(self) -> str:
|
||||||
# Singular message fields pass force=false (skip empty messages)
|
# Singular message fields skip encoding when empty
|
||||||
# The default for encode_nested_message is force=true (for repeated fields)
|
return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});"
|
||||||
return (
|
|
||||||
f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, false);"
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def decode_length(self) -> str:
|
def decode_length(self) -> str:
|
||||||
@@ -1322,9 +1319,9 @@ class FixedArrayRepeatedType(TypeInfo):
|
|||||||
"""Helper to generate encode statement for a single element."""
|
"""Helper to generate encode statement for a single element."""
|
||||||
if isinstance(self._ti, EnumType):
|
if isinstance(self._ti, EnumType):
|
||||||
return f"buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>({element}), true);"
|
return f"buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>({element}), true);"
|
||||||
# MessageType.encode_message doesn't have a force parameter
|
# Repeated message elements use encode_sub_message (force=true is default)
|
||||||
if isinstance(self._ti, MessageType):
|
if isinstance(self._ti, MessageType):
|
||||||
return f"buffer.{self._ti.encode_func}({self.number}, {element});"
|
return f"buffer.encode_sub_message({self.number}, {element});"
|
||||||
return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);"
|
return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1650,9 +1647,9 @@ class RepeatedTypeInfo(TypeInfo):
|
|||||||
"""Helper to generate encode call for a single element."""
|
"""Helper to generate encode call for a single element."""
|
||||||
if isinstance(self._ti, EnumType):
|
if isinstance(self._ti, EnumType):
|
||||||
return f"buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>({element}), true);"
|
return f"buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>({element}), true);"
|
||||||
# MessageType.encode_message doesn't have a force parameter
|
# Repeated message elements use encode_sub_message (force=true is default)
|
||||||
if isinstance(self._ti, MessageType):
|
if isinstance(self._ti, MessageType):
|
||||||
return f"buffer.{self._ti.encode_func}({self.number}, {element});"
|
return f"buffer.encode_sub_message({self.number}, {element});"
|
||||||
return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);"
|
return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
Reference in New Issue
Block a user