diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 06fcb5beece..90353b64026 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 9c0f072e3b9..e22b9599090 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 62073bb4401..5223ccc9706 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 53c18431d61..76c4d1637bd 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 06b8dd6e7b5..89a8b639100 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. @@ -291,7 +284,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 +294,26 @@ 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; - 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; + // First message: write header, record start position + const auto &first = messages[0]; + uint8_t header_len = write_plaintext_header(buffer_data + first.offset, first); + uint8_t *write_start = buffer_data + first.offset + HEADER_PADDING - header_len; + uint8_t *write_end = write_start + header_len + first.payload_size; + + // Subsequent messages: write header, then compact to close 0-3 byte gaps + for (size_t i = 1; i < messages.size(); i++) { + const auto &msg = messages[i]; + header_len = write_plaintext_header(buffer_data + msg.offset, msg); + uint8_t *src = buffer_data + msg.offset + HEADER_PADDING - header_len; + uint16_t msg_len = header_len + msg.payload_size; + 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