mirror of
https://github.com/esphome/esphome.git
synced 2025-09-26 15:12:21 +01:00
Implement zero-copy API for zwave_proxy
This commit is contained in:
@@ -2292,7 +2292,7 @@ message ZWaveProxyFrame {
|
|||||||
option (ifdef) = "USE_ZWAVE_PROXY";
|
option (ifdef) = "USE_ZWAVE_PROXY";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
|
||||||
bytes data = 1 [(fixed_array_size) = 257];
|
bytes data = 1 [(pointer_to_buffer) = true];
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ZWaveProxyRequestType {
|
enum ZWaveProxyRequestType {
|
||||||
|
@@ -32,6 +32,13 @@ extend google.protobuf.FieldOptions {
|
|||||||
optional string fixed_array_size_define = 50010;
|
optional string fixed_array_size_define = 50010;
|
||||||
optional string fixed_array_with_length_define = 50011;
|
optional string fixed_array_with_length_define = 50011;
|
||||||
|
|
||||||
|
// pointer_to_buffer: Use pointer instead of array for fixed-size byte fields
|
||||||
|
// When set, the field will be declared as a pointer (const uint8_t *data)
|
||||||
|
// instead of an array (uint8_t data[N]). This allows zero-copy on decode
|
||||||
|
// by pointing directly to the protobuf buffer. The buffer must remain valid
|
||||||
|
// until the message is processed (which is guaranteed for stack-allocated messages).
|
||||||
|
optional bool pointer_to_buffer = 50012 [default=false];
|
||||||
|
|
||||||
// container_pointer: Zero-copy optimization for repeated fields.
|
// container_pointer: Zero-copy optimization for repeated fields.
|
||||||
//
|
//
|
||||||
// When container_pointer is set on a repeated field, the generated message will
|
// When container_pointer is set on a repeated field, the generated message will
|
||||||
|
@@ -3029,12 +3029,9 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
|||||||
bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 1: {
|
case 1: {
|
||||||
const std::string &data_str = value.as_string();
|
// Use raw data directly to avoid allocation
|
||||||
this->data_len = data_str.size();
|
this->data = value.data();
|
||||||
if (this->data_len > 257) {
|
this->data_len = value.size();
|
||||||
this->data_len = 257;
|
|
||||||
}
|
|
||||||
memcpy(this->data, data_str.data(), this->data_len);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@@ -2929,11 +2929,11 @@ class UpdateCommandRequest final : public CommandProtoMessage {
|
|||||||
class ZWaveProxyFrame final : public ProtoDecodableMessage {
|
class ZWaveProxyFrame final : public ProtoDecodableMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 128;
|
static constexpr uint8_t MESSAGE_TYPE = 128;
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 33;
|
static constexpr uint8_t ESTIMATED_SIZE = 19;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "z_wave_proxy_frame"; }
|
const char *message_name() const override { return "z_wave_proxy_frame"; }
|
||||||
#endif
|
#endif
|
||||||
uint8_t data[257]{};
|
const uint8_t *data{nullptr};
|
||||||
uint16_t data_len{0};
|
uint16_t data_len{0};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
|
@@ -2126,12 +2126,7 @@ void UpdateCommandRequest::dump_to(std::string &out) const {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ZWAVE_PROXY
|
#ifdef USE_ZWAVE_PROXY
|
||||||
void ZWaveProxyFrame::dump_to(std::string &out) const {
|
void ZWaveProxyFrame::dump_to(std::string &out) const { dump_field(out, "data", this->data); }
|
||||||
MessageDumpHelper helper(out, "ZWaveProxyFrame");
|
|
||||||
out.append(" data: ");
|
|
||||||
out.append(format_hex_pretty(this->data, this->data_len));
|
|
||||||
out.append("\n");
|
|
||||||
}
|
|
||||||
void ZWaveProxyRequest::dump_to(std::string &out) const {
|
void ZWaveProxyRequest::dump_to(std::string &out) const {
|
||||||
MessageDumpHelper helper(out, "ZWaveProxyRequest");
|
MessageDumpHelper helper(out, "ZWaveProxyRequest");
|
||||||
dump_field(out, "type", static_cast<enums::ZWaveProxyRequestType>(this->type));
|
dump_field(out, "type", static_cast<enums::ZWaveProxyRequestType>(this->type));
|
||||||
|
@@ -182,6 +182,10 @@ class ProtoLengthDelimited {
|
|||||||
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
|
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
|
||||||
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
|
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
|
||||||
|
|
||||||
|
// Direct access to raw data without string allocation
|
||||||
|
const uint8_t *data() const { return this->value_; }
|
||||||
|
size_t size() const { return this->length_; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode the length-delimited data into an existing ProtoDecodableMessage instance.
|
* Decode the length-delimited data into an existing ProtoDecodableMessage instance.
|
||||||
*
|
*
|
||||||
|
@@ -61,14 +61,14 @@ void ZWaveProxy::loop() {
|
|||||||
}
|
}
|
||||||
ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr));
|
ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr));
|
||||||
if (this->api_connection_ != nullptr) {
|
if (this->api_connection_ != nullptr) {
|
||||||
// minimize copying to reduce CPU overhead
|
// Zero-copy: point directly to our buffer
|
||||||
|
this->outgoing_proto_msg_.data = this->buffer_.data();
|
||||||
if (this->in_bootloader_) {
|
if (this->in_bootloader_) {
|
||||||
this->outgoing_proto_msg_.data_len = this->buffer_index_;
|
this->outgoing_proto_msg_.data_len = this->buffer_index_;
|
||||||
} else {
|
} else {
|
||||||
// If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN
|
// If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN
|
||||||
this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1;
|
this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1;
|
||||||
}
|
}
|
||||||
std::memcpy(this->outgoing_proto_msg_.data, this->buffer_.data(), this->outgoing_proto_msg_.data_len);
|
|
||||||
this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE);
|
this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,7 +228,9 @@ void ZWaveProxy::parse_start_(uint8_t byte) {
|
|||||||
}
|
}
|
||||||
// Forward response (ACK/NAK/CAN) back to client for processing
|
// Forward response (ACK/NAK/CAN) back to client for processing
|
||||||
if (this->api_connection_ != nullptr) {
|
if (this->api_connection_ != nullptr) {
|
||||||
this->outgoing_proto_msg_.data[0] = byte;
|
// Store single byte in buffer and point to it
|
||||||
|
this->buffer_[0] = byte;
|
||||||
|
this->outgoing_proto_msg_.data = this->buffer_.data();
|
||||||
this->outgoing_proto_msg_.data_len = 1;
|
this->outgoing_proto_msg_.data_len = 1;
|
||||||
this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE);
|
this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
|
@@ -63,11 +63,11 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
|
|||||||
|
|
||||||
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
|
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
|
||||||
|
|
||||||
std::array<uint8_t, 4> home_id_{0, 0, 0, 0}; // Fixed buffer for home ID
|
std::array<uint8_t, 4> home_id_{0, 0, 0, 0}; // Fixed buffer for home ID
|
||||||
std::array<uint8_t, sizeof(api::ZWaveProxyFrame::data)> buffer_; // Fixed buffer for incoming data
|
std::array<uint8_t, 257> buffer_; // Fixed buffer for incoming data
|
||||||
uint8_t buffer_index_{0}; // Index for populating the data buffer
|
uint8_t buffer_index_{0}; // Index for populating the data buffer
|
||||||
uint8_t end_frame_after_{0}; // Payload reception ends after this index
|
uint8_t end_frame_after_{0}; // Payload reception ends after this index
|
||||||
uint8_t last_response_{0}; // Last response type sent
|
uint8_t last_response_{0}; // Last response type sent
|
||||||
ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START};
|
ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START};
|
||||||
bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode
|
bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode
|
||||||
|
|
||||||
|
@@ -353,12 +353,25 @@ def create_field_type_info(
|
|||||||
return FixedArrayRepeatedType(field, size_define)
|
return FixedArrayRepeatedType(field, size_define)
|
||||||
return RepeatedTypeInfo(field)
|
return RepeatedTypeInfo(field)
|
||||||
|
|
||||||
# Check for fixed_array_size option on bytes fields
|
# Check for mutually exclusive options on bytes fields
|
||||||
if (
|
if field.type == 12:
|
||||||
field.type == 12
|
has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False)
|
||||||
and (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None
|
fixed_size = get_field_opt(field, pb.fixed_array_size, None)
|
||||||
):
|
|
||||||
return FixedArrayBytesType(field, fixed_size)
|
if has_pointer_to_buffer and fixed_size is not None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Field '{field.name}' has both pointer_to_buffer and fixed_array_size. "
|
||||||
|
"These options are mutually exclusive. Use pointer_to_buffer for zero-copy "
|
||||||
|
"or fixed_array_size for traditional array storage."
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_pointer_to_buffer:
|
||||||
|
# Zero-copy pointer approach - no size needed, will use size_t for length
|
||||||
|
return PointerToBytesBufferType(field, None)
|
||||||
|
|
||||||
|
if fixed_size is not None:
|
||||||
|
# Traditional fixed array approach with copy
|
||||||
|
return FixedArrayBytesType(field, fixed_size)
|
||||||
|
|
||||||
# Special handling for bytes fields
|
# Special handling for bytes fields
|
||||||
if field.type == 12:
|
if field.type == 12:
|
||||||
@@ -818,6 +831,80 @@ class BytesType(TypeInfo):
|
|||||||
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes
|
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes
|
||||||
|
|
||||||
|
|
||||||
|
class PointerToBytesBufferType(TypeInfo):
|
||||||
|
"""Type for bytes fields that use pointer_to_buffer option for zero-copy."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def can_use_dump_field(cls) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, field: descriptor.FieldDescriptorProto, size: int | None = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__(field)
|
||||||
|
# Size is not used for pointer_to_buffer - we always use size_t for length
|
||||||
|
self.array_size = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cpp_type(self) -> str:
|
||||||
|
return "const uint8_t*"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_value(self) -> str:
|
||||||
|
return "nullptr"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reference_type(self) -> str:
|
||||||
|
return "const uint8_t*"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def const_reference_type(self) -> str:
|
||||||
|
return "const uint8_t*"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public_content(self) -> list[str]:
|
||||||
|
# Use uint16_t for length - max packet size is well below 65535
|
||||||
|
# Add pointer and length fields
|
||||||
|
return [
|
||||||
|
f"const uint8_t* {self.field_name}{{nullptr}};",
|
||||||
|
f"uint16_t {self.field_name}_len{{0}};",
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def encode_content(self) -> str:
|
||||||
|
return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decode_length_content(self) -> str | None:
|
||||||
|
# Decode directly stores the pointer to avoid allocation
|
||||||
|
return f"""case {self.number}: {{
|
||||||
|
// Use raw data directly to avoid allocation
|
||||||
|
this->{self.field_name} = value.data();
|
||||||
|
this->{self.field_name}_len = value.size();
|
||||||
|
break;
|
||||||
|
}}"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decode_length(self) -> str | None:
|
||||||
|
# This is handled in decode_length_content
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wire_type(self) -> WireType:
|
||||||
|
"""Get the wire type for this bytes field."""
|
||||||
|
return WireType.LENGTH_DELIMITED # Uses wire type 2
|
||||||
|
|
||||||
|
def dump(self, name: str) -> str:
|
||||||
|
return f"format_hex_pretty(this->{name}, this->{name}_len)"
|
||||||
|
|
||||||
|
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||||
|
return f"size.add_length({self.number}, this->{self.field_name}_len);"
|
||||||
|
|
||||||
|
def get_estimated_size(self) -> int:
|
||||||
|
# field ID + length varint + typical data (assume small for pointer fields)
|
||||||
|
return self.calculate_field_id_size() + 2 + 16
|
||||||
|
|
||||||
|
|
||||||
class FixedArrayBytesType(TypeInfo):
|
class FixedArrayBytesType(TypeInfo):
|
||||||
"""Special type for fixed-size byte arrays."""
|
"""Special type for fixed-size byte arrays."""
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user