diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 06fcb5beec..90353b6402 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -100,6 +100,12 @@ const LogString *api_error_to_logstr(APIError err) { return LOG_STR("UNKNOWN"); } +#ifdef HELPER_LOG_PACKETS +void APIFrameHelper::log_packet_sending_(const void *data, uint16_t len) { + LOG_PACKET_SENDING(reinterpret_cast(data), len); +} +#endif + APIError APIFrameHelper::drain_overflow_and_handle_errors_() { if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) { int err = errno; @@ -115,7 +121,13 @@ APIError APIFrameHelper::drain_overflow_and_handle_errors_() { // Single-buffer write path: wraps in iovec and delegates. APIError APIFrameHelper::write_raw_buf_(const void *data, uint16_t len, ssize_t sent) { struct iovec iov = {const_cast(data), len}; - return this->write_raw_iov_(&iov, 1, len, sent); + APIError err = this->write_raw_iov_(&iov, 1, len, sent); +#ifdef HELPER_LOG_PACKETS + // Log after write/enqueue so re-entrant log sends can't corrupt data before it's sent + if (err == APIError::OK) + LOG_PACKET_SENDING(reinterpret_cast(data), len); +#endif + return err; } // Handles partial writes, errors, and overflow buffering. @@ -152,7 +164,7 @@ APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uin } } - // Full write completed (possible when called directly, not via write_raw_fast_iov_) + // Full write completed (possible when called directly, not via write_raw_fast_buf_) if (sent == static_cast(total_write_len)) return APIError::OK; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 9c0f072e3b..e22b959909 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -207,30 +207,25 @@ class APIFrameHelper { inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len) { if (this->overflow_buf_.empty()) [[likely]] { ssize_t sent = this->socket_->write(data, len); - if (sent == static_cast(len)) [[likely]] + if (sent == static_cast(len)) [[likely]] { +#ifdef HELPER_LOG_PACKETS + this->log_packet_sending_(data, len); +#endif return APIError::OK; + } // sent is -1 (WRITE_FAILED) or partial write count return this->write_raw_buf_(data, len, sent); } return this->write_raw_buf_(data, len, WRITE_NOT_ATTEMPTED); } - inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_iov_(const struct iovec *iov, int iovcnt, - uint16_t total_write_len) { - if (this->overflow_buf_.empty()) [[likely]] { - ssize_t sent = this->socket_->writev(iov, iovcnt); - if (sent == static_cast(total_write_len)) [[likely]] - return APIError::OK; - // sent is -1 (WRITE_FAILED) or partial write count - return this->write_raw_iov_(iov, iovcnt, total_write_len, sent); - } - return this->write_raw_iov_(iov, iovcnt, total_write_len, WRITE_NOT_ATTEMPTED); - } - // Out-of-line write paths: handle partial writes, errors, overflow buffering // sent: WRITE_NOT_ATTEMPTED (cold path), WRITE_FAILED (fast path write returned -1), or bytes sent (partial write) APIError write_raw_buf_(const void *data, uint16_t len, ssize_t sent = WRITE_NOT_ATTEMPTED); APIError write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent = WRITE_NOT_ATTEMPTED); +#ifdef HELPER_LOG_PACKETS + void log_packet_sending_(const void *data, uint16_t len); +#endif // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) std::unique_ptr socket_; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 62073bb440..5223ccc970 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -47,15 +47,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168; format_hex_pretty_to(hex_buf_, (buffer).data(), \ (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \ } while (0) -#define LOG_PACKET_SENDING(data, len) \ - do { \ - char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ - ESP_LOGVV(TAG, "Sending raw: %s", \ - format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \ - } while (0) #else #define LOG_PACKET_RECEIVED(buffer) ((void) 0) -#define LOG_PACKET_SENDING(data, len) ((void) 0) #endif /// Convert a noise error code to a readable error @@ -452,10 +445,10 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { buffer->type = type; return APIError::OK; } -// Encrypt a single noise message in place and populate the iovec. +// Encrypt a single noise message in place and return the encrypted frame length. // Returns APIError::OK on success. APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, const MessageInfo &msg, - struct iovec &iov_out) { + uint16_t &encrypted_len_out) { // Write noise header buf_start[0] = 0x01; // indicator // buf_start[1], buf_start[2] to be set after encryption @@ -482,9 +475,7 @@ APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, const M buf_start[1] = static_cast(mbuf.size >> 8); buf_start[2] = static_cast(mbuf.size); - // Populate iovec for this encrypted message - size_t msg_len = static_cast(3 + mbuf.size); // indicator + size + encrypted data - iov_out = {buf_start, msg_len}; + encrypted_len_out = static_cast(3 + mbuf.size); // indicator + size + encrypted data return APIError::OK; } @@ -499,13 +490,11 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuff MessageInfo msg{type, 0, static_cast(buffer.get_buffer()->size() - HEADER_PADDING - frame_footer_size_)}; uint8_t *buf_start = buffer.get_buffer()->data(); - struct iovec iov; - APIError aerr = this->encrypt_noise_message_(buf_start, msg, iov); + uint16_t encrypted_len; + APIError aerr = this->encrypt_noise_message_(buf_start, msg, encrypted_len); if (aerr != APIError::OK) return aerr; - // buf_start and iov.iov_base point to the same location - LOG_PACKET_SENDING(buf_start, iov.iov_len); - return this->write_raw_fast_buf_(buf_start, static_cast(iov.iov_len)); + return this->write_raw_fast_buf_(buf_start, encrypted_len); } APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) { @@ -514,26 +503,23 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s assert(!messages.empty()); #endif + // Noise messages are already contiguous in the buffer: + // HEADER_PADDING (7) exactly matches the fixed header size, and + // footer space (16) is consumed by the encryption MAC. uint8_t *buffer_data = buffer.get_buffer()->data(); - StaticVector iovs; + uint8_t *write_start = buffer_data + messages[0].offset; uint16_t total_write_len = 0; for (const auto &msg : messages) { uint8_t *buf_start = buffer_data + msg.offset; - struct iovec iov; - APIError aerr = this->encrypt_noise_message_(buf_start, msg, iov); + uint16_t encrypted_len; + APIError aerr = this->encrypt_noise_message_(buf_start, msg, encrypted_len); if (aerr != APIError::OK) return aerr; - iovs.push_back(iov); - total_write_len += iov.iov_len; + total_write_len += encrypted_len; } -#ifdef HELPER_LOG_PACKETS - for (const auto &iov : iovs) { - LOG_PACKET_SENDING(reinterpret_cast(iov.iov_base), iov.iov_len); - } -#endif - return this->write_raw_fast_iov_(iovs.data(), iovs.size(), total_write_len); + return this->write_raw_fast_buf_(write_start, total_write_len); } APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 53c18431d6..76c4d1637b 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -31,7 +31,7 @@ class APINoiseFrameHelper final : public APIFrameHelper { APIError state_action_(); APIError try_read_frame_(); APIError write_frame_(const uint8_t *data, uint16_t len); - APIError encrypt_noise_message_(uint8_t *buf_start, const MessageInfo &msg, struct iovec &iov_out); + APIError encrypt_noise_message_(uint8_t *buf_start, const MessageInfo &msg, uint16_t &encrypted_len_out); APIError init_handshake_(); APIError check_handshake_finished_(); void send_explicit_handshake_reject_(const LogString *reason); diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 06b8dd6e7b..a9e294de73 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -39,15 +39,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168; format_hex_pretty_to(hex_buf_, (buffer).data(), \ (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \ } while (0) -#define LOG_PACKET_SENDING(data, len) \ - do { \ - char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ - ESP_LOGVV(TAG, "Sending raw: %s", \ - format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \ - } while (0) #else #define LOG_PACKET_RECEIVED(buffer) ((void) 0) -#define LOG_PACKET_SENDING(data, len) ((void) 0) #endif /// Initialize the frame helper, returns OK if successful. @@ -234,14 +227,44 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { buffer->type = this->rx_header_parsed_type_; return APIError::OK; } +// Compute varint encoded length for a 16-bit value (1, 2, or 3 bytes). +ESPHOME_ALWAYS_INLINE static inline uint8_t varint_encoded_length_16(uint16_t value) { + return value < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : (value < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3); +} + +// Compute varint encoded length for an 8-bit value (1 or 2 bytes). +ESPHOME_ALWAYS_INLINE static inline uint8_t varint_encoded_length_8(uint8_t value) { + return value < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2; +} + +// Encode a 16-bit varint (1-3 bytes) using pre-computed length. +ESPHOME_ALWAYS_INLINE static inline void encode_varint_16(uint16_t value, uint8_t varint_len, uint8_t *p) { + if (varint_len >= 2) { + *p++ = static_cast(value | 0x80); + value >>= 7; + if (varint_len == 3) { + *p++ = static_cast(value | 0x80); + value >>= 7; + } + } + *p = static_cast(value); +} + +// Encode an 8-bit varint (1-2 bytes) using pre-computed length. +ESPHOME_ALWAYS_INLINE static inline void encode_varint_8(uint8_t value, uint8_t varint_len, uint8_t *p) { + if (varint_len == 2) { + *p++ = static_cast(value | 0x80); + *p = static_cast(value >> 7); + } else { + *p = value; + } +} + // Write plaintext header into pre-allocated padding before payload. // Returns the total header length (indicator + varints). ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_start, const MessageInfo &msg) { - // Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead - uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_THRESHOLD_1_BYTE - ? 1 - : (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3); - uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2; + uint8_t size_varint_len = varint_encoded_length_16(msg.payload_size); + uint8_t type_varint_len = varint_encoded_length_8(msg.message_type); uint8_t total_header_len = 1 + size_varint_len + type_varint_len; // Calculate where to start writing the header @@ -274,9 +297,9 @@ ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_ // Write the plaintext header buf_start[header_offset] = 0x00; // indicator - // Encode varints directly into buffer - encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1); - encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len); + // Encode varints directly into buffer using pre-computed lengths + encode_varint_16(msg.payload_size, size_varint_len, buf_start + header_offset + 1); + encode_varint_8(msg.message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len); return total_header_len; } @@ -291,7 +314,6 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWrite uint8_t header_len = write_plaintext_header(buffer_data, msg); uint8_t *msg_start = buffer_data + HEADER_PADDING - header_len; uint16_t msg_len = static_cast(header_len + msg.payload_size); - LOG_PACKET_SENDING(msg_start, msg_len); return this->write_raw_fast_buf_(msg_start, msg_len); } @@ -302,23 +324,28 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe assert(!messages.empty()); #endif uint8_t *buffer_data = buffer.get_buffer()->data(); - StaticVector iovs; - uint16_t total_write_len = 0; + uint8_t *write_start = nullptr; + uint8_t *write_end = nullptr; + // Write headers and compact messages to close 0-3 byte varint padding gaps. + // First iteration records start position via continue; subsequent iterations + // memmove to close gaps between messages. for (const auto &msg : messages) { uint8_t header_len = write_plaintext_header(buffer_data + msg.offset, msg); - uint8_t *msg_start = buffer_data + msg.offset + HEADER_PADDING - header_len; - size_t msg_len = static_cast(header_len + msg.payload_size); - iovs.push_back({msg_start, msg_len}); - total_write_len += msg_len; + uint8_t *src = buffer_data + msg.offset + HEADER_PADDING - header_len; + uint16_t msg_len = header_len + msg.payload_size; + if (write_start == nullptr) { + write_start = src; + write_end = src + msg_len; + continue; + } + if (src != write_end) { + memmove(write_end, src, msg_len); + } + write_end += msg_len; } -#ifdef HELPER_LOG_PACKETS - for (const auto &iov : iovs) { - LOG_PACKET_SENDING(reinterpret_cast(iov.iov_base), iov.iov_len); - } -#endif - return this->write_raw_fast_iov_(iovs.data(), iovs.size(), total_write_len); + return this->write_raw_fast_buf_(write_start, static_cast(write_end - write_start)); } } // namespace esphome::api